<commit_before>const path = require('path');
const _ = require('./util/lodash-extended');
const fs = require('fs-extra');
const sass = require('node-sass');
const pathExists = require('path-exists');
const whicheverExists = require('./util/whichever-exists');
const csso = require('csso');
const uglifyjs = require('uglify-js');

module.exports = function(optimize) {
  const mappingsFileName = whicheverExists('src/wrapper.json', 'src/page.json');
  const mappings = fs.readJsonSync(mappingsFileName).files;
  const destDir = 'dest';
  const normalizedMappings = _(mappings)
    .mapValues(normalizeMapping)
    .thru(optimizer(_.mapValues, checkMinified))
    .thru(optimizer(_.mapValues, concat))
    .mapValues(finalizeDest)
    .value();
  const consumerMappings = _.mapValues(normalizedMappings, mapToRelativeDest);

  _.forEach(normalizedMappings, _.partial(processGroup, optimize));

  return consumerMappings;

  function normalizeMapping(mapping) {
    if (_.isString(mapping)) {
      return {dest: '', src: mapping};
    } else if (_.isArray(mapping)) {
      return _.map(mapping, normalizeMapping);
    } else if (_.isObject(mapping) && _.isArray(mapping.src)) {
      const dest = mapping.dest || '';
      return _.map(mapping.src, src => ({dest, src}));
    } else if (_.isObject(mapping)) {
      return {dest: mapping.dest || '', src: mapping.src};
    }
  }

  function checkMinified(group) {
    return _.isArray(group) ? _.map(group, mapping => check(mapping)) : check(group);

    function check(mapping) {
      const {src} = mapping;
      const extname = path.extname(src);
      if (extname === 'js' || extname === 'css') {
        const basenameStripped = path.basename(src, extname);
        const dirname = path.dirname(src);
        mapping.isMinified = path.extname(basenameStripped) === '.min';
        mapping.withMinified = pathExists.sync(path.join(dirname, basenameStripped + '.min' + extname));
      }
      return mapping;
    }
  }

  function finalizeDest(fileMapping) {
    const extMap = {
      scss: 'css',
      sass: 'css'
    };

    return _.isArray(fileMapping) ? _.map(fileMapping, finalize) : finalize(fileMapping);

    function finalize(mapping) {
      const extnameSrc = path.extname(mapping.src).substr(1);

      if (extnameSrc && !path.extname(mapping.dest)) {
        const extnameDest = extMap[extnameSrc] || extnameSrc;
        const basenameDest = path.basename(mapping.src, extnameSrc) + extnameDest;
        mapping.dest = path.join(mapping.dest, basenameDest);
      }

      mapping.relativeDest = mapping.dest;
      mapping.dest = path.join(destDir, mapping.dest);
      return mapping;
    }
  }

  function processGroup(optimize, group) {
    _.isArray(group) ? _.forEach(group, mapping => process(mapping)) : process(mapping);

    function process(mapping) {
      const {dest, src, minified} = mapping;
      const fileType = path.extname(src).substr(1);

      if (_.includes(['sass', 'scss', 'js'], fileType)) {
        const result = optimize && !minified ? minify({src: compile(src), dest}) : compile(src);
        return fs.outputFileSync(dest, result);
      }

      return fs.copySync(src, dest, {dereference: true});
    }

    function compile(src) {
      const fileType = path.extname(src).substr(1);
      if (fileType === 'scss' || fileType === 'sass') {
        return sass.renderSync({file: src}).css.toString();
      }
      return fs.readFileSync(src).toString();
    }

    function minify(mapping) {
      const {dest, src} = mapping;
      const fileType = path.extname(dest).substr(1);
      if (fileType === 'css') {
        return csso.minify(src).css;
      }
      if (fileType === 'js') {
        return uglifyjs.minify(src, {fromString: true}).code;
      }
      return src;
    }
  }

  function concat(group) {
    return group;
  }

  function optimizer(processor, task) {
    return optimize ? _.partialRight(processor, task) : _.identity;
  }

  function mapToRelativeDest(group) {
    return _.isArray(group) ? _.map(group, mapping => mapping.relativeDest) : group.relativeDest;
  }
}
<commit_msg>fix minified file naming, use existing min files when present<commit_after>const path = require('path');
const _ = require('./util/lodash-extended');
const fs = require('fs-extra');
const sass = require('node-sass');
const pathExists = require('path-exists');
const whicheverExists = require('./util/whichever-exists');
const csso = require('csso');
const uglifyjs = require('uglify-js');

module.exports = function(optimize) {
  const mappingsFileName = whicheverExists('src/wrapper.json', 'src/page.json');
  const mappings = fs.readJsonSync(mappingsFileName).files;
  const destDir = 'dest';
  const normalizedMappings = _(mappings)
    .mapValues(normalizeMapping)
    .thru(optimizer(_.mapValues, checkMinified))
    .thru(optimizer(_.mapValues, concat))
    .mapValues(_.partial(finalizeMapping, optimize))
    .value();
  const consumerMappings = _.mapValues(normalizedMappings, mapToRelativeDest);

  _.forEach(normalizedMappings, _.partial(processGroup, optimize));

  return consumerMappings;

  function normalizeMapping(mapping) {
    if (_.isString(mapping)) {
      return {dest: '', src: mapping};
    } else if (_.isArray(mapping)) {
      return _.map(mapping, normalizeMapping);
    } else if (_.isObject(mapping) && _.isArray(mapping.src)) {
      const dest = mapping.dest || '';
      return _.map(mapping.src, src => ({dest, src}));
    } else if (_.isObject(mapping)) {
      return {dest: mapping.dest || '', src: mapping.src};
    }
  }

  function checkMinified(group) {
    return _.isArray(group) ? _.map(group, mapping => check(mapping)) : check(group);

    function check(mapping) {
      const {src} = mapping;
      const extname = _.trim(path.extname(src), '.');
      if (extname === 'js' || extname === 'css') {
        const dirname = path.dirname(src);
        const basenameStripped = _.trim(path.basename(src, extname), '.');
        mapping.isMinified = _.trim(path.extname(basenameStripped), '.') === 'min';
        mapping.withMinified = pathExists.sync(path.join(dirname, basenameStripped + '.min.' + extname));
      }
      return mapping;
    }
  }

  function finalizeMapping(optimize, fileMapping) {
    const extMap = {
      scss: 'css',
      sass: 'css'
    };

    return _.isArray(fileMapping) ? _.map(fileMapping, finalize) : finalize(fileMapping);

    function finalize(mapping) {
      let {src, dest} = mapping;
      const extnameSrc = path.extname(src).substr(1);

      // If src specifies a file name (not just directory) and dest does not, we
      // reuse the file name from src.
      if (extnameSrc && !path.extname(dest)) {
        const extnameDest = extMap[extnameSrc] || extnameSrc;
        const basenameDest = path.basename(src, extnameSrc) + extnameDest;
        dest = path.join(dest, basenameDest);
      }

      // If we're optimizing, and the file extension is js or css, add 'min'.
      if (optimize && !mapping.isMinified) {
        const extnameDest = path.extname(dest).substr(1);

        if (_.includes(['js', 'css'], extnameDest)) {
          const basenameMin = path.basename(dest, extnameDest) + 'min.' + extnameDest;
          src = mapping.withMinified ? path.join(path.dirname(src), basenameMin) : src;
          dest = path.join(path.dirname(dest), basenameMin);
        }
      }

      mapping.src = src;
      mapping.relativeDest = dest;
      mapping.dest = path.join(destDir, dest);
      return mapping;
    }
  }

  function processGroup(optimize, group) {
    _.isArray(group) ? _.forEach(group, mapping => process(mapping)) : process(mapping);

    function process(mapping) {
      const {dest, src, isMinified} = mapping;
      const fileType = path.extname(src).substr(1);

      if (_.includes(['sass', 'scss', 'js'], fileType)) {
        const result = optimize && !isMinified ? minify({src: compile(src), dest}) : compile(src);
        return fs.outputFileSync(dest, result);
      }

      return fs.copySync(src, dest, {dereference: true});
    }

    function compile(src) {
      const fileType = path.extname(src).substr(1);
      if (fileType === 'scss' || fileType === 'sass') {
        return sass.renderSync({file: src}).css.toString();
      }
      return fs.readFileSync(src).toString();
    }

    function minify(mapping) {
      const {dest, src} = mapping;
      const fileType = path.extname(dest).substr(1);
      if (fileType === 'css') {
        return csso.minify(src).css;
      }
      if (fileType === 'js') {
        return uglifyjs.minify(src, {fromString: true}).code;
      }
      return src;
    }
  }

  function concat(group) {
    return group;
  }

  function optimizer(processor, task) {
    return optimize ? _.partialRight(processor, task) : _.identity;
  }

  function mapToRelativeDest(group) {
    return _.isArray(group) ? _.map(group, mapping => mapping.relativeDest) : group.relativeDest;
  }
}
<|endoftext|>


<commit_before>;(function ($) {

    function add_forwards(element) {
        var forward = element.attr('data-autocomplete-light-forward');
        if (forward !== undefined) {
            forward = forward.split(',');

            var prefix = $(element).getFormPrefix();
            var data_forward = {};

            for (var key in forward) {
                var name = prefix + forward[key];
                data_forward[forward[key]] = $('[name=' + name + ']').val();
            }

            return JSON.stringify(data_forward);
        }
    }

    $(document).on('autocompleteLightInitialize', '[data-autocomplete-light-function=selectize]', function() {
        var $elem = $(this);

        // If we have a URL, setup AJAX call
        var load;
        var url = $elem.attr('data-autocomplete-light-url');
        if (url) {
            load = function(query, callback) {
                var data = {
                    q: query,
                    forward: add_forwards($elem)
                }
                if ($elem.val() instanceof Array) {
                    data['selected'] = $elem.val().join(',');
                }
                $.ajax({
                    url: url,
                    dataType: 'json',
                    delay: 250,
                    //cache: true,
                    data: data,
                    error: function() {
                        // TODO: handle error better
                        console.log('Error calling `' + $elem.attr('name') + '` selectize AJAX endpoint');
                    },
                    success: function(data) {
                        if ($elem.attr('data-tags')) {
                            $.each(data.results, function(index, item) {
                                item.value = item.text;
                            });
                        }
                        callback(data.results);
                    }
                });
            };
        };

        // This widget has a clear button
        $elem.find('option[value=""]').remove();

        // Bind selectize
        $elem.selectize({
            delimiter: $elem.attr('data-tags') ? [','] : null,
            allowEmptyOption: ! $elem.is('required'),
            preload: true,
            load: load
        });

    });

})(yl.jQuery);
<commit_msg>Settings tweak<commit_after>;(function ($) {

    function add_forwards(element) {
        var forward = element.attr('data-autocomplete-light-forward');
        if (forward !== undefined) {
            forward = forward.split(',');

            var prefix = $(element).getFormPrefix();
            var data_forward = {};

            for (var key in forward) {
                var name = prefix + forward[key];
                data_forward[forward[key]] = $('[name=' + name + ']').val();
            }

            return JSON.stringify(data_forward);
        }
    }

    $(document).on('autocompleteLightInitialize', '[data-autocomplete-light-function=selectize]', function() {
        var $elem = $(this);

        // If we have a URL, setup AJAX call
        var load;
        var url = $elem.attr('data-autocomplete-light-url');
        if (url) {
            load = function(query, callback) {
                var data = {
                    q: query,
                    forward: add_forwards($elem)
                }
                if ($elem.val() instanceof Array) {
                    data['selected'] = $elem.val().join(',');
                }
                $.ajax({
                    url: url,
                    dataType: 'json',
                    delay: 250,
                    data: data,
                    error: function() {
                        // TODO: handle error better
                        console.log('Error calling `' + $elem.attr('name') + '` selectize AJAX endpoint');
                    },
                    success: function(data) {
                        if ($elem.attr('data-tags')) {
                            $.each(data.results, function(index, item) {
                                item.value = item.text;
                            });
                        }
                        callback(data.results);
                    }
                });
            };
        };

        // This widget has a clear button
        $elem.find('option[value=""]').remove();

        // Bind selectize
        $elem.selectize({
            delimiter: $elem.attr('data-tags') ? ',' : null,
            allowEmptyOption: ! $elem.is('required'),
            preload: true,
            load: load
        });

    });

})(yl.jQuery);
<|endoftext|>


<commit_before>/*
 * Copyright 2012, Mozilla Foundation and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

define(function(require, exports, module) {

'use strict';
var examiner = exports;

var promise = require('util/promise');
var util = require('util/util');
var assert = require('test/assert');
var stati = require('test/status').stati;

/**
 * Test harness data
 */
examiner._suites = {};

/**
 * The gap between tests when running async
 */
var delay = 10;

/**
 * Add a test suite. Generally used like:
 * test.addSuite('foo', require('path/to/foo'));
 */
examiner.addSuite = function(name, suite) {
  examiner._suites[name] = new Suite(name, suite);
};

/**
 * Provide a clone of examiner._suites for external consumption
 */
Object.defineProperty(examiner, 'suites', {
  get: function() {
    var reply = {};
    Object.keys(examiner._suites).forEach(function(name) {
      reply[name] = examiner._suites[name];
    });
    return reply;
  },
  enumerable: true
});

/**
 * Run all the tests asynchronously
 */
examiner.run = function(options, callback) {
  var onComplete = function() {
    if (typeof callback === 'function') {
      callback.call();
    }
  };

  var suiteNames = Object.keys(examiner._suites);
  return util.promiseEach(suiteNames, function(suiteName) {
    return examiner._suites[suiteName].run(options);
  }).then(onComplete);
};

/**
 * Create a JSON object suitable for serialization
 */
examiner.toRemote = function() {
  return {
    suites: Object.keys(examiner._suites).map(function(suiteName) {
      return examiner._suites[suiteName].toRemote();
    }),
    summary: {
      checks: this.checks,
      status: this.status
    }
  };
};

/**
 * Reset all the tests to their original state
 */
examiner.reset = function() {
  Object.keys(examiner._suites).forEach(function(suiteName) {
    examiner._suites[suiteName].reset();
  }, this);
};

/**
 * The number of checks in this set of test suites is the sum of the checks in
 * the test suites.
 */
Object.defineProperty(examiner, 'checks', {
  get: function() {
    return  Object.keys(examiner._suites).reduce(function(current, suiteName) {
      return current + examiner._suites[suiteName].checks;
    }, 0);
  },
  enumerable: true
});

/**
 * The status of this set of test suites is the worst of the statuses of the
 * contained test suites.
 */
Object.defineProperty(examiner, 'status', {
  get: function() {
    return Object.keys(examiner._suites).reduce(function(status, suiteName) {
      var suiteStatus = examiner._suites[suiteName].status;
      return status.index > suiteStatus.index ? status : suiteStatus;
    }, stati.notrun);
  },
  enumerable: true
});

/**
 * Return a test summary
 */
examiner.detailedResultLog = function(name) {
  var reply = name + ' Summary: ' + this.status.name +
              ' (' + this.checks + ' checks)';

  if (this.status !== stati.pass) {
    reply += '\n';
    Object.keys(this.suites).forEach(function(suiteName) {
      var suite = examiner._suites[suiteName];
      if (suite.status !== stati.pass) {
        reply += '- ' + suite.name + ': ' + suite.status.name +
                 ' (funcs=' + Object.keys(suite.tests).length +
                 ', checks=' + suite.checks +
                 ', skipped=' + suite.skippedCount + ')\n';

        Object.keys(suite.tests).forEach(function(testName) {
          var test = suite.tests[testName];
          if (test.status !== stati.pass) {
            reply += '  - ' + test.name + ': ' + test.status.name + '\n';
            test.failures.forEach(function(failure) {
              reply += '    - ' + failure.message + '\n';
              if (failure.params) {
                reply += '      - P1: ' + failure.p1 + '\n';
                reply += '      - P2: ' + failure.p2 + '\n';
              }
            }, this);
          }
        }, this);
      }
    }, this);
  }

  return reply;
};

/**
 * A suite is a group of tests
 */
function Suite(suiteName, suite) {
  this.name = suiteName.replace(/gclitest\//, '');
  this.suite = suite;

  this.tests = {};

  Object.keys(suite).forEach(function(testName) {
    if (testName !== 'setup' && testName !== 'shutdown') {
      var test = new Test(this, testName, suite[testName]);
      this.tests[testName] = test;
    }
  }, this);
}

/**
 * Reset all the tests to their original state
 */
Suite.prototype.reset = function() {
  Object.keys(this.tests).forEach(function(testName) {
    this.tests[testName].reset();
  }, this);
};

/**
 * Run all the tests in this suite asynchronously (includes doing a setup and
 * shutdown of the suite)
 */
Suite.prototype.run = function(options) {
  this._setup(options);

  var onComplete = function() {
    this._shutdown(options);
  }.bind(this);

  var testNames = Object.keys(this.tests);
  return util.promiseEach(testNames, function(testName) {
    return this.tests[testName].run(options);
  }.bind(this)).then(onComplete);
};

/**
 * Create a JSON object suitable for serialization
 */
Suite.prototype.toRemote = function() {
  return {
    name: this.name,
    tests: Object.keys(this.tests).map(function(testName) {
      return this.tests[testName].toRemote();
    }.bind(this))
  };
};

/**
 * The number of checks in this suite is the sum of the checks in the contained
 * tests.
 */
Object.defineProperty(Suite.prototype, 'checks', {
  get: function() {
    return Object.keys(this.tests).reduce(function(prevChecks, testName) {
      return prevChecks + this.tests[testName].checks;
    }.bind(this), 0);
  },
  enumerable: true
});

/**
 * The status of a test suite is the worst of the statuses of the contained
 * tests.
 */
Object.defineProperty(Suite.prototype, 'status', {
  get: function() {
    return Object.keys(this.tests).reduce(function(prevStatus, testName) {
      var suiteStatus = this.tests[testName].status;
      return prevStatus.index > suiteStatus.index ? prevStatus : suiteStatus;
    }.bind(this), stati.notrun);
  },
  enumerable: true
});

/**
 * The status of a test suite is the worst of the statuses of the contained
 * tests.
 */
Object.defineProperty(Suite.prototype, 'skippedCount', {
  get: function() {
    return Object.keys(this.tests).reduce(function(count, testName) {
      return count + this.tests[testName].skipped.length;
    }.bind(this), 0);
  },
  enumerable: true
});

/**
 * Defensively setup the test suite
 */
Suite.prototype._setup = function(options) {
  if (typeof this.suite.setup !== 'function') {
    return true;
  }

  try {
    this.suite.setup(options);
    return true;
  }
  catch (ex) {
    this._logToAllTests('' + ex);
    console.error(ex);
    if (ex.stack) {
      console.error(ex.stack);
    }
    return false;
  }
};

/**
 * Defensively shutdown the test suite
 */
Suite.prototype._shutdown = function(options) {
  if (typeof this.suite.shutdown !== 'function') {
    return true;
  }

  try {
    this.suite.shutdown(options);
    return true;
  }
  catch (ex) {
    this._logToAllTests('' + ex);
    console.error(ex);
    if (ex.stack) {
      console.error(ex.stack);
    }
    return false;
  }
};

/**
 * Something has gone wrong that affects all tests in this Suite
 */
Suite.prototype._logToAllTests = function(message) {
  var priorCurrentTest = assert.currentTest;
  Object.keys(this.tests).forEach(function(testName) {
    assert.currentTest = this.tests[testName];
    assert.ok(false, message);
  }.bind(this));
  assert.currentTest = priorCurrentTest;
};

/**
 * A test represents data about a single test function
 */
function Test(suite, name, func) {
  this.suite = suite;
  this.name = name;
  this.func = func;
  this.title = name.replace(/^test/, '').replace(/([A-Z])/g, ' $1');

  this.failures = [];
  this.skipped = [];
  this.status = stati.notrun;
  this.checks = 0;
}

Test.prototype.toString = function() {
  return this.suite.name + '.' + this.name;
};

/**
 * Reset the test to its original state
 */
Test.prototype.reset = function() {
  this.failures = [];
  this.skipped = [];
  this.status = stati.notrun;
  this.checks = 0;
};

/**
 * Run all the tests in this suite asynchronously
 */
Test.prototype.run = function(options) {
  assert.currentTest = this;
  this.status = stati.executing;

  if (assert.testLogging) {
    console.log('START: ' + assert.currentTest);
  }

  var deferred = promise.defer();
  setTimeout(function() {
    try {
      var reply = this.func.apply(this.suite, [ options ]);
      deferred.resolve(reply);
    }
    catch (ex) {
      deferred.reject(ex);
    }
  }.bind(this), delay);

  var onDone = function() {
    if (this.status === stati.executing) {
      this.status = this.failures.length === 0 ? stati.pass : stati.fail;
    }

    if (assert.testLogging) {
      console.log('END: ' + assert.currentTest);
    }

    assert.currentTest = null;
  }.bind(this);

  var onError = function(ex) {
    assert.ok(false, assert.currentTest + ': ' + ex);
    util.errorHandler(ex);
    onDone();
  };

  return deferred.promise.then(onDone, onError);
};

/**
 * Create a JSON object suitable for serialization
 */
Test.prototype.toRemote = function() {
  return {
    name: this.name,
    title: this.title,
    status: this.status,
    failures: this.failures,
    checks: this.checks
  };
};

});
<commit_msg>refactor-864370: Report test failure better<commit_after>/*
 * Copyright 2012, Mozilla Foundation and contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

define(function(require, exports, module) {

'use strict';
var examiner = exports;

var promise = require('util/promise');
var util = require('util/util');
var assert = require('test/assert');
var stati = require('test/status').stati;

/**
 * Test harness data
 */
examiner._suites = {};

/**
 * The gap between tests when running async
 */
var delay = 10;

/**
 * Add a test suite. Generally used like:
 * test.addSuite('foo', require('path/to/foo'));
 */
examiner.addSuite = function(name, suite) {
  examiner._suites[name] = new Suite(name, suite);
};

/**
 * Provide a clone of examiner._suites for external consumption
 */
Object.defineProperty(examiner, 'suites', {
  get: function() {
    var reply = {};
    Object.keys(examiner._suites).forEach(function(name) {
      reply[name] = examiner._suites[name];
    });
    return reply;
  },
  enumerable: true
});

/**
 * Run all the tests asynchronously
 */
examiner.run = function(options, callback) {
  var onComplete = function() {
    if (typeof callback === 'function') {
      callback.call();
    }
  };

  var suiteNames = Object.keys(examiner._suites);
  return util.promiseEach(suiteNames, function(suiteName) {
    return examiner._suites[suiteName].run(options);
  }).then(onComplete);
};

/**
 * Create a JSON object suitable for serialization
 */
examiner.toRemote = function() {
  return {
    suites: Object.keys(examiner._suites).map(function(suiteName) {
      return examiner._suites[suiteName].toRemote();
    }),
    summary: {
      checks: this.checks,
      status: this.status
    }
  };
};

/**
 * Reset all the tests to their original state
 */
examiner.reset = function() {
  Object.keys(examiner._suites).forEach(function(suiteName) {
    examiner._suites[suiteName].reset();
  }, this);
};

/**
 * The number of checks in this set of test suites is the sum of the checks in
 * the test suites.
 */
Object.defineProperty(examiner, 'checks', {
  get: function() {
    return  Object.keys(examiner._suites).reduce(function(current, suiteName) {
      return current + examiner._suites[suiteName].checks;
    }, 0);
  },
  enumerable: true
});

/**
 * The status of this set of test suites is the worst of the statuses of the
 * contained test suites.
 */
Object.defineProperty(examiner, 'status', {
  get: function() {
    return Object.keys(examiner._suites).reduce(function(status, suiteName) {
      var suiteStatus = examiner._suites[suiteName].status;
      return status.index > suiteStatus.index ? status : suiteStatus;
    }, stati.notrun);
  },
  enumerable: true
});

/**
 * Return a test summary
 */
examiner.detailedResultLog = function(name) {
  var reply = name + ' Summary: ' + this.status.name +
              ' (' + this.checks + ' checks)';

  if (this.status !== stati.pass) {
    reply += '\n';
    Object.keys(this.suites).forEach(function(suiteName) {
      var suite = examiner._suites[suiteName];
      if (suite.status !== stati.pass) {
        reply += '- ' + suite.name + ': ' + suite.status.name +
                 ' (funcs=' + Object.keys(suite.tests).length +
                 ', checks=' + suite.checks +
                 ', skipped=' + suite.skippedCount + ')\n';

        Object.keys(suite.tests).forEach(function(testName) {
          var test = suite.tests[testName];
          if (test.status !== stati.pass) {
            reply += '  - ' + test.name + ': ' + test.status.name + '\n';
            test.failures.forEach(function(failure) {
              reply += '    - ' + failure.message + '\n';
              if (failure.params) {
                reply += '      - P1: ' + failure.p1 + '\n';
                reply += '      - P2: ' + failure.p2 + '\n';
              }
            }, this);
          }
        }, this);
      }
    }, this);
  }

  return reply;
};

/**
 * A suite is a group of tests
 */
function Suite(suiteName, suite) {
  this.name = suiteName.replace(/gclitest\//, '');
  this.suite = suite;

  this.tests = {};

  Object.keys(suite).forEach(function(testName) {
    if (testName !== 'setup' && testName !== 'shutdown') {
      var test = new Test(this, testName, suite[testName]);
      this.tests[testName] = test;
    }
  }, this);
}

/**
 * Reset all the tests to their original state
 */
Suite.prototype.reset = function() {
  Object.keys(this.tests).forEach(function(testName) {
    this.tests[testName].reset();
  }, this);
};

/**
 * Run all the tests in this suite asynchronously (includes doing a setup and
 * shutdown of the suite)
 */
Suite.prototype.run = function(options) {
  this._setup(options);

  var onComplete = function() {
    this._shutdown(options);
  }.bind(this);

  var testNames = Object.keys(this.tests);
  return util.promiseEach(testNames, function(testName) {
    return this.tests[testName].run(options);
  }.bind(this)).then(onComplete);
};

/**
 * Create a JSON object suitable for serialization
 */
Suite.prototype.toRemote = function() {
  return {
    name: this.name,
    tests: Object.keys(this.tests).map(function(testName) {
      return this.tests[testName].toRemote();
    }.bind(this))
  };
};

/**
 * The number of checks in this suite is the sum of the checks in the contained
 * tests.
 */
Object.defineProperty(Suite.prototype, 'checks', {
  get: function() {
    return Object.keys(this.tests).reduce(function(prevChecks, testName) {
      return prevChecks + this.tests[testName].checks;
    }.bind(this), 0);
  },
  enumerable: true
});

/**
 * The status of a test suite is the worst of the statuses of the contained
 * tests.
 */
Object.defineProperty(Suite.prototype, 'status', {
  get: function() {
    return Object.keys(this.tests).reduce(function(prevStatus, testName) {
      var suiteStatus = this.tests[testName].status;
      return prevStatus.index > suiteStatus.index ? prevStatus : suiteStatus;
    }.bind(this), stati.notrun);
  },
  enumerable: true
});

/**
 * The status of a test suite is the worst of the statuses of the contained
 * tests.
 */
Object.defineProperty(Suite.prototype, 'skippedCount', {
  get: function() {
    return Object.keys(this.tests).reduce(function(count, testName) {
      return count + this.tests[testName].skipped.length;
    }.bind(this), 0);
  },
  enumerable: true
});

/**
 * Defensively setup the test suite
 */
Suite.prototype._setup = function(options) {
  if (typeof this.suite.setup !== 'function') {
    return true;
  }

  try {
    this.suite.setup(options);
    return true;
  }
  catch (ex) {
    this._logToAllTests('' + ex);
    console.error(ex);
    if (ex.stack) {
      console.error(ex.stack);
    }
    return false;
  }
};

/**
 * Defensively shutdown the test suite
 */
Suite.prototype._shutdown = function(options) {
  if (typeof this.suite.shutdown !== 'function') {
    return true;
  }

  try {
    this.suite.shutdown(options);
    return true;
  }
  catch (ex) {
    this._logToAllTests('' + ex);
    console.error(ex);
    if (ex.stack) {
      console.error(ex.stack);
    }
    return false;
  }
};

/**
 * Something has gone wrong that affects all tests in this Suite
 */
Suite.prototype._logToAllTests = function(message) {
  var priorCurrentTest = assert.currentTest;
  Object.keys(this.tests).forEach(function(testName) {
    assert.currentTest = this.tests[testName];
    assert.ok(false, message);
  }.bind(this));
  assert.currentTest = priorCurrentTest;
};

/**
 * A test represents data about a single test function
 */
function Test(suite, name, func) {
  this.suite = suite;
  this.name = name;
  this.func = func;
  this.title = name.replace(/^test/, '').replace(/([A-Z])/g, ' $1');

  this.failures = [];
  this.skipped = [];
  this.status = stati.notrun;
  this.checks = 0;
}

Test.prototype.toString = function() {
  return this.suite.name + '.' + this.name;
};

/**
 * Reset the test to its original state
 */
Test.prototype.reset = function() {
  this.failures = [];
  this.skipped = [];
  this.status = stati.notrun;
  this.checks = 0;
};

/**
 * Run all the tests in this suite asynchronously
 */
Test.prototype.run = function(options) {
  assert.currentTest = this;
  this.status = stati.executing;

  if (assert.testLogging) {
    console.log('START: ' + assert.currentTest);
  }

  var deferred = promise.defer();
  setTimeout(function() {
    try {
      var reply = this.func.apply(this.suite, [ options ]);
      deferred.resolve(reply);
    }
    catch (ex) {
      deferred.reject(ex);
    }
  }.bind(this), delay);

  var onDone = function() {
    if (this.status === stati.executing) {
      this.status = this.failures.length === 0 ? stati.pass : stati.fail;
    }

    if (assert.testLogging) {
      console.log('END: ' + assert.currentTest);
    }

    assert.currentTest = null;
  }.bind(this);

  var onError = function(err) {
    assert.ok(false, 'Returned promise, rejected with: ' + toString(err));
    onDone();
  };

  return deferred.promise.then(onDone, onError);
};

/**
 * Object.toString could be a lot better
 */
function toString(err) {
  // Convert err to a string
  if (typeof err === 'string') {
    return err;
  }

  // If an object has it's own toString, that's probably the best way
  if (err.toString !== Object.prototype.toString) {
    return err.toString();
  }

  // Next best is JSON, if the object is JSONable
  try {
    return JSON.stringify(err);
  }
  catch (ex) {
    // Resort to Object.toString otherwise
    err = '' + err;
  }
}

/**
 * Create a JSON object suitable for serialization
 */
Test.prototype.toRemote = function() {
  return {
    name: this.name,
    title: this.title,
    status: this.status,
    failures: this.failures,
    checks: this.checks
  };
};

});
<|endoftext|>


<commit_before>// Copyright 2013 The Polymer Authors. All rights reserved.
// Use of this source code is goverened by a BSD-style
// license that can be found in the LICENSE file.

(function(scope) {
  'use strict';

  var EventTarget = scope.wrappers.EventTarget;
  var Selection = scope.wrappers.Selection;
  var mixin = scope.mixin;
  var registerWrapper = scope.registerWrapper;
  var renderAllPending = scope.renderAllPending;
  var unwrap = scope.unwrap;
  var unwrapIfNeeded = scope.unwrapIfNeeded;
  var wrap = scope.wrap;

  var OriginalWindow = window.Window;
  var originalGetComputedStyle = window.getComputedStyle;
  var originalGetSelection = window.getSelection;

  function Window(impl) {
    EventTarget.call(this, impl);
  }
  Window.prototype = Object.create(EventTarget.prototype);

  OriginalWindow.prototype.getComputedStyle = function(el, pseudo) {
    return wrap(this || window).getComputedStyle(unwrapIfNeeded(el), pseudo);
  };

  OriginalWindow.prototype.getSelection = function() {
    return wrap(this || window).getSelection();
  };

  // Work around for https://bugzilla.mozilla.org/show_bug.cgi?id=943065
  delete window.getComputedStyle;
  delete window.getSelection;

  ['addEventListener', 'removeEventListener', 'dispatchEvent'].forEach(
      function(name) {
        OriginalWindow.prototype[name] = function() {
          var w = wrap(this || window);
          return w[name].apply(w, arguments);
        };

        // Work around for https://bugzilla.mozilla.org/show_bug.cgi?id=943065
        delete window[name];
      });

  mixin(Window.prototype, {
    getComputedStyle: function(el, pseudo) {
      renderAllPending();
      return originalGetComputedStyle.call(unwrap(this), unwrapIfNeeded(el),
                                           pseudo);
    },
    getSelection: function() {
      renderAllPending();
      return new Selection(originalGetSelection.call(unwrap(this)));
    },

    get document() {
      return wrap(unwrap(this).document);
    }
  });

  registerWrapper(OriginalWindow, Window, window);

  scope.wrappers.Window = Window;

})(window.ShadowDOMPolyfill);
<commit_msg>Wrap window getDefaultComputedStyle<commit_after>// Copyright 2013 The Polymer Authors. All rights reserved.
// Use of this source code is goverened by a BSD-style
// license that can be found in the LICENSE file.

(function(scope) {
  'use strict';

  var EventTarget = scope.wrappers.EventTarget;
  var Selection = scope.wrappers.Selection;
  var mixin = scope.mixin;
  var registerWrapper = scope.registerWrapper;
  var renderAllPending = scope.renderAllPending;
  var unwrap = scope.unwrap;
  var unwrapIfNeeded = scope.unwrapIfNeeded;
  var wrap = scope.wrap;

  var OriginalWindow = window.Window;
  var originalGetComputedStyle = window.getComputedStyle;
  var originalGetDefaultComputedStyle = window.getDefaultComputedStyle;
  var originalGetSelection = window.getSelection;

  function Window(impl) {
    EventTarget.call(this, impl);
  }
  Window.prototype = Object.create(EventTarget.prototype);

  OriginalWindow.prototype.getComputedStyle = function(el, pseudo) {
    return wrap(this || window).getComputedStyle(unwrapIfNeeded(el), pseudo);
  };

  // Mozilla proprietary extension.
  if (originalGetDefaultComputedStyle) {
    OriginalWindow.prototype.getDefaultComputedStyle = function(el, pseudo) {
      return wrap(this || window).getDefaultComputedStyle(
          unwrapIfNeeded(el), pseudo);
    };
  }

  OriginalWindow.prototype.getSelection = function() {
    return wrap(this || window).getSelection();
  };

  // Work around for https://bugzilla.mozilla.org/show_bug.cgi?id=943065
  delete window.getComputedStyle;
  delete window.getSelection;

  ['addEventListener', 'removeEventListener', 'dispatchEvent'].forEach(
      function(name) {
        OriginalWindow.prototype[name] = function() {
          var w = wrap(this || window);
          return w[name].apply(w, arguments);
        };

        // Work around for https://bugzilla.mozilla.org/show_bug.cgi?id=943065
        delete window[name];
      });

  mixin(Window.prototype, {
    getComputedStyle: function(el, pseudo) {
      renderAllPending();
      return originalGetComputedStyle.call(unwrap(this), unwrapIfNeeded(el),
                                           pseudo);
    },
    getSelection: function() {
      renderAllPending();
      return new Selection(originalGetSelection.call(unwrap(this)));
    },

    get document() {
      return wrap(unwrap(this).document);
    }
  });

  // Mozilla proprietary extension.
  if (originalGetDefaultComputedStyle) {
    Window.prototype.getDefaultComputedStyle = function(el, pseudo) {
      renderAllPending();
      return originalGetDefaultComputedStyle.call(unwrap(this),
          unwrapIfNeeded(el),pseudo);
    };
  }

  registerWrapper(OriginalWindow, Window, window);

  scope.wrappers.Window = Window;

})(window.ShadowDOMPolyfill);
<|endoftext|>


<commit_before>var React = require('react');
var ReactDOM = require('react-dom');
var utilities = require("../../../node_modules/simple-react-utilities/js/utilities.js");

import cardData from '../json/monsters.json';
import filterOptions from '../json/filter-configs.json';
import Filter from '../../../node_modules/card-filters/src/www/js/filter.js';
import { CardBook } from '../../../node_modules/card-filters/src/www/js/cards.js'
import { Card } from '../../../node_modules/card-filters/src/www/js/components/card.js'
import { FilterButtonGroup } from '../../../node_modules/card-filters/src/www/js/components/filter-button-group.js';
import { FilterButton } from '../../../node_modules/card-filters/src/www/js/components/filter-button.js';
import { TextInput } from '../../../node_modules/simple-react-forms/form-fields/text-input.js';
import { ShowHideButton } from '../../../node_modules/card-filters/src/www/js/components/show-hide-button.js';
import { SubmitButton } from '../../../node_modules/simple-react-forms/form-fields/submit-button.js';

class MonsterBook extends React.Component {
  /**
   * constructor for CardBook
   * @param  {object} props [element properties/attributes passed in at initialization]
   */
  constructor(props) {
    super(props);

    this.searchFilter = this.searchFilter.bind(this);
    this.renderCards = this.renderCards.bind(this);
  }

  searchFilter(activate,deactivate) {
    var filter = new Filter(cardData);
    var _this = this;

    var searchOptions = {
      filterName : "level",
      filterFunc : filter.by.search,
      onActivate : activate,
      onDeactivate : deactivate
    }

    function update(e) {
      if (typeof activate === "function") {
        activate('search',{filterFunc : filter.by.search, filterArgs : [false, e.target.value]}); // TODO: this boolean indicates description search
      }
    }

    function clearInput() {
      let inputElem = document.querySelector('.form-control');
      inputElem.value = "";
      activate('search',{filterFunc : filter.by.search, filterArgs : [false, ""]}); // TODO: this boolean indicates description search
    }

    return

            <TextInput className="filterByText" label="Search" type="search" name="filterByText" onChange={update} />

x

  }

  crFilters(activate,deactivate) {
    var filter = new Filter(cardData);
    var buttonOptions = {
      filterName : "CR",
      filterFunc : filter.by.value,
      onActivate : activate,
      onDeactivate : deactivate
    }

    return

CR

              <FilterButtonGroup cardData={cardData} buttonOptions={buttonOptions} filters={filterOptions.cr} />

  }
  /**
   * [typeFilters description]
   * @param  {[type]} activate
   * @param  {[type]} deactivate [description]
   * @return {[type]}            [description]
   */
  typeFilters(activate,deactivate) {
    var filter = new Filter(cardData);

    function update(e) {
      let activeBtn = document.querySelector(".active.type-filter-btn");
      let activeFilterProp = activeBtn.getAttribute('data-filter-prop');
      let activeFilterValue = activeBtn.getAttribute('data-filter-value');

      if (e.target.className.indexOf("active") === -1) {
        activeBtn.className = activeBtn.className.replace(/active/g,'');
        e.target.className += " active";

        activate('type', {
          filterArgs : [e.target.getAttribute('data-filter-prop'), e.target.getAttribute('data-filter-value')],
          filterFunc : filter.by.match
        });

        deactivate('type', {
          filterArgs : [activeFilterProp, activeFilterValue],
          filterFunc : filter.by.match,
          exclude : true
        });
      }

    }

    function showAll(e) {
      let activeBtn = document.querySelector(".active.type-filter-btn");
      let activeFilterProp = activeBtn.getAttribute('data-filter-prop');
      let activeFilterValue = activeBtn.getAttribute('data-filter-value');

      if (e.target.className.indexOf("active") === -1) {
        activeBtn.className = activeBtn.className.replace(/active/g,'');
        e.target.className += " active";

        activate('type', {
          filterArgs : [e.target.getAttribute('data-filter-prop'), e.target.getAttribute('data-filter-value')],
          filterFunc : filter.by.all
        });

        deactivate('type', {
          filterArgs : [activeFilterProp, activeFilterValue],
          filterFunc : filter.by.match,
          exclude : true
        });
      }
    }

    function getClassName(index) {
      const activeClass = index === 0 ? ' active' : '';
      return "btn type-filter-btn" + activeClass;
    }

    function buttons() {

    }
    var buttons = function() {
      return filterOptions.type.map((button, index) => {
        return (

            {button.label}

        )
      });
    }

    return

                {buttons()}

Show all types (slow)

  }

  sourceFilters(activate,deactivate) {
    var filter = new Filter(cardData);
    var buttonOptions = {
      filterName : "Source",
      filterFunc : filter.by.match,
      onActivate : activate,
      onDeactivate : deactivate
    }

    return

Source

              <FilterButtonGroup cardData={cardData} buttonOptions={buttonOptions} filters={filterOptions.source} />

  }

  renderCards(cardsData, saveFunc) {
    var i = 0;
    var cardsArr = cardsData || [];

    function cardId(card) {
      return card.name.toLowerCase()
                      .replace(/\s/g,'-')
                      .replace(/'/g,'')
                      .replace(/\//g,'-');
    }

    function abilityScoreModifier(score) {
      let modifier = Math.floor((score - 10)/2);
      return modifier > -1 ? '+'+modifier : modifier;
    }

    function creatureSpecialAbilities(creature) {
      if(Array.isArray(creature.special_abilities)) {
        return creature.special_abilities.map((ability) => {
          return (

              {ability.name}.

          )
        });
      }
    }

    function creatureActions(creature) {
      if(Array.isArray(creature.actions)) {
        return creature.actions.map((action) => {
          return (

              {action.name}.

          )
        });
      }
    }

    function saveCard(e) {
      const cardObject = utilities.getObjectByName(cardsData,e.target.getAttribute('data-card-name'));
      e.target.className += " saved"

      saveFunc(cardObject);
    }

    function searchString(creatureName) {

      return "https://www.google.com/search?safe=active&tbm=isch&q="+creatureName.toLowerCase().replace(/\s/g,'+');;
    }

    let cards =

      {cardsArr.map((creature) => {
        const index = Math.floor(Math.random() * 1001)
        return

{creature.name} [IMG]
Save

          <ShowHideButton target={"."+cardId(creature)+index+" .card-content"} showText="+" hideText="-" />
          <ShowHideButton target={"."+cardId(creature)+index+" .card-content"} showText="+" hideText="-" startClosed="true" />

{creature.size} {creature.type}, {creature.alignment}

Armor class {creature.armor_class}

Hit Points {creature.hit_points}

Speed {creature.speed}

                  STR
                  DEX
                  CON
                  INT
                  WIS
                  CHA

                  {creature.strength} ({abilityScoreModifier(creature.strength)})
                  {creature.dexterity} ({abilityScoreModifier(creature.dexterity)})
                  {creature.constitution} ({abilityScoreModifier(creature.constitution)})
                  {creature.intelligence} ({abilityScoreModifier(creature.intelligence)})
                  {creature.wisdom} ({abilityScoreModifier(creature.wisdom)})
                  {creature.charisma} ({abilityScoreModifier(creature.charisma)})

Vulnerabilities {creature.vulnerable}

Damage Immunities {creature.immune}

Condition Immunities {creature.conditionImmune}

Senses {creature.senses}
     Perception {creature.passive}

Languages {creature.languages}

Challenge {creature.cr}

              {creatureSpecialAbilities(creature)}

Actions

              {creatureActions(creature)}

{creature.source}

})}

;

    if (!cardsData || cardsData.length === 0) {
      cards =

No cards matching the selected filters

    }

    return cards;
  }

  /**
   * puts everything in the DOM
   */
  render() {
    const filter = new Filter(cardData);
    const startData = filter.by.match(['type','aberration'],cardData);
    const startFilter = {
      'type' : {
        'filterArgs' : [['type','aberration']],
        'filterFunc' : filter.by.match
      }
    }

    function uniqueData () {
      let uniqueData = [];
      let isUnique = true;
      let i = 0, j = 0, l = 0, k = 0;

      for (i = 0, l = cardData.length; i < l; i++) {
        isUnique = true;

        for (j = 0, k = uniqueData.length; j < k; j++) {
          if (cardData[i].name === uniqueData[j].name) {
            isUnique = false;
          }
        }

        if (isUnique) {
          uniqueData.push(cardData[i])
        }

      }

      uniqueData = utilities.sortObjectsByProp(uniqueData, 'name');

     return
 {cardData.length}

      {uniqueData.length}

      {JSON.stringify(uniqueData)}

    }

    function uniqueProps (prop) {
      let uniqueProps = [];
      let isUnique = true;
      let i = 0, j = 0, l = 0, k = 0;

      for (i = 0, l = cardData.length; i < l; i++) {
        isUnique = true;

        for (j = 0, k = uniqueProps.length; j < k; j++) {
          if (cardData[i].source === uniqueProps[j].source) {
            isUnique = false;
          }
        }

        if (isUnique) {
          uniqueProps.push(cardData[i].source)
        }

      }

     return
{uniqueProps.map((prop) => {
      return
{prop}

     })}
;
    }

    return

              <CardBook
                label="Monsters"
                cardData={cardData}
                startData={startData}
                startFilter={startFilter}
                searchFilter={this.searchFilter}
                navigation={this.typeFilters}
                filters={[this.crFilters, this.sourceFilters]}
                renderCards={this.renderCards}
                advancedFilters={[/*this.componentFilters,this.optionsFilters,this.schoolFilters,this.durationFilters,this.castingTimeFilters,this.rangeFilters,this.sourceFilters*/]}
              />

;
  }
}

ReactDOM.render(<MonsterBook />, document.querySelector('main'));
<commit_msg>clean up<commit_after>var React = require('react');
var ReactDOM = require('react-dom');
var utilities = require("../../../node_modules/simple-react-utilities/js/utilities.js");

import cardData from '../json/monsters.json';
import filterOptions from '../json/filter-configs.json';
import Filter from '../../../node_modules/card-filters/src/www/js/filter.js';
import { CardBook } from '../../../node_modules/card-filters/src/www/js/cards.js'
import { Card } from '../../../node_modules/card-filters/src/www/js/components/card.js'
import { FilterButtonGroup } from '../../../node_modules/card-filters/src/www/js/components/filter-button-group.js';
import { FilterButton } from '../../../node_modules/card-filters/src/www/js/components/filter-button.js';
import { TextInput } from '../../../node_modules/simple-react-forms/form-fields/text-input.js';
import { ShowHideButton } from '../../../node_modules/card-filters/src/www/js/components/show-hide-button.js';
import { SubmitButton } from '../../../node_modules/simple-react-forms/form-fields/submit-button.js';

class MonsterBook extends React.Component {
  /**
   * constructor for CardBook
   * @param  {object} props [element properties/attributes passed in at initialization]
   */
  constructor(props) {
    super(props);

    this.searchFilter = this.searchFilter.bind(this);
    this.renderCards = this.renderCards.bind(this);
  }

  searchFilter(activate,deactivate) {
    var filter = new Filter(cardData);
    var _this = this;

    var searchOptions = {
      filterName : "level",
      filterFunc : filter.by.search,
      onActivate : activate,
      onDeactivate : deactivate
    }

    function update(e) {
      if (typeof activate === "function") {
        activate('search',{filterFunc : filter.by.search, filterArgs : [false, e.target.value]}); // TODO: this boolean indicates description search
      }
    }

    function clearInput() {
      let inputElem = document.querySelector('.form-control');
      inputElem.value = "";
      activate('search',{filterFunc : filter.by.search, filterArgs : [false, ""]}); // TODO: this boolean indicates description search
    }

    return

            <TextInput className="filterByText" label="Search" type="search" name="filterByText" onChange={update} />

x

  }

  crFilters(activate,deactivate) {
    var filter = new Filter(cardData);
    var buttonOptions = {
      filterName : "CR",
      filterFunc : filter.by.value,
      onActivate : activate,
      onDeactivate : deactivate
    }

    return

CR

              <FilterButtonGroup cardData={cardData} buttonOptions={buttonOptions} filters={filterOptions.cr} />

  }
  /**
   * [typeFilters description]
   * @param  {[type]} activate
   * @param  {[type]} deactivate [description]
   * @return {[type]}            [description]
   */
  typeFilters(activate,deactivate) {
    var filter = new Filter(cardData);

    function update(e) {
      let activeBtn = document.querySelector(".active.type-filter-btn");
      let activeFilterProp = activeBtn.getAttribute('data-filter-prop');
      let activeFilterValue = activeBtn.getAttribute('data-filter-value');

      if (e.target.className.indexOf("active") === -1) {
        activeBtn.className = activeBtn.className.replace(/active/g,'');
        e.target.className += " active";

        activate('type', {
          filterArgs : [e.target.getAttribute('data-filter-prop'), e.target.getAttribute('data-filter-value')],
          filterFunc : filter.by.match
        });

        deactivate('type', {
          filterArgs : [activeFilterProp, activeFilterValue],
          filterFunc : filter.by.match,
          exclude : true
        });
      }

    }

    function showAll(e) {
      let activeBtn = document.querySelector(".active.type-filter-btn");
      let activeFilterProp = activeBtn.getAttribute('data-filter-prop');
      let activeFilterValue = activeBtn.getAttribute('data-filter-value');

      if (e.target.className.indexOf("active") === -1) {
        activeBtn.className = activeBtn.className.replace(/active/g,'');
        e.target.className += " active";

        activate('type', {
          filterArgs : [e.target.getAttribute('data-filter-prop'), e.target.getAttribute('data-filter-value')],
          filterFunc : filter.by.all
        });

        deactivate('type', {
          filterArgs : [activeFilterProp, activeFilterValue],
          filterFunc : filter.by.match,
          exclude : true
        });
      }
    }

    function getClassName(index) {
      const activeClass = index === 0 ? ' active' : '';
      return "btn type-filter-btn" + activeClass;
    }

    var buttons = function() {
      return filterOptions.type.map((button, index) => {
        return (

            {button.label}

        )
      });
    }

    return

                {buttons()}

Show all types (slow)

  }

  sourceFilters(activate,deactivate) {
    var filter = new Filter(cardData);
    var buttonOptions = {
      filterName : "Source",
      filterFunc : filter.by.match,
      onActivate : activate,
      onDeactivate : deactivate
    }

    return

Source

              <FilterButtonGroup cardData={cardData} buttonOptions={buttonOptions} filters={filterOptions.source} />

  }

  renderCards(cardsData, saveFunc) {
    var i = 0;
    var cardsArr = cardsData || [];

    function cardId(card) {
      return card.name.toLowerCase()
                      .replace(/\s/g,'-')
                      .replace(/'/g,'')
                      .replace(/\//g,'-');
    }

    function abilityScoreModifier(score) {
      let modifier = Math.floor((score - 10)/2);
      return modifier > -1 ? '+'+modifier : modifier;
    }

    function creatureSpecialAbilities(creature) {
      if(Array.isArray(creature.special_abilities)) {
        return creature.special_abilities.map((ability) => {
          return (

              {ability.name}.

          )
        });
      }
    }

    function creatureActions(creature) {
      if(Array.isArray(creature.actions)) {
        return creature.actions.map((action) => {
          return (

              {action.name}.

          )
        });
      }
    }

    function saveCard(e) {
      const cardObject = utilities.getObjectByName(cardsData,e.target.getAttribute('data-card-name'));
      e.target.className += " saved"

      saveFunc(cardObject);
    }

    function searchString(creatureName) {

      return "https://www.google.com/search?safe=active&tbm=isch&q="+creatureName.toLowerCase().replace(/\s/g,'+');;
    }

    let cards =

      {cardsArr.map((creature) => {
        const index = Math.floor(Math.random() * 1001)
        return

{creature.name} [IMG]
Save

          <ShowHideButton target={"."+cardId(creature)+index+" .card-content"} showText="+" hideText="-" />
          <ShowHideButton target={"."+cardId(creature)+index+" .card-content"} showText="+" hideText="-" startClosed="true" />

{creature.size} {creature.type}, {creature.alignment}

Armor class {creature.armor_class}

Hit Points {creature.hit_points}

Speed {creature.speed}

                  STR
                  DEX
                  CON
                  INT
                  WIS
                  CHA

                  {creature.strength} ({abilityScoreModifier(creature.strength)})
                  {creature.dexterity} ({abilityScoreModifier(creature.dexterity)})
                  {creature.constitution} ({abilityScoreModifier(creature.constitution)})
                  {creature.intelligence} ({abilityScoreModifier(creature.intelligence)})
                  {creature.wisdom} ({abilityScoreModifier(creature.wisdom)})
                  {creature.charisma} ({abilityScoreModifier(creature.charisma)})

Vulnerabilities {creature.vulnerable}

Damage Immunities {creature.immune}

Condition Immunities {creature.conditionImmune}

Senses {creature.senses}
     Perception {creature.passive}

Languages {creature.languages}

Challenge {creature.cr}

              {creatureSpecialAbilities(creature)}

Actions

              {creatureActions(creature)}

{creature.source}

})}

;

    if (!cardsData || cardsData.length === 0) {
      cards =

No cards matching the selected filters

    }

    return cards;
  }

  /**
   * puts everything in the DOM
   */
  render() {
    const filter = new Filter(cardData);
    const startData = filter.by.match(['type','aberration'],cardData);
    const startFilter = {
      'type' : {
        'filterArgs' : [['type','aberration']],
        'filterFunc' : filter.by.match
      }
    }

    function uniqueData () {
      let uniqueData = [];
      let isUnique = true;
      let i = 0, j = 0, l = 0, k = 0;

      for (i = 0, l = cardData.length; i < l; i++) {
        isUnique = true;

        for (j = 0, k = uniqueData.length; j < k; j++) {
          if (cardData[i].name === uniqueData[j].name) {
            isUnique = false;
          }
        }

        if (isUnique) {
          uniqueData.push(cardData[i])
        }

      }

      uniqueData = utilities.sortObjectsByProp(uniqueData, 'name');

     return
 {cardData.length}

      {uniqueData.length}

      {JSON.stringify(uniqueData)}

    }

    function uniqueProps (prop) {
      let uniqueProps = [];
      let isUnique = true;
      let i = 0, j = 0, l = 0, k = 0;

      for (i = 0, l = cardData.length; i < l; i++) {
        isUnique = true;

        for (j = 0, k = uniqueProps.length; j < k; j++) {
          if (cardData[i].source === uniqueProps[j].source) {
            isUnique = false;
          }
        }

        if (isUnique) {
          uniqueProps.push(cardData[i].source)
        }

      }

     return
{uniqueProps.map((prop) => {
      return
{prop}

     })}
;
    }

    return

              <CardBook
                label="Monsters"
                cardData={cardData}
                startData={startData}
                startFilter={startFilter}
                searchFilter={this.searchFilter}
                navigation={this.typeFilters}
                filters={[this.crFilters, this.sourceFilters]}
                renderCards={this.renderCards}
                advancedFilters={[/*this.componentFilters,this.optionsFilters,this.schoolFilters,this.durationFilters,this.castingTimeFilters,this.rangeFilters,this.sourceFilters*/]}
              />

;
  }
}

ReactDOM.render(<MonsterBook />, document.querySelector('main'));
<|endoftext|>


<commit_before>/*
Copyright (c) 2015, EDINA
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
  list of conditions and the following disclaimer in the documentation and/or
  other materials provided with the distribution.
* Neither the name of EDINA nor the names of its contributors may be used to
  endorse or promote products derived from this software without specific prior
  written permission.

THIS SOFTWARE IS PROVIDED BY EDINA ''AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL EDINA BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
*/

"use strict";

/* global pcapi */

/**
 * TODO
 */
/* jshint ignore:start */
define(['map', 'file', 'utils', 'settings', 'records', './mbtiles', './ext/pcapi'], function(/* jshint ignore:end */
    map, file, utils, settings, records, mbtiles, pcapi){// jshint ignore:line

    var layersDir;
    var TILES_FOLDER = 'features';
    var LAYER_TYPE = 'mbtiles';

    /**
     * check for layers inside a dir
     * @param dir the name of the directory to look in
     * @callback
     */
    var checkForLayers = function(dir, callback) {
        var directoryReader = dir.createReader();
        directoryReader.readEntries(function(entries) {
            var id, name, options, fileEntry;

            for (var i = 0; i < entries.length; i++) {
                fileEntry = entries[i];

                if (fileEntry.name.indexOf('-journal') === -1) {
                    id = fileEntry.name;
                    name = fileEntry.name;
                    options = {
                        fileURL: fileEntry.toURL()
                    };

                    map.addLayerToLayersList(id, name, LAYER_TYPE, options);
                }
            }
        });
    };

    /**
     * Download layer to disk.
     */
    var downloadLayer = function(event){
        var $target = $(event.target);
        var layer = $("label[for='"+$target.attr('id')+"']").text();

        var itemUrl;

        // COBWEB uses the anonymous user id for downloading layers,
        // so if its defined use it.
        var annonUserId = utils.getAnonymousUserId();
        if(annonUserId){
            itemUrl = pcapi.buildFSUserUrl(utils.getAnonymousUserId(), TILES_FOLDER, layer);
        }
        else{
            itemUrl = pcapi.buildFSUrl(TILES_FOLDER, layer);
        }

        var target = file.appendFile(layersDir, layer);

        // Download or delete the editor from the device
        if($target.prop('checked')){
            utils.showPageLoadingMsg('Download Layer '+layer);
            file.ftDownload(
                itemUrl,
                target,
                function(){
                    $.mobile.loading('hide');
                    checkForLayers(layersDir, function(layers){
                        $.mobile.changePage('map.html');
                    });
                }
            );
        }else{
            utils.showPageLoadingMsg('Delete Layer '+layer);
            file.deleteFile(layer, layersDir, function(){
                map.removeLayersFromLayerList(layer);
                $.mobile.loading('hide');
            });

        }
    };

    /**
     * create layers list for downloading on download section
     */
    var savedLayersPage = function(){
        var $layersList = $(".layers-list");
        var layers = map.getLayersFromLayerList();
        var list = [];

        utils.showPageLoadingMsg('Checking for Layers ');

        //fetch the metadata from mbtiles and add them to the listview
        pcapi.getFSItems(TILES_FOLDER, utils.getAnonymousUserId()).then(function(data){

            $.each(data.metadata, $.proxy(function(i, item){
                var fileName = item.substring(item.lastIndexOf('/') + 1, item.length);
                var checked = '';
                if (layers.hasOwnProperty(fileName)) {
                    checked = 'checked';
                }
                list.push('
'+fileName+'\
                          <input data-role="flipswitch"\
                                name="flip-checkbox-'+ i +'"\
                                id="flip-checkbox-'+ i +'"\
                                class="download-layer" type="checkbox" '+ checked +'>
');
            }, this));
            $layersList.html(list.join(""));
            $('input[data-role="flipswitch"]', ".layers-list").flipswitch();
            $layersList.listview("refresh");
            $.mobile.loading('hide');
        });
    };

    /**
     * Show a POI layer adding it if necessary
     * @param layerMetadata {Object}
     *     - id {String} layer identifier
     *     - name {String} layer name
     *     - options {Object} options for the layer
     */
    var showLayer = function(layerMetadata) {
        var $target = $(event.target);
        //var layerName = $("label[for='"+$target.attr('id')+"']").text();
        var layerName = layerMetadata.name;

        var layerType;
        // if($target.prop('checked')){
        if (!map.layerExists(layerName)) {
            layerType = layerName.match(/\.(.*)$/)[1];

            switch (layerType) {
                case 'db':
                case 'mbtiles':
                    mbtiles.showMbTilesLayer(layerName);
                    break;
                case 'kml':
                    var kml = file.appendFile(layersDir, layerName);
                    var layer = map.addKMLLayer({
                        id: layerName,
                        url: kml
                    });

                    layer.events.register('loadend', this, function() {
                        map.zoomToExtent(layer.getDataExtent());
                    });

                    $('#map-page-layers-panel').panel('close');
                    break;
                default:
                    utils.inform('Don\'t know how to display ' + layerName);
            }
        }
        else {
            map.showLayerByName(layerName);
        }
    };

    /**
     * Hide a layer
     * @param layerMetadata {Object}
     *     - id {String} layer identifier
     *     - name {String} layer name
     *     - options {Object} options for the layer
     */
    var hideLayer = function(layerMetadata) {
        var layerName = layerMetadata.name;
        map.hideLayerByName(layerName);
    };

    /**
     * Zom to the extent of the layer
     * @param layerMetadata {Object}
     *     - id {String} layer identifier
     *     - name {String} layer name
     *     - options {Object} options for the layer
     */
    var zoomToExtentLayer = function(layerMetadata) {
        var layerName = layerMetadata.name;
        var layer = map.getLayerByName(layerName);
        if (layer) {
            map.zoomToExtent(layer);
        }
    };

    /**
     * Initialize the tiles directory and scan for tiles
     */
    var initLayersLocation = function() {
        var root;

        if (utils.isMobileDevice()) {
            // check settings first for defined pcapi root url
            root = settings.get('pcapi-url');

            if (root === undefined) {
                root = utils.getPCAPIURL();
            }

            file.createDir({
                name: TILES_FOLDER,
                success: function(dir) {
                    layersDir = dir;
                    mbtiles.init(layersDir);
                    checkForLayers(layersDir);

                }
            });

        }
        else {
            root = 'http://' + location.hostname;
            if (location.port) {
                root += ':' + location.port;
            }
        }

        //initialize pcapi
        pcapi.init({url: root, version: utils.getPCAPIVersion()});
    };

   /**
     * Perform initialization actions for the the plugin
     */
    var initPlugin = function() {
        initLayersLocation();

        // Listen for on/off events in the layer list
        map.suscribeToLayersControl({
            enableLayer: function(event, layerMetadata) {
                if (layerMetadata.type === LAYER_TYPE) {
                    showLayer(layerMetadata);
                }
            },
            disableLayer: function(event, layerMetadata) {
                if (layerMetadata.type === LAYER_TYPE) {
                    hideLayer(layerMetadata);
                }
            },
            clickLayer: function(event, layerMetadata) {
                if (layerMetadata.type === LAYER_TYPE) {
                    zoomToExtentLayer(layerMetadata);
                }
            }
        });

        $(document).on('pageshow', '#saved-layers-page', savedLayersPage);

        //download layer event
        $(document).on('change', '.download-layer', downloadLayer);

        // click on layer on map side panel
        $(document).on('change', '.show-layer', showLayer);

        $(document).on('change', '#settings-pcapi-url', function(){
            pcapi.setCloudProviderUrl(
                $('#settings-pcapi-url option:selected').val());
        });

        $('head').prepend('<link rel="stylesheet" href="plugins/overlays/css/style.css" type="text/css" />');
    };

    initPlugin();

});
<commit_msg>handle empty feature directory: edina/fieldtrip-cobweb-project#115<commit_after>/*
Copyright (c) 2015, EDINA
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
  list of conditions and the following disclaimer in the documentation and/or
  other materials provided with the distribution.
* Neither the name of EDINA nor the names of its contributors may be used to
  endorse or promote products derived from this software without specific prior
  written permission.

THIS SOFTWARE IS PROVIDED BY EDINA ''AS IS'' AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL EDINA BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
*/

"use strict";

/* global pcapi */

/**
 * TODO
 */
/* jshint ignore:start */
define(['map', 'file', 'utils', 'settings', 'records', './mbtiles', './ext/pcapi'], function(/* jshint ignore:end */
    map, file, utils, settings, records, mbtiles, pcapi){// jshint ignore:line

    var layersDir;
    var TILES_FOLDER = 'features';
    var LAYER_TYPE = 'mbtiles';

    /**
     * check for layers inside a dir
     * @param dir the name of the directory to look in
     * @callback
     */
    var checkForLayers = function(dir, callback) {
        var directoryReader = dir.createReader();
        directoryReader.readEntries(function(entries) {
            var id, name, options, fileEntry;

            for (var i = 0; i < entries.length; i++) {
                fileEntry = entries[i];

                if (fileEntry.name.indexOf('-journal') === -1) {
                    id = fileEntry.name;
                    name = fileEntry.name;
                    options = {
                        fileURL: fileEntry.toURL()
                    };

                    map.addLayerToLayersList(id, name, LAYER_TYPE, options);
                }
            }
        });
    };

    /**
     * Download layer to disk.
     */
    var downloadLayer = function(event){
        var $target = $(event.target);
        var layer = $("label[for='"+$target.attr('id')+"']").text();

        var itemUrl;

        // COBWEB uses the anonymous user id for downloading layers,
        // so if its defined use it.
        var annonUserId = utils.getAnonymousUserId();
        if(annonUserId){
            itemUrl = pcapi.buildFSUserUrl(utils.getAnonymousUserId(), TILES_FOLDER, layer);
        }
        else{
            itemUrl = pcapi.buildFSUrl(TILES_FOLDER, layer);
        }

        var target = file.appendFile(layersDir, layer);

        // Download or delete the editor from the device
        if($target.prop('checked')){
            utils.showPageLoadingMsg('Download Layer '+layer);
            file.ftDownload(
                itemUrl,
                target,
                function(){
                    $.mobile.loading('hide');
                    checkForLayers(layersDir, function(layers){
                        $.mobile.changePage('map.html');
                    });
                }
            );
        }else{
            utils.showPageLoadingMsg('Delete Layer '+layer);
            file.deleteFile(layer, layersDir, function(){
                map.removeLayersFromLayerList(layer);
                $.mobile.loading('hide');
            });

        }
    };

    /**
     * create layers list for downloading on download section
     */
    var savedLayersPage = function(){
        var $layersList = $(".layers-list");
        var layers = map.getLayersFromLayerList();
        var list = [];

        utils.showPageLoadingMsg('Checking for Layers ');

        //fetch the metadata from mbtiles and add them to the listview
        pcapi.getFSItems(TILES_FOLDER, utils.getAnonymousUserId()).then(function(data){
            if(data.hasOwnProperty('metadata')){
                $.each(data.metadata, $.proxy(function(i, item){
                    var fileName = item.substring(item.lastIndexOf('/') + 1, item.length);
                    var checked = '';
                    if (layers.hasOwnProperty(fileName)) {
                        checked = 'checked';
                    }
                    list.push('
'+fileName+'\
                                 <input data-role="flipswitch"\
                                        name="flip-checkbox-'+ i +'"\
                                        id="flip-checkbox-'+ i +'"\
                                        class="download-layer" type="checkbox" '+ checked +'>
');
                }, this));
            }

            $layersList.html(list.join(""));
            $('input[data-role="flipswitch"]', ".layers-list").flipswitch();
            $layersList.listview("refresh");
            $.mobile.loading('hide');
        });
    };

    /**
     * Show a POI layer adding it if necessary
     * @param layerMetadata {Object}
     *     - id {String} layer identifier
     *     - name {String} layer name
     *     - options {Object} options for the layer
     */
    var showLayer = function(layerMetadata) {
        var $target = $(event.target);
        //var layerName = $("label[for='"+$target.attr('id')+"']").text();
        var layerName = layerMetadata.name;

        var layerType;
        // if($target.prop('checked')){
        if (!map.layerExists(layerName)) {
            layerType = layerName.match(/\.(.*)$/)[1];

            switch (layerType) {
                case 'db':
                case 'mbtiles':
                    mbtiles.showMbTilesLayer(layerName);
                    break;
                case 'kml':
                    var kml = file.appendFile(layersDir, layerName);
                    var layer = map.addKMLLayer({
                        id: layerName,
                        url: kml
                    });

                    layer.events.register('loadend', this, function() {
                        map.zoomToExtent(layer.getDataExtent());
                    });

                    $('#map-page-layers-panel').panel('close');
                    break;
                default:
                    utils.inform('Don\'t know how to display ' + layerName);
            }
        }
        else {
            map.showLayerByName(layerName);
        }
    };

    /**
     * Hide a layer
     * @param layerMetadata {Object}
     *     - id {String} layer identifier
     *     - name {String} layer name
     *     - options {Object} options for the layer
     */
    var hideLayer = function(layerMetadata) {
        var layerName = layerMetadata.name;
        map.hideLayerByName(layerName);
    };

    /**
     * Zom to the extent of the layer
     * @param layerMetadata {Object}
     *     - id {String} layer identifier
     *     - name {String} layer name
     *     - options {Object} options for the layer
     */
    var zoomToExtentLayer = function(layerMetadata) {
        var layerName = layerMetadata.name;
        var layer = map.getLayerByName(layerName);
        if (layer) {
            map.zoomToExtent(layer);
        }
    };

    /**
     * Initialize the tiles directory and scan for tiles
     */
    var initLayersLocation = function() {
        var root;

        if (utils.isMobileDevice()) {
            // check settings first for defined pcapi root url
            root = settings.get('pcapi-url');

            if (root === undefined) {
                root = utils.getPCAPIURL();
            }

            file.createDir({
                name: TILES_FOLDER,
                success: function(dir) {
                    layersDir = dir;
                    mbtiles.init(layersDir);
                    checkForLayers(layersDir);

                }
            });

        }
        else {
            root = 'http://' + location.hostname;
            if (location.port) {
                root += ':' + location.port;
            }
        }

        //initialize pcapi
        pcapi.init({url: root, version: utils.getPCAPIVersion()});
    };

   /**
     * Perform initialization actions for the the plugin
     */
    var initPlugin = function() {
        initLayersLocation();

        // Listen for on/off events in the layer list
        map.suscribeToLayersControl({
            enableLayer: function(event, layerMetadata) {
                if (layerMetadata.type === LAYER_TYPE) {
                    showLayer(layerMetadata);
                }
            },
            disableLayer: function(event, layerMetadata) {
                if (layerMetadata.type === LAYER_TYPE) {
                    hideLayer(layerMetadata);
                }
            },
            clickLayer: function(event, layerMetadata) {
                if (layerMetadata.type === LAYER_TYPE) {
                    zoomToExtentLayer(layerMetadata);
                }
            }
        });

        $(document).on('pageshow', '#saved-layers-page', savedLayersPage);

        //download layer event
        $(document).on('change', '.download-layer', downloadLayer);

        // click on layer on map side panel
        $(document).on('change', '.show-layer', showLayer);

        $(document).on('change', '#settings-pcapi-url', function(){
            pcapi.setCloudProviderUrl(
                $('#settings-pcapi-url option:selected').val());
        });

        $('head').prepend('<link rel="stylesheet" href="plugins/overlays/css/style.css" type="text/css" />');
    };

    initPlugin();

});
<|endoftext|>


<commit_before>// @flow
import { observable, action, computed, runInAction } from 'mobx';
import moment from 'moment';
import Store from './lib/Store';
import Request from './lib/LocalizedRequest';
import { Logger } from '../../../common/logging';
import type { GetSyncProgressResponse, GetLocalTimeDifferenceResponse } from '../api/common';
import environment from '../../../common/environment';

// To avoid slow reconnecting on store reset, we cache the most important props
let cachedState = null;

// Maximum number of out-of-sync blocks above which we consider to be out-of-sync
const OUT_OF_SYNC_BLOCKS_LIMIT = 6;
const SYNC_PROGRESS_INTERVAL = 2000;
const TIME_DIFF_POLL_INTERVAL = 30 * 60 * 1000; // 30 minutes
const ALLOWED_TIME_DIFFERENCE = 15 * 1000000; // 15 seconds
const ALLOWED_NETWORK_DIFFICULTY_STALL = 2 * 60 * 1000; // 2 minutes

const STARTUP_STAGES = {
  CONNECTING: 0,
  SYNCING: 1,
  RUNNING: 2,
};

export default class NetworkStatusStore extends Store {

  _startTime = Date.now();
  _startupStage = STARTUP_STAGES.CONNECTING;
  _lastNetworkDifficultyChange = 0;
  _syncProgressPollInterval: ?number = null;
  _updateLocalTimeDifferencePollInterval: ?number = null;

  @observable isConnected = false;
  @observable hasBeenConnected = false;
  @observable localDifficulty = 0;
  @observable networkDifficulty = 0;
  @observable syncProgress = 0;
  @observable localTimeDifference = 0;
  @observable syncProgressRequest: Request<GetSyncProgressResponse> = new Request(
    // Use the sync progress for target API
    this.api[environment.API].getSyncProgress
  );
  @observable localTimeDifferenceRequest: Request<GetLocalTimeDifferenceResponse> = new Request(
    this.api.ada.getLocalTimeDifference
  );
  @observable _localDifficultyStartedWith = null;

  @action initialize() {
    super.initialize();
    if (cachedState !== null) Object.assign(this, cachedState);
  }

  setup() {
    this.registerReactions([
      this._updateSyncProgressWhenDisconnected,
      this._updateLocalTimeDifferenceWhenConnected,
    ]);

    // Setup polling intervals
    this._syncProgressPollInterval = setInterval(
      this._updateSyncProgress, SYNC_PROGRESS_INTERVAL
    );
    if (environment.isAdaApi()) {
      this._updateLocalTimeDifferencePollInterval = setInterval(
        this._updateLocalTimeDifference, TIME_DIFF_POLL_INTERVAL
      );
    }
  }

  teardown() {
    super.teardown();

    // Teardown polling intervals
    if (this._syncProgressPollInterval) {
      clearInterval(this._syncProgressPollInterval);
    }
    if (this._updateLocalTimeDifferencePollInterval) {
      clearInterval(this._updateLocalTimeDifferencePollInterval);
    }
    // Save current state into the cache
    cachedState = {
      isConnected: this.isConnected,
      hasBeenConnected: this.hasBeenConnected,
      localDifficulty: this.localDifficulty,
      networkDifficulty: this.networkDifficulty,
    };
  }

  @computed get isConnecting(): boolean {
    // until we start receiving network difficulty messages we are not connected to node
    return !this.isConnected;
  }

  @computed get hasBlockSyncingStarted(): boolean {
    return this.syncProgress >= 1;
  }

  @computed get relativeSyncBlocksDifference(): number {
    if (this.networkDifficulty > 0 && this._localDifficultyStartedWith !== null) {
      const relativeLocal = this.localDifficulty - this._localDifficultyStartedWith;
      const relativeNetwork = this.networkDifficulty - this._localDifficultyStartedWith;
      // In case node is in sync after first local difficulty messages
      // local and network difficulty will be the same (0)
      Logger.debug('Network difficulty: ' + this.networkDifficulty);
      Logger.debug('Local difficulty: ' + this.localDifficulty);
      Logger.debug('Relative local difficulty: ' + relativeLocal);
      Logger.debug('Relative network difficulty: ' + relativeNetwork);

      if (relativeLocal >= relativeNetwork) return 0;
      return relativeNetwork - relativeLocal;
    }
    return 0;
  }

  @computed get syncPercentage(): number {
    return this.syncProgress;
  }

  @computed get isSystemTimeCorrect(): boolean {
    if (!environment.isAdaApi()) return true;
    // We assume that system time is correct by default
    if (!this.localTimeDifferenceRequest.wasExecuted) return true;
    // Compare time difference if we have a result
    return this.localTimeDifference <= ALLOWED_TIME_DIFFERENCE;
  }

  @computed get isSyncing(): boolean {
    return !this.isConnecting && this.hasBlockSyncingStarted && !this.isSynced;
  }

  @computed get isSynced(): boolean {
    return (
      !this.isConnecting &&
      this.hasBlockSyncingStarted &&
      this.relativeSyncBlocksDifference <= OUT_OF_SYNC_BLOCKS_LIMIT &&
      this.syncProgress === 100
    );
  }

  @action _updateSyncProgress = async () => {
    try {
      const {
        localBlockchainHeight,
        blockchainHeight,
        syncProgress
      } = await this.syncProgressRequest.execute().promise;

      runInAction('update syncProgress', () => {
        this.syncProgress = syncProgress;
      });

      runInAction('update difficulties', () => {
        // We are connected, move on to syncing stage
        if (this._startupStage === STARTUP_STAGES.CONNECTING) {
          Logger.info(
            `========== Connected after ${this._getStartupTimeDelta()} milliseconds ==========`
          );
          this._startupStage = STARTUP_STAGES.SYNCING;
        }

        // If we haven't set local difficulty before, mark the first
        // result as 'start' difficulty for the sync progress
        if (this._localDifficultyStartedWith === null) {
          this._localDifficultyStartedWith = localBlockchainHeight;
          Logger.debug('Initial difficulty: ' + JSON.stringify({ localBlockchainHeight, blockchainHeight }));
        }

        // Update the local difficulty on each request
        this.localDifficulty = localBlockchainHeight;
        Logger.debug('Local difficulty changed: ' + this.localDifficulty);

        // Check if network difficulty is stalled (e.g. unchanged for more than 2 minutes)
        // e.g. in case there is no Internet connection Api will send the last known value
        if (this.networkDifficulty !== blockchainHeight) {
          if (!this.isConnected) this.isConnected = true;
          this._lastNetworkDifficultyChange = Date.now();
        } else if (this.isConnected) {
          const currentNetworkDifficultyStall = moment(Date.now()).diff(
            moment(this._lastNetworkDifficultyChange)
          );
          if (currentNetworkDifficultyStall > ALLOWED_NETWORK_DIFFICULTY_STALL) {
            this.isConnected = false;
            if (!this.hasBeenConnected) this.hasBeenConnected = true;
          }
        }

        // Update the network difficulty on each request
        this.networkDifficulty = blockchainHeight;
      });
      Logger.debug('Network difficulty changed: ' + this.networkDifficulty);

      if (this._startupStage === STARTUP_STAGES.SYNCING && this.isSynced) {
        Logger.info(`========== Synced after ${this._getStartupTimeDelta()} milliseconds ==========`);
        this._startupStage = STARTUP_STAGES.RUNNING;
        this.actions.networkStatus.isSyncedAndReady.trigger();
      }
    } catch (error) {
      // If the sync progress request fails, switch to disconnected state
      runInAction('update connected status', () => {
        if (this.isConnected) {
          this.isConnected = false;
          if (!this.hasBeenConnected) this.hasBeenConnected = true;
        }
      });
      Logger.debug('Connection Lost. Reconnecting...');
    }
  };

  @action _updateLocalTimeDifference = async () => {
    if (!this.isConnected) return;
    try {
      const response = await this.localTimeDifferenceRequest.execute().promise;
      runInAction('update time difference', () => (this.localTimeDifference = response));
    } catch (error) {
      runInAction('update time difference', () => (this.localTimeDifference = 0));
    }
  };

  _updateLocalTimeDifferenceWhenConnected = async () => {
    if (this.isConnected) await this._updateLocalTimeDifference();
  };

  _updateSyncProgressWhenDisconnected = async () => {
    if (!this.isConnected) await this._updateSyncProgress();
  };

  _getStartupTimeDelta() {
    return Date.now() - this._startTime;
  }
}
<commit_msg>[DDW-321] Remove syncProgress===100 condition in isSynced logic<commit_after>// @flow
import { observable, action, computed, runInAction } from 'mobx';
import moment from 'moment';
import Store from './lib/Store';
import Request from './lib/LocalizedRequest';
import { Logger } from '../../../common/logging';
import type { GetSyncProgressResponse, GetLocalTimeDifferenceResponse } from '../api/common';
import environment from '../../../common/environment';

// To avoid slow reconnecting on store reset, we cache the most important props
let cachedState = null;

// Maximum number of out-of-sync blocks above which we consider to be out-of-sync
const OUT_OF_SYNC_BLOCKS_LIMIT = 6;
const SYNC_PROGRESS_INTERVAL = 2000;
const TIME_DIFF_POLL_INTERVAL = 30 * 60 * 1000; // 30 minutes
const ALLOWED_TIME_DIFFERENCE = 15 * 1000000; // 15 seconds
const ALLOWED_NETWORK_DIFFICULTY_STALL = 2 * 60 * 1000; // 2 minutes

const STARTUP_STAGES = {
  CONNECTING: 0,
  SYNCING: 1,
  RUNNING: 2,
};

export default class NetworkStatusStore extends Store {

  _startTime = Date.now();
  _startupStage = STARTUP_STAGES.CONNECTING;
  _lastNetworkDifficultyChange = 0;
  _syncProgressPollInterval: ?number = null;
  _updateLocalTimeDifferencePollInterval: ?number = null;

  @observable isConnected = false;
  @observable hasBeenConnected = false;
  @observable localDifficulty = 0;
  @observable networkDifficulty = 0;
  @observable syncProgress = 0;
  @observable localTimeDifference = 0;
  @observable syncProgressRequest: Request<GetSyncProgressResponse> = new Request(
    // Use the sync progress for target API
    this.api[environment.API].getSyncProgress
  );
  @observable localTimeDifferenceRequest: Request<GetLocalTimeDifferenceResponse> = new Request(
    this.api.ada.getLocalTimeDifference
  );
  @observable _localDifficultyStartedWith = null;

  @action initialize() {
    super.initialize();
    if (cachedState !== null) Object.assign(this, cachedState);
  }

  setup() {
    this.registerReactions([
      this._updateSyncProgressWhenDisconnected,
      this._updateLocalTimeDifferenceWhenConnected,
    ]);

    // Setup polling intervals
    this._syncProgressPollInterval = setInterval(
      this._updateSyncProgress, SYNC_PROGRESS_INTERVAL
    );
    if (environment.isAdaApi()) {
      this._updateLocalTimeDifferencePollInterval = setInterval(
        this._updateLocalTimeDifference, TIME_DIFF_POLL_INTERVAL
      );
    }
  }

  teardown() {
    super.teardown();

    // Teardown polling intervals
    if (this._syncProgressPollInterval) {
      clearInterval(this._syncProgressPollInterval);
    }
    if (this._updateLocalTimeDifferencePollInterval) {
      clearInterval(this._updateLocalTimeDifferencePollInterval);
    }
    // Save current state into the cache
    cachedState = {
      isConnected: this.isConnected,
      hasBeenConnected: this.hasBeenConnected,
      localDifficulty: this.localDifficulty,
      networkDifficulty: this.networkDifficulty,
    };
  }

  @computed get isConnecting(): boolean {
    // until we start receiving network difficulty messages we are not connected to node
    return !this.isConnected;
  }

  @computed get hasBlockSyncingStarted(): boolean {
    return this.syncProgress >= 1;
  }

  @computed get relativeSyncBlocksDifference(): number {
    if (this.networkDifficulty > 0 && this._localDifficultyStartedWith !== null) {
      const relativeLocal = this.localDifficulty - this._localDifficultyStartedWith;
      const relativeNetwork = this.networkDifficulty - this._localDifficultyStartedWith;
      // In case node is in sync after first local difficulty messages
      // local and network difficulty will be the same (0)
      Logger.debug('Network difficulty: ' + this.networkDifficulty);
      Logger.debug('Local difficulty: ' + this.localDifficulty);
      Logger.debug('Relative local difficulty: ' + relativeLocal);
      Logger.debug('Relative network difficulty: ' + relativeNetwork);

      if (relativeLocal >= relativeNetwork) return 0;
      return relativeNetwork - relativeLocal;
    }
    return 0;
  }

  @computed get syncPercentage(): number {
    return this.syncProgress;
  }

  @computed get isSystemTimeCorrect(): boolean {
    if (!environment.isAdaApi()) return true;
    // We assume that system time is correct by default
    if (!this.localTimeDifferenceRequest.wasExecuted) return true;
    // Compare time difference if we have a result
    return this.localTimeDifference <= ALLOWED_TIME_DIFFERENCE;
  }

  @computed get isSyncing(): boolean {
    return !this.isConnecting && this.hasBlockSyncingStarted && !this.isSynced;
  }

  @computed get isSynced(): boolean {
    return (
      !this.isConnecting &&
      this.hasBlockSyncingStarted &&
      this.relativeSyncBlocksDifference <= OUT_OF_SYNC_BLOCKS_LIMIT
    );
  }

  @action _updateSyncProgress = async () => {
    try {
      const {
        localBlockchainHeight,
        blockchainHeight,
        syncProgress
      } = await this.syncProgressRequest.execute().promise;

      runInAction('update syncProgress', () => {
        this.syncProgress = syncProgress;
      });

      runInAction('update difficulties', () => {
        // We are connected, move on to syncing stage
        if (this._startupStage === STARTUP_STAGES.CONNECTING) {
          Logger.info(
            `========== Connected after ${this._getStartupTimeDelta()} milliseconds ==========`
          );
          this._startupStage = STARTUP_STAGES.SYNCING;
        }

        // If we haven't set local difficulty before, mark the first
        // result as 'start' difficulty for the sync progress
        if (this._localDifficultyStartedWith === null) {
          this._localDifficultyStartedWith = localBlockchainHeight;
          Logger.debug('Initial difficulty: ' + JSON.stringify({ localBlockchainHeight, blockchainHeight }));
        }

        // Update the local difficulty on each request
        this.localDifficulty = localBlockchainHeight;
        Logger.debug('Local difficulty changed: ' + this.localDifficulty);

        // Check if network difficulty is stalled (e.g. unchanged for more than 2 minutes)
        // e.g. in case there is no Internet connection Api will send the last known value
        if (this.networkDifficulty !== blockchainHeight) {
          if (!this.isConnected) this.isConnected = true;
          this._lastNetworkDifficultyChange = Date.now();
        } else if (this.isConnected) {
          const currentNetworkDifficultyStall = moment(Date.now()).diff(
            moment(this._lastNetworkDifficultyChange)
          );
          if (currentNetworkDifficultyStall > ALLOWED_NETWORK_DIFFICULTY_STALL) {
            this.isConnected = false;
            if (!this.hasBeenConnected) this.hasBeenConnected = true;
          }
        }

        // Update the network difficulty on each request
        this.networkDifficulty = blockchainHeight;
      });
      Logger.debug('Network difficulty changed: ' + this.networkDifficulty);

      if (this._startupStage === STARTUP_STAGES.SYNCING && this.isSynced) {
        Logger.info(`========== Synced after ${this._getStartupTimeDelta()} milliseconds ==========`);
        this._startupStage = STARTUP_STAGES.RUNNING;
        this.actions.networkStatus.isSyncedAndReady.trigger();
      }
    } catch (error) {
      // If the sync progress request fails, switch to disconnected state
      runInAction('update connected status', () => {
        if (this.isConnected) {
          this.isConnected = false;
          if (!this.hasBeenConnected) this.hasBeenConnected = true;
        }
      });
      Logger.debug('Connection Lost. Reconnecting...');
    }
  };

  @action _updateLocalTimeDifference = async () => {
    if (!this.isConnected) return;
    try {
      const response = await this.localTimeDifferenceRequest.execute().promise;
      runInAction('update time difference', () => (this.localTimeDifference = response));
    } catch (error) {
      runInAction('update time difference', () => (this.localTimeDifference = 0));
    }
  };

  _updateLocalTimeDifferenceWhenConnected = async () => {
    if (this.isConnected) await this._updateLocalTimeDifference();
  };

  _updateSyncProgressWhenDisconnected = async () => {
    if (!this.isConnected) await this._updateSyncProgress();
  };

  _getStartupTimeDelta() {
    return Date.now() - this._startTime;
  }
}
<|endoftext|>