<commit_before>var assert = require('assert');

describe('methods', function (){

  var f_ = require('./../../index.js');

  describe('#abort', function (){
    it('emits an abort event with an err_object.message prefixed with "abort, "', function (done){
      var test = new (f_.getConstructor({
        function_flow: []
      }));

      test.on('abort', function (err_object) {
        assert.equal(err_object.message, 'abort, test1');
        done();
      });

      test.f_abort('test1');
    });

    it('results in not calling any more functions from function_flow', function (done) {
      var next_called = false;

      var test = new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { this.f_abort(); } },
          { name: 'two', function: function () { this.f_next(); } }
        ]
      }));

      setTimeout(function () {
        assert.equal(next_called, false);
        done();
      }, 50);

      test.f_go();

      test.on('next', function () {
        next_called = true;
      });
    });
  });

  describe('#next', function () {
    it('calls all methods in the function_flow and emits the finish event', function (done) {
      var method1_called = false,
          method2_called = false;

          // @TODO FIX
      var instance = (new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { method1_called = true; this.f_next(); } },
          { name: 'two', function: function () { method2_called = true; this.f_next(); } }
        ]
      }))).f_go();

      setTimeout(function () {
        assert.equal(method1_called, true);
        assert.equal(method2_called, true);
        done();
      }, 50);
    });
    it('emits the finish event when all methods in function_flow are completed', function (done) {
      var method1_called = false,
          method2_called = false;

      var instance = new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { method1_called = true; this.f_next(); } },
          { name: 'two', function: function () { method2_called = true; this.f_next(); } }
        ]
      }));

      instance.on('finish', function () {
        done();
      });

      instance.f_go();
    });

    it('emits an error event/object when there is no function in function_flow (deletion?)', function (done) {
      var instance = new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { } },
          { name: 'two', function: function () { } }
        ]
      }));

      instance.on('abort', function (err) {
        done();
      });

      instance.f_go();
      instance.f_function_flow[1] = undefined;
      instance.f_next();
    });

    it('emits an error event/object when there is no function in function_flow (deletion?)', function (done) {
      var instance = new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { } },
          { name: 'two', function: function () { } }
        ]
      }));

      instance.on('abort', function (err) {
        done();
      });

      instance.f_go();
      instance.f_function_flow[1].name = undefined;
      instance.f_next();
    });

    it('emits an error event/object when the next item in the function_flow is not a function', function (done) {
      var instance = new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { } },
          { name: 'two', function: function () { } }
        ]
      }));

      instance.on('abort', function (err) {
        done();
      });

      instance.f_go();
      instance.f_function_flow[1] = { name: 'test', tries: 2, max_tries: 1 };
      instance.f_next();
    });

  });

  describe('#go', function (){
    it('calls f_next with the passed arguments', function (done){
      var test = new (f_.getConstructor({
        function_flow: [
          { name: 'one',
            function: function (test1, test2) {
              assert.equal(test1, 'test1');
              assert.equal(test2, 'test2');
              done();
            }
          },
        ]
      }));

      test.f_go('test1', 'test2');
    });
  });

  describe('#retryAll', function (){
    var f_ = require('./../../index.js');

    it('resets f_flow_i to 0 and starts the flow from the first method specified in the function_flow', function (done){
      var methods_called = 0;

      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              methods_called++;
              this.f_next();
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
              methods_called++;
              this.f_retryAll();
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.f_go();

      assert.equal(methods_called, 10);
      done();
    });

    it('emits an retryAll event', function (done) {
      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              this.f_next();
            },
            max_tries: 1
          },
          {
            name: 'm2',
            function: function () {
              this.f_retryAll();
            },
            max_tries: 1
          }
        ]
      });

      var instance = new Constructor();

      instance.on('retryAll', function () {
        done();
      });

      instance.f_go();
    });
  });

  describe('#retryThis', function () {
    it('restarts the f_ task list from where retryThis was called', function (done) {
      var called_m2 = false;

      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              this.f_next();
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
              if (called_m2) {
                this.f_next();
              }
              else {
                called_m2 = true;
                this.f_retryThis();
              }
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.on('retryThis', function () {
        assert.equal(called_m2, true);
        done();
      });

      instance.f_go();
    });

    it('emits an retryThis/error event', function (done) {
      var called_m2 = false;

      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              this.f_next();
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
              if (called_m2) {
                this.f_next();
              }
              else {
                called_m2 = true;
                this.f_retryThis();
              }
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.on('retryThis', function () {
        done();
      });

      instance.f_go();
    });
  });

  describe('#retryFrom', function () {
    it('restarts the f_task list from the method given as a string as argument 1', function (done) {
      var called_m2 = false;

      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              this.f_next();
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
              if (called_m2) {
                this.f_next();
              }
              else {
                called_m2 = true;
                this.f_retryFrom('m1');
              }
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.on('finish', function () {
        var tried_twice = true;

        this.f_function_flow.forEach(function (flow) {
          console.log(flow.tries);
          if (flow.tries !== 2) {
            tried_twice = false;
          }
        });
        assert.equal(tried_twice, true);
        done();
      });

      instance.f_go();
    });

    it('sets the flow_i to the index of the method passed in function_flow + 1', function (done) {
      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.f_go();
      instance.f_next();
      instance.f_retryFrom('m1');

      assert.equal(instance.f_flow_i, 1);
      done();
    });

    it('emits abort event/error when no function is found to call', function (done) {
      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              this.f_next();
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
              this.m1 = undefined;
              this.f_retryFrom('m1');
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.on('abort', function () {
        done();
      });

      instance.f_go();
    });

  });

});<commit_msg>retryThis fix<commit_after>var assert = require('assert');

describe('methods', function (){

  var f_ = require('./../../index.js');

  describe('#abort', function (){
    it('emits an abort event with an err_object.message prefixed with "abort, "', function (done){
      var test = new (f_.getConstructor({
        function_flow: []
      }));

      test.on('abort', function (err_object) {
        assert.equal(err_object.message, 'abort, test1');
        done();
      });

      test.f_abort('test1');
    });

    it('results in not calling any more functions from function_flow', function (done) {
      var next_called = false;

      var test = new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { this.f_abort(); } },
          { name: 'two', function: function () { this.f_next(); } }
        ]
      }));

      setTimeout(function () {
        assert.equal(next_called, false);
        done();
      }, 50);

      test.f_go();

      test.on('next', function () {
        next_called = true;
      });
    });
  });

  describe('#next', function () {
    it('calls all methods in the function_flow and emits the finish event', function (done) {
      var method1_called = false,
          method2_called = false;

          // @TODO FIX
      var instance = (new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { method1_called = true; this.f_next(); } },
          { name: 'two', function: function () { method2_called = true; this.f_next(); } }
        ]
      }))).f_go();

      setTimeout(function () {
        assert.equal(method1_called, true);
        assert.equal(method2_called, true);
        done();
      }, 50);
    });
    it('emits the finish event when all methods in function_flow are completed', function (done) {
      var method1_called = false,
          method2_called = false;

      var instance = new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { method1_called = true; this.f_next(); } },
          { name: 'two', function: function () { method2_called = true; this.f_next(); } }
        ]
      }));

      instance.on('finish', function () {
        done();
      });

      instance.f_go();
    });

    it('emits an error event/object when there is no function in function_flow (deletion?)', function (done) {
      var instance = new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { } },
          { name: 'two', function: function () { } }
        ]
      }));

      instance.on('abort', function (err) {
        done();
      });

      instance.f_go();
      instance.f_function_flow[1] = undefined;
      instance.f_next();
    });

    it('emits an error event/object when there is no function in function_flow (deletion?)', function (done) {
      var instance = new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { } },
          { name: 'two', function: function () { } }
        ]
      }));

      instance.on('abort', function (err) {
        done();
      });

      instance.f_go();
      instance.f_function_flow[1].name = undefined;
      instance.f_next();
    });

    it('emits an error event/object when the next item in the function_flow is not a function', function (done) {
      var instance = new (f_.getConstructor({
        function_flow: [
          { name: 'one', function: function () { } },
          { name: 'two', function: function () { } }
        ]
      }));

      instance.on('abort', function (err) {
        done();
      });

      instance.f_go();
      instance.f_function_flow[1] = { name: 'test', tries: 2, max_tries: 1 };
      instance.f_next();
    });

  });

  describe('#go', function (){
    it('calls f_next with the passed arguments', function (done){
      var test = new (f_.getConstructor({
        function_flow: [
          { name: 'one',
            function: function (test1, test2) {
              assert.equal(test1, 'test1');
              assert.equal(test2, 'test2');
              done();
            }
          },
        ]
      }));

      test.f_go('test1', 'test2');
    });
  });

  describe('#retryAll', function (){
    var f_ = require('./../../index.js');

    it('resets f_flow_i to 0 and starts the flow from the first method specified in the function_flow', function (done){
      var methods_called = 0;

      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              methods_called++;
              this.f_next();
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
              methods_called++;
              this.f_retryAll();
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.f_go();

      assert.equal(methods_called, 10);
      done();
    });

    it('emits an retryAll event', function (done) {
      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              this.f_next();
            },
            max_tries: 1
          },
          {
            name: 'm2',
            function: function () {
              this.f_retryAll();
            },
            max_tries: 1
          }
        ]
      });

      var instance = new Constructor();

      instance.on('retryAll', function () {
        done();
      });

      instance.f_go();
    });
  });

  describe('#retryThis', function () {
    it('restarts the f_ task list from where retryThis was called', function (done) {
      var called_m2 = false;

      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              this.f_next();
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
              if (called_m2) {
                this.f_next();
              }
              else {
                called_m2 = true;
                this.f_retryThis();
              }
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.on('finish', function () {
        assert.equal(called_m2, true);
        done();
      });

      instance.f_go();
    });

    it('emits an retryThis/error event', function (done) {
      var called_m2 = false;

      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              this.f_next();
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
              if (called_m2) {
                this.f_next();
              }
              else {
                called_m2 = true;
                this.f_retryThis();
              }
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.on('retryThis', function () {
        done();
      });

      instance.f_go();
    });
  });

  describe('#retryFrom', function () {
    it('restarts the f_task list from the method given as a string as argument 1', function (done) {
      var called_m2 = false;

      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              this.f_next();
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
              if (called_m2) {
                this.f_next();
              }
              else {
                called_m2 = true;
                this.f_retryFrom('m1');
              }
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.on('finish', function () {
        var tried_twice = true;

        this.f_function_flow.forEach(function (flow) {
          console.log(flow.tries);
          if (flow.tries !== 2) {
            tried_twice = false;
          }
        });
        assert.equal(tried_twice, true);
        done();
      });

      instance.f_go();
    });

    it('sets the flow_i to the index of the method passed in function_flow + 1', function (done) {
      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.f_go();
      instance.f_next();
      instance.f_retryFrom('m1');

      assert.equal(instance.f_flow_i, 1);
      done();
    });

    it('emits abort event/error when no function is found to call', function (done) {
      var Constructor = f_.getConstructor({
        function_flow: [
          {
            name: 'm1',
            function: function () {
              this.f_next();
            },
            max_tries: 5
          },
          {
            name: 'm2',
            function: function () {
              this.m1 = undefined;
              this.f_retryFrom('m1');
            },
            max_tries: 5
          }
        ]
      });

      var instance = new Constructor();

      instance.on('abort', function () {
        done();
      });

      instance.f_go();
    });

  });

});<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var BluebirdPromise = require('bluebird');
var BaseRelation = require('./base');
var Actionable = require('../util/actionable');
var Hooks = require('./has_many_hooks');
var Config = require('./has_many_config');
var Query = require('./has_many_query');
var Associations = require('./has_many_associations');
var Overrides = require('./has_many_overrides');
var Through = require('./has_many_through');
var Prefetch = require('./has_many_prefetch');
var Collection = require('./has_many_collection');
var InFlight = require('./has_many_in_flight');

/**
 * The has many relation for models.
 *
 * For example:
 *
 *     var User = Model.extend({
 *       articles: hasMany('articles')
 *     });
 *
 * @public
 * @param {Class|String} [relatedModel] The model to which this relates.
 * @param {Object} [options]
 * @param {String} [options.inverse] The name of the inverse relationship.
 * @param {String} [options.primaryKey] The name of the primary key in the
 * relationship.
 * @param {String} [options.foreignKey] The name of the foreign key in the
 * relationship.
 * @param {String} [options.through] Specify the name of a relationship through
 * which this collection is accessed.
 * @param {String} [options.source] When using `through` this is the name of
 * the relationship on the destination model. The default value is the name of
 * the attribute for the relationship.
 * @function Database#hasMany
 */

/**
 * The has many relation for models.
 *
 * @protected
 * @constructor HasMany
 * @extends BaseRelation
 * @see {@link Database#hasMany}
 */
var HasMany = BaseRelation.extend();

HasMany.reopen(Hooks); // must be first, contains overridable methods
HasMany.reopen(Config);
HasMany.reopen(Query);
HasMany.reopen(Associations);
HasMany.reopen(Overrides);
HasMany.reopen(Through);
HasMany.reopen(Prefetch);
HasMany.reopen(Collection);
HasMany.reopen(InFlight);

HasMany.reopenClass({
  joinKey: 'primaryKey',
  inverseKey: 'foreignKey',
});

/**
 * Notify of changes around a call to super.
 *
 * @function HasMany~notify
 * @private
 * @param {String} name The suffix of the before/after methods to call.
 * @return {Function} A method that will call before & after methods.
 */
var notify = function(name) {
  var before = 'before' + _.capitalize(name);
  var after = 'after' + _.capitalize(name);
  return function() {
    this[before].apply(this, arguments);
    this._super.apply(this, arguments);
    this[after].apply(this, arguments);
  };
};

HasMany.reopen(/** @lends HasMany# */ {

  /**
   * The identity property for this relation.
   *
   * This property allows access to the underlying relation object, allowing
   * you to access details about how the relation was created/configured. This
   * method is currently only intended for internal use, but if you have a
   * reason for it to be documented publicly, please create an issue on GitHub
   * and let us know.
   *
   * It is accessible on an individual model via `<plural>Relation`. For
   * instance, a user that has many articles would cause this method to get
   * triggered via `user.articlesRelation`.
   *
   * The naming conventions are set forth in {@link HasMany.methods}.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @see {@link BaseRelation#methods}
   */
  identity: function(/*instance*/) {
    return this;
  },

  /**
   * Determine if this relation is a many-to-many relationship, that is if the
   * inverse relationship is a many relationship as well. Has many
   * relationships are not many-to-many by default.
   *
   * Mixins can override {@link HasMany#inverseIsMany} when they work in such a
   * way to allow many-to-many relationships. This is the default
   * implementation.
   *
   * @return {Boolean} True if the relationship is many-to-many.
   */
  _inverseIsMany: function() {
    return false;
  },

  /**
   * The create object method for this relation.
   *
   * This simply creates a new object that has the appropriate relation data
   * already set.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and adds
   * the created object to the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `create<Singular>`. For
   * instance, a user that has many articles would cause this method to get
   * triggered via `user.createArticle`.
   *
   * The naming conventions are set forth in {@link HasMany.methods}.
   *
   * Mixins can override {@link HasMany#createObject} to change the way related
   * objects are created. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {...Object} [args] The arguments to the method.
   * @see {@link BaseRelation#methods}
   */
  _createObject: function(instance) {
    var args = _.rest(arguments);
    var relatedModel = this._relatedModel;
    var result = relatedModel.create.apply(relatedModel, args);

    this.beforeCreatingObject(instance, result); // note: no after hook
    this.associate(instance, result);

    return result;
  },

  /**
   * The add objects method for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and adds
   * the related objects to the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `add<Singular>` and
   * `add<Plural>`. For instance a user that has many articles would cause this
   * method to get triggered via `user.addArticle` or `user.addArticles`.
   *
   * The naming conventions are set forth in {@link HasMany.methods}.
   *
   * Mixins can override {@link HasMany#addObjects} to change the way related
   * objects are added. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {...Object} [args] The arguments to the method.
   * @return {Actionable} A thenable object, use of which will trigger the
   * instance to be saved.
   * @see {@link BaseRelation#methods}
   */
  _addObjects: function(instance) {
    var args = _.rest(arguments);
    var objects = _.flatten(args);
    this.beforeAddingObjects(instance, objects);
    this.associateObjects(instance, objects);
    return Actionable.create(instance.save.bind(instance));
  },

  /**
   * Perform the necessary updates to add objects for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery}.
   *
   * Mixins can override {@link HasMany#executeAdd} to change the way updates
   * are performed when related objects are added. This is the default
   * implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {Array} objects The objects to add to the relationship.
   */
  _executeAdd: function(instance, objects) {
    var self = this;
    var after = this.afterAddingObjects.bind(this, instance, objects);
    return self._prepareForForeignKeyUpdate(instance, objects)
    .then(function() {
      return self._updateForeignKeys(instance, objects, instance.id);
    })
    .tap(after);
  },

  /**
   * The remove objects method for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and removes
   * the related objects from the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `remove<Singular>` and
   * `remove<Plural>`. For instance a user that has many articles would cause
   * this method to get triggered via `user.removeArticle` or
   * `user.removeArticles`.
   *
   * The naming conventions are set forth in {@link HasMany.methods}.
   *
   * Mixins can override {@link HasMany#removeObjects} to change the way
   * related objects are removed. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {...Object} [args] The arguments to the method.
   * @return {Actionable} A thenable object, use of which will trigger the
   * instance to be saved.
   * @see {@link BaseRelation#methods}
   */
  _removeObjects: function(instance) {
    var args = _.rest(arguments);
    var objects = _.flatten(args);
    this.beforeRemovingObjects(instance, objects);
    this.disassociateObjects(instance, objects);
    return Actionable.create(instance.save.bind(instance));
  },

  /**
   * Perform the necessary updates to remove objects for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery}.
   *
   * Mixins can override {@link HasMany#executeRemove} to change the way
   * updates are performed when related objects are removed. This is the
   * default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {Array} objects The objects to remove from the relationship.
   */
  _executeRemove: function(instance, objects) {
    var self = this;
    var after = this.afterRemovingObjects.bind(this, instance, objects);
    var removable = _.filter(objects, 'persisted');
    return self._prepareForForeignKeyUpdate(instance, removable)
    .then(function() {
      return self._updateForeignKeys(instance, removable, undefined);
    })
    .tap(after);
  },

  /**
   * The clear objects method for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and removes
   * all objects from the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `clear<Plural>`. For
   * instance, a user that has many articles would cause this method to get
   * triggered via `user.clearArticles`.
   *
   * The naming conventions are set forth in {@link HasMany.methods}.
   *
   * Mixins can override {@link HasMany#clearObjects} to change the way related
   * objects are cleared. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @return {Actionable} A thenable object, use of which will trigger the
   * instance to be saved.
   * @see {@link BaseRelation#methods}
   */
  _clearObjects: function(instance) {
    this.beforeClearingObjects(instance);
    return Actionable.create(instance.save.bind(instance));
  },

  /**
   * Perform the necessary updates to clear objects for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery}.
   *
   * Mixins can override {@link HasMany#executeClear} to change the way clears
   * are performed when related objects are cleared. This is the default
   * implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   */
  _executeClear: function(instance) {
    var updates = _.object([[this.foreignKey, undefined]]);
    var query = this.objectsQuery(instance).update(updates);
    var after = this.afterClearingObjects.bind(this, instance);
    return query.execute().tap(after);
  },

  /**
   * Prepare many related objects for a foreign key update.
   *
   * This will save any dirty records unless the only attribute that is dirty
   * is the foreign key. It will also save any newly created objects regardless
   * of whether or not they are dirty.
   *
   * @method
   * @private
   * @param {Model} instance The model instance on which to operate.
   * @param {Array.<Model>} relatedObjects The related models to prepare.
   * @return {Promise} A promise that resolves when the prep work has finished.
   */
  _prepareForForeignKeyUpdate: function(instance, relatedObjects) {
    var foreignKeyAttr = this.foreignKeyAttr;
    return BluebirdPromise.map(relatedObjects, function(obj) {
      var dirty = _.without(obj.dirtyAttributes, foreignKeyAttr);
      var save = dirty.length || obj.newRecord;
      return save && obj.save();
    });
  },

  /**
   * Update foreign keys for a set of related objects.
   *
   * This will update the foreign keys of several objects in a single query.
   * This allows much faster creation of associations at the database level.
   *
   * After a successful update, it will clean the foreign key attribute of
   * each updated object, likely leaving the object completely clean.
   *
   * @method
   * @private
   * @param {Model} instance The model instance on which to operate.
   * @param {Array.<Model>} relatedObjects The related models on which to
   * update keys.
   * @param {?} id The id to use as the value of the foreign key.
   * @return {Promise} A promise that resolves when the update has finished.
   */
  _updateForeignKeys: function(instance, relatedObjects, id) {
    if (!_.any(relatedObjects, 'dirty')) { return BluebirdPromise.bind(); }

    var foreignKey = this.foreignKey;
    var foreignKeyAttr = this.foreignKeyAttr;
    var relatedPks = _.map(relatedObjects, 'pk');

    var updates = _.object([[foreignKey, id]]);
    var conditions = {};
    if (relatedPks.length === 1) { conditions.pk = relatedPks[0]; }
    else { conditions['pk$in'] = relatedPks; }

    var query = this._relatedModel.objects;
    return query.where(conditions).update(updates).execute().then(function() {
      relatedObjects.forEach(function(obj) {
        obj.cleanAttribute(foreignKeyAttr);
      });
    });
  },

  /**
   * Override of {@link HasMany#associateFetchedObjects} to call before & after
   * hooks so that mixins can get involved in the association process.
   *
   * @method
   * @protected
   * @see {@link HasMany#associateFetchedObjects}
   */
  associateFetchedObjects: notify('associatingFetchedObjects'),

  /**
   * Override of {@link HasMany#associateObjects} to call before & after hooks
   * so that mixins can get involved in the association process.
   *
   * @method
   * @protected
   * @see {@link HasMany#associateObjects}
   */
  associateObjects: notify('associatingObjects'),

  /**
   * Override of {@link HasMany#disassociateObjects} to call before & after
   * hooks so that mixins can get involved in the disassociation process.
   *
   * @method
   * @protected
   * @see {@link HasMany#disassociateObjects}
   */
  disassociateObjects: notify('disassociatingObjects'),

});

HasMany.reopenClass(/** @lends HasMany */ {

  /**
   * Naming conventions for `HasMany` relations.
   *
   * @private
   * @type {Object}
   * @see {@link BaseRelation#methods}
   */
  methods: {
    '<plural>': BaseRelation.property('collection'),
    '<plural>Relation': BaseRelation.property('identity'),
    '<singular>Objects': BaseRelation.property('objectsQuery'),
    'create<Singular>': BaseRelation.method('createObject'),
    'add<Singular>': BaseRelation.method('addObjects'),
    'add<Plural>': BaseRelation.method('addObjects'),
    'remove<Singular>': BaseRelation.method('removeObjects'),
    'remove<Plural>': BaseRelation.method('removeObjects'),
    'clear<Plural>': BaseRelation.method('clearObjects'),
    'init': BaseRelation.method('initialize'),
    'save': BaseRelation.method('save')
  }

});

module.exports = HasMany.reopenClass({ __name__: 'HasMany' });
<commit_msg>Fixed style issue.<commit_after>'use strict';

var _ = require('lodash');
var BluebirdPromise = require('bluebird');
var BaseRelation = require('./base');
var Actionable = require('../util/actionable');
var Hooks = require('./has_many_hooks');
var Config = require('./has_many_config');
var Query = require('./has_many_query');
var Associations = require('./has_many_associations');
var Overrides = require('./has_many_overrides');
var Through = require('./has_many_through');
var Prefetch = require('./has_many_prefetch');
var Collection = require('./has_many_collection');
var InFlight = require('./has_many_in_flight');

/**
 * The has many relation for models.
 *
 * For example:
 *
 *     var User = Model.extend({
 *       articles: hasMany('articles')
 *     });
 *
 * @public
 * @param {Class|String} [relatedModel] The model to which this relates.
 * @param {Object} [options]
 * @param {String} [options.inverse] The name of the inverse relationship.
 * @param {String} [options.primaryKey] The name of the primary key in the
 * relationship.
 * @param {String} [options.foreignKey] The name of the foreign key in the
 * relationship.
 * @param {String} [options.through] Specify the name of a relationship through
 * which this collection is accessed.
 * @param {String} [options.source] When using `through` this is the name of
 * the relationship on the destination model. The default value is the name of
 * the attribute for the relationship.
 * @function Database#hasMany
 */

/**
 * The has many relation for models.
 *
 * @protected
 * @constructor HasMany
 * @extends BaseRelation
 * @see {@link Database#hasMany}
 */
var HasMany = BaseRelation.extend();

HasMany.reopen(Hooks); // must be first, contains overridable methods
HasMany.reopen(Config);
HasMany.reopen(Query);
HasMany.reopen(Associations);
HasMany.reopen(Overrides);
HasMany.reopen(Through);
HasMany.reopen(Prefetch);
HasMany.reopen(Collection);
HasMany.reopen(InFlight);

HasMany.reopenClass({
  joinKey: 'primaryKey',
  inverseKey: 'foreignKey',
});

/**
 * Notify of changes around a call to super.
 *
 * @function HasMany~notify
 * @private
 * @param {String} name The suffix of the before/after methods to call.
 * @return {Function} A method that will call before & after methods.
 */
var notify = function(name) {
  var before = 'before' + _.capitalize(name);
  var after = 'after' + _.capitalize(name);
  return function() {
    this[before].apply(this, arguments);
    this._super.apply(this, arguments);
    this[after].apply(this, arguments);
  };
};

HasMany.reopen(/** @lends HasMany# */ {

  /**
   * The identity property for this relation.
   *
   * This property allows access to the underlying relation object, allowing
   * you to access details about how the relation was created/configured. This
   * method is currently only intended for internal use, but if you have a
   * reason for it to be documented publicly, please create an issue on GitHub
   * and let us know.
   *
   * It is accessible on an individual model via `<plural>Relation`. For
   * instance, a user that has many articles would cause this method to get
   * triggered via `user.articlesRelation`.
   *
   * The naming conventions are set forth in {@link HasMany.methods}.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @see {@link BaseRelation#methods}
   */
  identity: function(/*instance*/) {
    return this;
  },

  /**
   * Determine if this relation is a many-to-many relationship, that is if the
   * inverse relationship is a many relationship as well. Has many
   * relationships are not many-to-many by default.
   *
   * Mixins can override {@link HasMany#inverseIsMany} when they work in such a
   * way to allow many-to-many relationships. This is the default
   * implementation.
   *
   * @return {Boolean} True if the relationship is many-to-many.
   */
  _inverseIsMany: function() {
    return false;
  },

  /**
   * The create object method for this relation.
   *
   * This simply creates a new object that has the appropriate relation data
   * already set.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and adds
   * the created object to the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `create<Singular>`. For
   * instance, a user that has many articles would cause this method to get
   * triggered via `user.createArticle`.
   *
   * The naming conventions are set forth in {@link HasMany.methods}.
   *
   * Mixins can override {@link HasMany#createObject} to change the way related
   * objects are created. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {...Object} [args] The arguments to the method.
   * @see {@link BaseRelation#methods}
   */
  _createObject: function(instance) {
    var args = _.rest(arguments);
    var relatedModel = this._relatedModel;
    var result = relatedModel.create.apply(relatedModel, args);

    this.beforeCreatingObject(instance, result); // note: no after hook
    this.associate(instance, result);

    return result;
  },

  /**
   * The add objects method for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and adds
   * the related objects to the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `add<Singular>` and
   * `add<Plural>`. For instance a user that has many articles would cause this
   * method to get triggered via `user.addArticle` or `user.addArticles`.
   *
   * The naming conventions are set forth in {@link HasMany.methods}.
   *
   * Mixins can override {@link HasMany#addObjects} to change the way related
   * objects are added. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {...Object} [args] The arguments to the method.
   * @return {Actionable} A thenable object, use of which will trigger the
   * instance to be saved.
   * @see {@link BaseRelation#methods}
   */
  _addObjects: function(instance) {
    var args = _.rest(arguments);
    var objects = _.flatten(args);
    this.beforeAddingObjects(instance, objects);
    this.associateObjects(instance, objects);
    return Actionable.create(instance.save.bind(instance));
  },

  /**
   * Perform the necessary updates to add objects for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery}.
   *
   * Mixins can override {@link HasMany#executeAdd} to change the way updates
   * are performed when related objects are added. This is the default
   * implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {Array} objects The objects to add to the relationship.
   */
  _executeAdd: function(instance, objects) {
    var self = this;
    var after = this.afterAddingObjects.bind(this, instance, objects);
    return self._prepareForForeignKeyUpdate(instance, objects)
    .then(function() {
      return self._updateForeignKeys(instance, objects, instance.id);
    })
    .tap(after);
  },

  /**
   * The remove objects method for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and removes
   * the related objects from the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `remove<Singular>` and
   * `remove<Plural>`. For instance a user that has many articles would cause
   * this method to get triggered via `user.removeArticle` or
   * `user.removeArticles`.
   *
   * The naming conventions are set forth in {@link HasMany.methods}.
   *
   * Mixins can override {@link HasMany#removeObjects} to change the way
   * related objects are removed. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {...Object} [args] The arguments to the method.
   * @return {Actionable} A thenable object, use of which will trigger the
   * instance to be saved.
   * @see {@link BaseRelation#methods}
   */
  _removeObjects: function(instance) {
    var args = _.rest(arguments);
    var objects = _.flatten(args);
    this.beforeRemovingObjects(instance, objects);
    this.disassociateObjects(instance, objects);
    return Actionable.create(instance.save.bind(instance));
  },

  /**
   * Perform the necessary updates to remove objects for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery}.
   *
   * Mixins can override {@link HasMany#executeRemove} to change the way
   * updates are performed when related objects are removed. This is the
   * default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @param {Array} objects The objects to remove from the relationship.
   */
  _executeRemove: function(instance, objects) {
    var self = this;
    var after = this.afterRemovingObjects.bind(this, instance, objects);
    var removable = _.filter(objects, 'persisted');
    return self._prepareForForeignKeyUpdate(instance, removable)
    .then(function() {
      return self._updateForeignKeys(instance, removable, undefined);
    })
    .tap(after);
  },

  /**
   * The clear objects method for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery} cache and removes
   * all objects from the {@link HasMany#collection} (if loaded).
   *
   * It is accessible on an individual model via `clear<Plural>`. For
   * instance, a user that has many articles would cause this method to get
   * triggered via `user.clearArticles`.
   *
   * The naming conventions are set forth in {@link HasMany.methods}.
   *
   * Mixins can override {@link HasMany#clearObjects} to change the way related
   * objects are cleared. This is the default implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   * @return {Actionable} A thenable object, use of which will trigger the
   * instance to be saved.
   * @see {@link BaseRelation#methods}
   */
  _clearObjects: function(instance) {
    this.beforeClearingObjects(instance);
    return Actionable.create(instance.save.bind(instance));
  },

  /**
   * Perform the necessary updates to clear objects for this relation.
   *
   * This method invalidates the {@link HasMany#objectsQuery}.
   *
   * Mixins can override {@link HasMany#executeClear} to change the way clears
   * are performed when related objects are cleared. This is the default
   * implementation.
   *
   * @method
   * @protected
   * @param {Model} instance The model instance on which to operate.
   */
  _executeClear: function(instance) {
    var updates = _.object([[this.foreignKey, undefined]]);
    var query = this.objectsQuery(instance).update(updates);
    var after = this.afterClearingObjects.bind(this, instance);
    return query.execute().tap(after);
  },

  /**
   * Prepare many related objects for a foreign key update.
   *
   * This will save any dirty records unless the only attribute that is dirty
   * is the foreign key. It will also save any newly created objects regardless
   * of whether or not they are dirty.
   *
   * @method
   * @private
   * @param {Model} instance The model instance on which to operate.
   * @param {Array.<Model>} relatedObjects The related models to prepare.
   * @return {Promise} A promise that resolves when the prep work has finished.
   */
  _prepareForForeignKeyUpdate: function(instance, relatedObjects) {
    var foreignKeyAttr = this.foreignKeyAttr;
    return BluebirdPromise.map(relatedObjects, function(obj) {
      var dirty = _.without(obj.dirtyAttributes, foreignKeyAttr);
      var save = dirty.length || obj.newRecord;
      return save && obj.save();
    });
  },

  /**
   * Update foreign keys for a set of related objects.
   *
   * This will update the foreign keys of several objects in a single query.
   * This allows much faster creation of associations at the database level.
   *
   * After a successful update, it will clean the foreign key attribute of
   * each updated object, likely leaving the object completely clean.
   *
   * @method
   * @private
   * @param {Model} instance The model instance on which to operate.
   * @param {Array.<Model>} relatedObjects The related models on which to
   * update keys.
   * @param {?} id The id to use as the value of the foreign key.
   * @return {Promise} A promise that resolves when the update has finished.
   */
  _updateForeignKeys: function(instance, relatedObjects, id) {
    if (!_.any(relatedObjects, 'dirty')) { return BluebirdPromise.bind(); }

    var foreignKey = this.foreignKey;
    var foreignKeyAttr = this.foreignKeyAttr;
    var relatedPks = _.map(relatedObjects, 'pk');

    var updates = _.object([[foreignKey, id]]);
    var conditions = {};
    if (relatedPks.length === 1) { conditions.pk = relatedPks[0]; }
    else { conditions.pk$in = relatedPks; }

    var query = this._relatedModel.objects;
    return query.where(conditions).update(updates).execute().then(function() {
      relatedObjects.forEach(function(obj) {
        obj.cleanAttribute(foreignKeyAttr);
      });
    });
  },

  /**
   * Override of {@link HasMany#associateFetchedObjects} to call before & after
   * hooks so that mixins can get involved in the association process.
   *
   * @method
   * @protected
   * @see {@link HasMany#associateFetchedObjects}
   */
  associateFetchedObjects: notify('associatingFetchedObjects'),

  /**
   * Override of {@link HasMany#associateObjects} to call before & after hooks
   * so that mixins can get involved in the association process.
   *
   * @method
   * @protected
   * @see {@link HasMany#associateObjects}
   */
  associateObjects: notify('associatingObjects'),

  /**
   * Override of {@link HasMany#disassociateObjects} to call before & after
   * hooks so that mixins can get involved in the disassociation process.
   *
   * @method
   * @protected
   * @see {@link HasMany#disassociateObjects}
   */
  disassociateObjects: notify('disassociatingObjects'),

});

HasMany.reopenClass(/** @lends HasMany */ {

  /**
   * Naming conventions for `HasMany` relations.
   *
   * @private
   * @type {Object}
   * @see {@link BaseRelation#methods}
   */
  methods: {
    '<plural>': BaseRelation.property('collection'),
    '<plural>Relation': BaseRelation.property('identity'),
    '<singular>Objects': BaseRelation.property('objectsQuery'),
    'create<Singular>': BaseRelation.method('createObject'),
    'add<Singular>': BaseRelation.method('addObjects'),
    'add<Plural>': BaseRelation.method('addObjects'),
    'remove<Singular>': BaseRelation.method('removeObjects'),
    'remove<Plural>': BaseRelation.method('removeObjects'),
    'clear<Plural>': BaseRelation.method('clearObjects'),
    'init': BaseRelation.method('initialize'),
    'save': BaseRelation.method('save')
  }

});

module.exports = HasMany.reopenClass({ __name__: 'HasMany' });
<|endoftext|>


<commit_before>var cluster = require('cluster');
var fs = require('fs');
var domain = require('domain');

var thinkHttp = thinkRequire('Http');
var Dispatcher = thinkRequire('Dispatcher');

var App = module.exports = {};
/**
 * 根据http里的group和controller获取对应的controller实例
 * @param  {[type]} http [description]
 * @return {[type]}      [description]
 */
App.getBaseController = function(http){
  'use strict';
  var gc = ucfirst(http.group) + '/' + ucfirst(http.controller) + 'Controller';
  var path = getThinkRequirePath(gc);
  if (path) {
    return require(path)(http);
  }
}
/**
 * controller不存在时调用的默认controller
 * @return {[type]} [description]
 */
App.getCallController = function(http){
  'use strict';
  //如果是RESTFUL API，则调用RestController
  if (http.isRestful) {
    return thinkRequire('RestController')(http);
  }
  var config = C('call_controller');
  if (!config) {
    return;
  }
  if (isString(config)) {
    config = config.split(':');
  }
  var action = Dispatcher.getAction(config.pop());
  var controller = Dispatcher.getController(config.pop());
  var group = Dispatcher.getGroup(config.pop());
  var instance = this.getBaseController({
    group: group,
    controller: controller
  })
  if (instance && isFunction(instance[action + C('action_suffix')])) {
    http.group = group;
    http.controller = controller;
    http.action = action;
  }
  return instance;
}

/**
 * 执行具体的action，调用前置和后置操作
 * @return {[type]} [description]
 */
App.execAction = function(controller, action, data, callMethod){
  'use strict';
  //action操作
  var act = action + C('action_suffix');
  var flag = false;
  //action不存在时执行魔术方法
  if (callMethod && !isFunction(controller[act])) {
    var call = C('call_method');
    if (call && isFunction(controller[call])) {
      flag = true;
      act = call;
    }
  }
  //action不存在
  if (!isFunction(controller[act])) {
    return getPromise(new Error('action `' + action + '` not found. '), true);
  }
  var promise = getPromise();
  //action前置操作
  var before = C('before_action');
  if (before && isFunction(controller[before])) {
    promise = getPromise(controller[before](action));
  }
  promise = promise.then(function(){
    //action魔术方法只传递action参数
    if (flag) {
      return controller[act](action);
    }
    if (data) {
      return controller[act].apply(controller, data);
    }else{
      return controller[act]();
    }
  });
  //action后置操作
  var after = C('after_action');
  if (after && isFunction(controller[after])) {
    promise = promise.then(function(){
      return controller[after](action);
    })
  }
  return promise;
}

/**
 * 获取action的形参
 * @return {[type]} [description]
 */
App.getActionParams = function(fn, http){
  'use strict';
  //注释的正则
  var commentReg = /((\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s))/mg;
  //获取形参的正则
  var parsReg = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
  var toString = fn.toString().replace(commentReg, '');
  var match = toString.match(parsReg)[1].split(/\s*,\s*/);
  //匹配到形参
  var params;
  if (match && match.length) {
    params = match.map(function(item){
      return http.post[item] || http.get[item] || '';
    });
  }
  return params;
}
/**
 * 执行
 * @param  {[type]} http [description]
 * @return {[type]}      [description]
 */
App.exec = function(http){
  'use strict';
  var controller = this.getBaseController(http) || this.getCallController(http);
  //controller不存在
  if (!controller) {
    var err = new Error('Controller `' + http.controller + '` not found. ' + http.pathname);
    return getPromise(err, true);
  }
  //controller类实例
  http.controllerInstance = controller;
  var params;
  var actionFn = controller[http.action + C('action_suffix')];
  //参数绑定
  if (C('url_params_bind') && isFunction(actionFn)) {
    params = this.getActionParams(actionFn, http);
  }
  var promise = getPromise(controller.__initReturn);
  var self = this;
  return promise.then(function(){
    return self.execAction(controller, http.action, params, true);
  })
}
/**
 * 发送错误信息
 * @param  {[type]} error [description]
 * @return {[type]}       [description]
 */
App.sendError = function(http, error){
  'use strict';
  var message = isError(error) ? error.stack : error;
  console.error(message);
  if (!http.res) {
    return;
  }
  if (APP_DEBUG) {
    http.res.statusCode = 500;
    http.res.end(message);
  }else{
    http.res.statusCode = 500;
    http.setHeader('Content-Type', 'text/html; charset=' + C('encoding'));
    var readStream = fs.createReadStream(C('error_tpl_path'));
    readStream.pipe(http.res);
    readStream.on('end', function(){
      http.res.end();
    });
  }
}

/**
 * run
 * @return {[type]} [description]
 */
App.run = function(){
  'use strict';
  if (APP_MODE && App.mode[APP_MODE]) {
    return App.mode[APP_MODE]();
  }
  return App.mode.http();
};
/**
 * 不同模式下的run
 * @type {Object}
 */
App.mode = {
  //命令行模式
  cli: function(){
    'use strict';
    var defaultHttp = thinkHttp.getDefaultHttp(process.argv[2]);
    thinkHttp(defaultHttp.req, defaultHttp.res).run().then(App.listener);
  },
  //HTTP模式
  http: function(){
    'use strict';
    var clusterNums = C('use_cluster');
    //不使用cluster
    if (!clusterNums) {
      return App.createServer();
    }
    //使用cpu的个数
    if (clusterNums === true) {
      clusterNums = require('os').cpus().length;
    }
    if (cluster.isMaster) {
      for (var i = 0; i < clusterNums; i++) {
        cluster.fork();
      }
      cluster.on('exit', function(worker) {
        console.log('worker ' + worker.process.pid + ' died');
        process.nextTick(function(){
          cluster.fork();
        });
      });
    }else {
      App.createServer();
    }
  }
};
/**
 * 创建服务
 * @return {[type]} [description]
 */
App.createServer = function(){
  'use strict';
  //自定义创建server
  var createServerFn = C('create_server_fn');
  if (createServerFn) {
    if (isFunction(createServerFn)) {
      return createServerFn(App);
    }else if (isFunction(global[createServerFn])) {
      return global[createServerFn](App);
    }
  }
  var server = require('http').createServer(function (req, res) {
    thinkHttp(req, res).run().then(App.listener);
  });
  thinkRequire('WebSocket')(server, App).run();
  server.listen(C('port'));
  if (APP_DEBUG) {
    console.log('Server running at http://127.0.0.1:' + C('port') + '/');
  }
}
/**
 * 监听回调函数
 * @param  {[type]} http [description]
 * @return {[type]}      [description]
 */
App.listener = function(http){
  'use strict';
  //自动发送thinkjs和版本的header
  http.setHeader('X-Powered-By', 'thinkjs-' + THINK_VERSION);
  //禁止远程直接用带端口的访问,websocket下允许
  if (C('use_proxy') && http.host !== http.hostname && !http.websocket) {
    http.res.statusCode = 403;
    http.res.end();
    return getDefer().promise;
  }
  var domainInstance = domain.create();
  var deferred = getDefer();
  domainInstance.on('error', function(err){
    App.sendError(http, err);
    deferred.reject(err);
  });
  domainInstance.run(function(){
    return tag('app_init', http).then(function(){
      return Dispatcher(http).run();
    }).then(function(){
      return tag('app_begin', http);
    }).then(function(){
      return tag('action_init', http);
    }).then(function(){
      return App.exec(http);
    }).then(function(){
      return tag('app_end', http);
    }).catch(function(err){
      App.sendError(http, err);
    }).then(function(){
      deferred.resolve();
    })
  });
  return deferred.promise;
};<commit_msg>sendError接口如果error为空，则不处理<commit_after>var cluster = require('cluster');
var fs = require('fs');
var domain = require('domain');

var thinkHttp = thinkRequire('Http');
var Dispatcher = thinkRequire('Dispatcher');

var App = module.exports = {};
/**
 * 根据http里的group和controller获取对应的controller实例
 * @param  {[type]} http [description]
 * @return {[type]}      [description]
 */
App.getBaseController = function(http){
  'use strict';
  var gc = ucfirst(http.group) + '/' + ucfirst(http.controller) + 'Controller';
  var path = getThinkRequirePath(gc);
  if (path) {
    return require(path)(http);
  }
}
/**
 * controller不存在时调用的默认controller
 * @return {[type]} [description]
 */
App.getCallController = function(http){
  'use strict';
  //如果是RESTFUL API，则调用RestController
  if (http.isRestful) {
    return thinkRequire('RestController')(http);
  }
  var config = C('call_controller');
  if (!config) {
    return;
  }
  if (isString(config)) {
    config = config.split(':');
  }
  var action = Dispatcher.getAction(config.pop());
  var controller = Dispatcher.getController(config.pop());
  var group = Dispatcher.getGroup(config.pop());
  var instance = this.getBaseController({
    group: group,
    controller: controller
  })
  if (instance && isFunction(instance[action + C('action_suffix')])) {
    http.group = group;
    http.controller = controller;
    http.action = action;
  }
  return instance;
}

/**
 * 执行具体的action，调用前置和后置操作
 * @return {[type]} [description]
 */
App.execAction = function(controller, action, data, callMethod){
  'use strict';
  //action操作
  var act = action + C('action_suffix');
  var flag = false;
  //action不存在时执行魔术方法
  if (callMethod && !isFunction(controller[act])) {
    var call = C('call_method');
    if (call && isFunction(controller[call])) {
      flag = true;
      act = call;
    }
  }
  //action不存在
  if (!isFunction(controller[act])) {
    return getPromise(new Error('action `' + action + '` not found. '), true);
  }
  var promise = getPromise();
  //action前置操作
  var before = C('before_action');
  if (before && isFunction(controller[before])) {
    promise = getPromise(controller[before](action));
  }
  promise = promise.then(function(){
    //action魔术方法只传递action参数
    if (flag) {
      return controller[act](action);
    }
    if (data) {
      return controller[act].apply(controller, data);
    }else{
      return controller[act]();
    }
  });
  //action后置操作
  var after = C('after_action');
  if (after && isFunction(controller[after])) {
    promise = promise.then(function(){
      return controller[after](action);
    })
  }
  return promise;
}

/**
 * 获取action的形参
 * @return {[type]} [description]
 */
App.getActionParams = function(fn, http){
  'use strict';
  //注释的正则
  var commentReg = /((\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s))/mg;
  //获取形参的正则
  var parsReg = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
  var toString = fn.toString().replace(commentReg, '');
  var match = toString.match(parsReg)[1].split(/\s*,\s*/);
  //匹配到形参
  var params;
  if (match && match.length) {
    params = match.map(function(item){
      return http.post[item] || http.get[item] || '';
    });
  }
  return params;
}
/**
 * 执行
 * @param  {[type]} http [description]
 * @return {[type]}      [description]
 */
App.exec = function(http){
  'use strict';
  var controller = this.getBaseController(http) || this.getCallController(http);
  //controller不存在
  if (!controller) {
    var err = new Error('Controller `' + http.controller + '` not found. ' + http.pathname);
    return getPromise(err, true);
  }
  //controller类实例
  http.controllerInstance = controller;
  var params;
  var actionFn = controller[http.action + C('action_suffix')];
  //参数绑定
  if (C('url_params_bind') && isFunction(actionFn)) {
    params = this.getActionParams(actionFn, http);
  }
  var promise = getPromise(controller.__initReturn);
  var self = this;
  return promise.then(function(){
    return self.execAction(controller, http.action, params, true);
  })
}
/**
 * 发送错误信息
 * @param  {[type]} error [description]
 * @return {[type]}       [description]
 */
App.sendError = function(http, error){
  'use strict';
  if (!error) {
    return;
  }
  var message = isError(error) ? error.stack : error;
  console.error(message);
  if (!http.res) {
    return;
  }
  if (APP_DEBUG) {
    http.res.statusCode = 500;
    http.res.end(message);
  }else{
    http.res.statusCode = 500;
    http.setHeader('Content-Type', 'text/html; charset=' + C('encoding'));
    var readStream = fs.createReadStream(C('error_tpl_path'));
    readStream.pipe(http.res);
    readStream.on('end', function(){
      http.res.end();
    });
  }
}

/**
 * run
 * @return {[type]} [description]
 */
App.run = function(){
  'use strict';
  if (APP_MODE && App.mode[APP_MODE]) {
    return App.mode[APP_MODE]();
  }
  return App.mode.http();
};
/**
 * 不同模式下的run
 * @type {Object}
 */
App.mode = {
  //命令行模式
  cli: function(){
    'use strict';
    var defaultHttp = thinkHttp.getDefaultHttp(process.argv[2]);
    thinkHttp(defaultHttp.req, defaultHttp.res).run().then(App.listener);
  },
  //HTTP模式
  http: function(){
    'use strict';
    var clusterNums = C('use_cluster');
    //不使用cluster
    if (!clusterNums) {
      return App.createServer();
    }
    //使用cpu的个数
    if (clusterNums === true) {
      clusterNums = require('os').cpus().length;
    }
    if (cluster.isMaster) {
      for (var i = 0; i < clusterNums; i++) {
        cluster.fork();
      }
      cluster.on('exit', function(worker) {
        console.log('worker ' + worker.process.pid + ' died');
        process.nextTick(function(){
          cluster.fork();
        });
      });
    }else {
      App.createServer();
    }
  }
};
/**
 * 创建服务
 * @return {[type]} [description]
 */
App.createServer = function(){
  'use strict';
  //自定义创建server
  var createServerFn = C('create_server_fn');
  if (createServerFn) {
    if (isFunction(createServerFn)) {
      return createServerFn(App);
    }else if (isFunction(global[createServerFn])) {
      return global[createServerFn](App);
    }
  }
  var server = require('http').createServer(function (req, res) {
    thinkHttp(req, res).run().then(App.listener);
  });
  thinkRequire('WebSocket')(server, App).run();
  server.listen(C('port'));
  if (APP_DEBUG) {
    console.log('Server running at http://127.0.0.1:' + C('port') + '/');
  }
}
/**
 * 监听回调函数
 * @param  {[type]} http [description]
 * @return {[type]}      [description]
 */
App.listener = function(http){
  'use strict';
  //自动发送thinkjs和版本的header
  http.setHeader('X-Powered-By', 'thinkjs-' + THINK_VERSION);
  //禁止远程直接用带端口的访问,websocket下允许
  if (C('use_proxy') && http.host !== http.hostname && !http.websocket) {
    http.res.statusCode = 403;
    http.res.end();
    return getDefer().promise;
  }
  var domainInstance = domain.create();
  var deferred = getDefer();
  domainInstance.on('error', function(err){
    App.sendError(http, err);
    deferred.reject(err);
  });
  domainInstance.run(function(){
    return tag('app_init', http).then(function(){
      return Dispatcher(http).run();
    }).then(function(){
      return tag('app_begin', http);
    }).then(function(){
      return tag('action_init', http);
    }).then(function(){
      return App.exec(http);
    }).then(function(){
      return tag('app_end', http);
    }).catch(function(err){
      App.sendError(http, err);
    }).then(function(){
      deferred.resolve();
    })
  });
  return deferred.promise;
};<|endoftext|>


<commit_before>/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog */

(function() {
    var viewer;

    module('Basic', {
        setup: function () {
            var example = $('

').appendTo("#qunit-fixture");

            testLog.reset();

            viewer = OpenSeadragon({
                id:            'example',
                prefixUrl:     '/build/openseadragon/images/',
                springStiffness: 100 // Faster animation = faster tests
            });
        },
        teardown: function () {
            if (viewer && viewer.close) {
                viewer.close();
            }

            viewer = null;
        }
    });

    // ----------
    asyncTest('Open', function() {
        ok(viewer, 'Viewer exists');

        var openHandler = function(event) {
            viewer.removeHandler('open', openHandler);
            ok(true, 'Open event was sent');
            ok(event, 'Handler received event data');
            equal(event.eventSource, viewer, 'Sender of open event was viewer');
            ok(viewer.viewport, 'Viewport exists');
            ok(viewer.source, 'source exists');
            ok(viewer._updateRequestId, 'timer is on');
            start();
        };

        viewer.addHandler('open', openHandler);
        viewer.open('/test/data/testpattern.dzi');
    });

    asyncTest('Open Error Handling', function() {
        ok(viewer, 'Viewer exists');

        viewer.addHandler('open', function(event) {
            ok(false, "The open event should not fire for failed opens");
            start();
        });

        viewer.addHandler('open-failed', function(event) {
            ok(true, "The open-failed event should be fired when the source 404s");

            equal($(".openseadragon-message").length, 1, "Open failures should display a message");

            ok(testLog.log.contains('["AJAX request returned %d: %s",404,"/test/data/not-a-real-file"]'),
               "AJAX failures should be logged to the console");

            start();
        });

        viewer.open('/test/data/not-a-real-file');
    });

    // ----------
    asyncTest('Zoom', function() {
        viewer.addHandler("open", function () {
            var viewport = viewer.viewport;

            equal(viewport.getZoom(), 1, 'We start out unzoomed');

            var zoomHandler = function() {
                viewer.removeHandler('animation-finish', zoomHandler);
                equal(viewport.getZoom(), 2, 'Zoomed correctly');
                start();
            };

            viewer.addHandler('animation-finish', zoomHandler);
            viewport.zoomTo(2);
        });
        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('Pan', function() {
        viewer.addHandler("open", function () {
            var viewport = viewer.viewport,
                center = viewport.getCenter();

            ok(center.x === 0.5 && center.y === 0.5, 'We start out unpanned');

            var panHandler = function() {
                viewer.removeHandler('animation-finish', panHandler);
                center = viewport.getCenter();
                Util.assessNumericValue(center.x, 0.1, 0.00001, 'panned horizontally');
                Util.assessNumericValue(center.y, 0.1, 0.00001, 'panned vertically');
                start();
            };

            viewer.addHandler('animation-finish', panHandler);
            viewport.panTo(new OpenSeadragon.Point(0.1, 0.1));
        });

        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('Home', function() {
        // Test setup:
        function opener() {
            var viewport = viewer.viewport;
            viewport.panTo(new OpenSeadragon.Point(0.1, 0.1));
            viewport.zoomTo(2);
        }

        function stage1() {
            var viewport = viewer.viewport,
                center = viewport.getCenter();

            viewer.removeHandler('animation-finish', stage1);

            ok(center.x !== 0.5 && center.y !== 0.5, 'We start out panned');
            notEqual(viewport.getZoom(), 1, 'We start out zoomed');

            var homeHandler = function() {
                viewer.removeHandler('animation-finish', homeHandler);
                center = viewport.getCenter();
                ok(center.x === 0.5 && center.y === 0.5, 'We end up unpanned');
                equal(viewport.getZoom(), 1, 'We end up unzoomed');
                start();
            };

            viewer.addHandler('animation-finish', homeHandler);
            viewer.viewport.goHome(true);
        }

        viewer.addHandler("open", opener);
        viewer.addHandler("animation-finish", stage1);

        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('Click', function() {
        viewer.addHandler("open", function () {
            var viewport = viewer.viewport,
            center = viewport.getCenter();

            ok(center.x === 0.5 && center.y === 0.5, 'We start out unpanned');
            equal(viewport.getZoom(), 1, 'We start out unzoomed');

            var clickHandler = function() {
                viewer.removeHandler('animation-finish', clickHandler);
                center = viewport.getCenter();
                ok(center.x > 0.37 && center.x < 0.38 && center.y > 0.37 && center.y < 0.38, 'Panned correctly');
                equal(viewport.getZoom(), 2, 'Zoomed correctly');
                start();
            };

            viewer.addHandler('animation-finish', clickHandler);
            Util.simulateViewerClickWithDrag( {
                viewer: viewer,
                widthFactor: 0.25,
                heightFactor: 0.25,
                dragCount: 0,
                dragDx: 0,
                dragDy: 0
            } );
        } );

        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('FullPage', function() {
        viewer.addHandler("open", function () {
            ok(!viewer.isFullPage(), 'Started out not fullpage');
            ok(!$(viewer.element).hasClass('fullpage'),
                'No fullpage class on div');

            var checkEnteringPreFullPage = function(event) {
                viewer.removeHandler('pre-full-page', checkEnteringPreFullPage);
                ok(event.fullPage, 'Switching to fullpage');
                ok(!viewer.isFullPage(), 'Not yet fullpage');
            };

            var checkEnteringFullPage = function(event) {
                viewer.removeHandler('full-page', checkEnteringFullPage);
                ok(event.fullPage, 'Switched to fullpage');
                ok(viewer.isFullPage(), 'Enabled fullpage');
                ok($(viewer.element).hasClass('fullpage'),
                    'Fullpage class added to div');

                var checkExitingPreFullPage = function(event) {
                    viewer.removeHandler('pre-full-page', checkExitingPreFullPage);
                    ok(!event.fullPage, 'Exiting fullpage');
                    ok(viewer.isFullPage(), 'Still fullpage');
                };

                var checkExitingFullPage = function(event) {
                    viewer.removeHandler('full-page', checkExitingFullPage);
                    ok(!event.fullPage, 'Exiting fullpage');
                    ok(!viewer.isFullPage(), 'Disabled fullpage');
                    ok(!$(viewer.element).hasClass('fullpage'),
                        'Fullpage class removed from div');
                    start();
                };

                viewer.addHandler("pre-full-page", checkExitingPreFullPage);
                viewer.addHandler("full-page", checkExitingFullPage);
                viewer.setFullPage(false);
            };
            viewer.addHandler("pre-full-page", checkEnteringPreFullPage);
            viewer.addHandler("full-page", checkEnteringFullPage);
            viewer.setFullPage(true);
        });

        viewer.open('/test/data/testpattern.dzi');
    });

    asyncTest('FullScreen', function() {

        if (!OpenSeadragon.supportsFullScreen) {
            expect(0);
            start();
            return;
        }

        viewer.addHandler("open", function () {
            ok(!OpenSeadragon.isFullScreen(), 'Started out not fullscreen');

            var checkEnteringPreFullScreen = function(event) {
                viewer.removeHandler('pre-full-screen', checkEnteringPreFullScreen);
                ok(event.fullScreen, 'Switching to fullscreen');
                ok(!OpenSeadragon.isFullScreen(), 'Not yet fullscreen');
            };

            // The fullscreen mode is always denied during tests so we are
            // exiting directly.
            var checkExitingFullScreen = function(event) {
                viewer.removeHandler('full-screen', checkExitingFullScreen);
                ok(!event.fullScreen, 'Exiting fullscreen');
                ok(!OpenSeadragon.isFullScreen(), 'Disabled fullscreen');
                start();
            };
            viewer.addHandler("pre-full-screen", checkEnteringPreFullScreen);
            viewer.addHandler("full-screen", checkExitingFullScreen);
            viewer.setFullScreen(true);
        });

        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('Close', function() {
        viewer.addHandler("open", function () {
            var closeHandler = function() {
                viewer.removeHandler('close', closeHandler);
                ok(!viewer.source, 'no source');
                ok(true, 'Close event was sent');
                setTimeout(function() {
                    ok(!viewer._updateRequestId, 'timer is off');
                    start();
                }, 100);
            };

            viewer.addHandler('close', closeHandler);
            viewer.close();
        });
        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('Destroy', function() {
        viewer.addHandler("open", function () {
            // Check that the DOM has been modified
            notEqual(0, $('#example').children().length);

            var closeCalled = false;
            var closeHandler = function() {
                viewer.removeHandler('close', closeHandler);
                closeCalled = true;
            };

            viewer.addHandler('close', closeHandler);
            viewer.destroy();

            // Check that the DOM has been cleaned up
            equal(0, $('#example').children().length);
            equal(null, viewer.canvas);
            equal(null, viewer.keyboardCommandArea);
            equal(null, viewer.container);
            equal(null, viewer.element);
            equal(true, closeCalled);
            viewer = null;
            start();
        });
        viewer.open('/test/data/testpattern.dzi');
    });

    // The Wikipedia logo has CORS enabled
    var corsImg = 'http://upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png';

    // PhantomJS always taint the canvas, so we only run some tests on browsers
    // supporting CORS images.
    function browserSupportsImgCrossOrigin(callback) {
        var img = new Image();
        img.crossOrigin = 'anonymous';
        img.onload = function() {
            var canvas = document.createElement("canvas");
            var ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0);
            callback(!isCanvasTainted(ctx));
        };
        img.src = corsImg;
    }

    function isCanvasTainted(context) {
        var isTainted = false;
        try {
            // We test if the canvas is tainted by retrieving data from it.
            // An exception will be raised if the canvas is tainted.
            var url = context.getImageData(0, 0, 1, 1);
        } catch (e) {
            isTainted = true;
        }
        return isTainted;
    }

    asyncTest( 'CrossOriginPolicyMissing', function () {

        viewer.crossOriginPolicy = false;
        viewer.open( {
            type: 'legacy-image-pyramid',
            levels: [ {
                    url: corsImg,
                    width: 135,
                    height: 155
                } ]
        } );
        viewer.addHandler('tile-drawn', function() {
            ok(isCanvasTainted(viewer.drawer.context), "Canvas should be tainted.");
            start();
        });

    } );

    asyncTest( 'CrossOriginPolicyAnonymous', function () {

        browserSupportsImgCrossOrigin(function(supported) {
            if (!supported) {
                expect(0);
                start();
            } else {
                viewer.crossOriginPolicy = 'Anonymous';
                viewer.open( {
                    type: 'legacy-image-pyramid',
                    levels: [ {
                            url: corsImg,
                            width: 135,
                            height: 155
                        } ]
                } );
                viewer.addHandler('tile-drawn', function() {
                    ok(!isCanvasTainted(viewer.drawer.context), "Canvas should not be tainted.");
                    start();
                });
            }
        });

    } );

})();
<commit_msg>Remove duplicate isCanvasTainted function in tests.<commit_after>/* global module, asyncTest, $, ok, equal, notEqual, start, test, Util, testLog */

(function() {
    var viewer;

    module('Basic', {
        setup: function () {
            var example = $('

').appendTo("#qunit-fixture");

            testLog.reset();

            viewer = OpenSeadragon({
                id:            'example',
                prefixUrl:     '/build/openseadragon/images/',
                springStiffness: 100 // Faster animation = faster tests
            });
        },
        teardown: function () {
            if (viewer && viewer.close) {
                viewer.close();
            }

            viewer = null;
        }
    });

    // ----------
    asyncTest('Open', function() {
        ok(viewer, 'Viewer exists');

        var openHandler = function(event) {
            viewer.removeHandler('open', openHandler);
            ok(true, 'Open event was sent');
            ok(event, 'Handler received event data');
            equal(event.eventSource, viewer, 'Sender of open event was viewer');
            ok(viewer.viewport, 'Viewport exists');
            ok(viewer.source, 'source exists');
            ok(viewer._updateRequestId, 'timer is on');
            start();
        };

        viewer.addHandler('open', openHandler);
        viewer.open('/test/data/testpattern.dzi');
    });

    asyncTest('Open Error Handling', function() {
        ok(viewer, 'Viewer exists');

        viewer.addHandler('open', function(event) {
            ok(false, "The open event should not fire for failed opens");
            start();
        });

        viewer.addHandler('open-failed', function(event) {
            ok(true, "The open-failed event should be fired when the source 404s");

            equal($(".openseadragon-message").length, 1, "Open failures should display a message");

            ok(testLog.log.contains('["AJAX request returned %d: %s",404,"/test/data/not-a-real-file"]'),
               "AJAX failures should be logged to the console");

            start();
        });

        viewer.open('/test/data/not-a-real-file');
    });

    // ----------
    asyncTest('Zoom', function() {
        viewer.addHandler("open", function () {
            var viewport = viewer.viewport;

            equal(viewport.getZoom(), 1, 'We start out unzoomed');

            var zoomHandler = function() {
                viewer.removeHandler('animation-finish', zoomHandler);
                equal(viewport.getZoom(), 2, 'Zoomed correctly');
                start();
            };

            viewer.addHandler('animation-finish', zoomHandler);
            viewport.zoomTo(2);
        });
        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('Pan', function() {
        viewer.addHandler("open", function () {
            var viewport = viewer.viewport,
                center = viewport.getCenter();

            ok(center.x === 0.5 && center.y === 0.5, 'We start out unpanned');

            var panHandler = function() {
                viewer.removeHandler('animation-finish', panHandler);
                center = viewport.getCenter();
                Util.assessNumericValue(center.x, 0.1, 0.00001, 'panned horizontally');
                Util.assessNumericValue(center.y, 0.1, 0.00001, 'panned vertically');
                start();
            };

            viewer.addHandler('animation-finish', panHandler);
            viewport.panTo(new OpenSeadragon.Point(0.1, 0.1));
        });

        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('Home', function() {
        // Test setup:
        function opener() {
            var viewport = viewer.viewport;
            viewport.panTo(new OpenSeadragon.Point(0.1, 0.1));
            viewport.zoomTo(2);
        }

        function stage1() {
            var viewport = viewer.viewport,
                center = viewport.getCenter();

            viewer.removeHandler('animation-finish', stage1);

            ok(center.x !== 0.5 && center.y !== 0.5, 'We start out panned');
            notEqual(viewport.getZoom(), 1, 'We start out zoomed');

            var homeHandler = function() {
                viewer.removeHandler('animation-finish', homeHandler);
                center = viewport.getCenter();
                ok(center.x === 0.5 && center.y === 0.5, 'We end up unpanned');
                equal(viewport.getZoom(), 1, 'We end up unzoomed');
                start();
            };

            viewer.addHandler('animation-finish', homeHandler);
            viewer.viewport.goHome(true);
        }

        viewer.addHandler("open", opener);
        viewer.addHandler("animation-finish", stage1);

        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('Click', function() {
        viewer.addHandler("open", function () {
            var viewport = viewer.viewport,
            center = viewport.getCenter();

            ok(center.x === 0.5 && center.y === 0.5, 'We start out unpanned');
            equal(viewport.getZoom(), 1, 'We start out unzoomed');

            var clickHandler = function() {
                viewer.removeHandler('animation-finish', clickHandler);
                center = viewport.getCenter();
                ok(center.x > 0.37 && center.x < 0.38 && center.y > 0.37 && center.y < 0.38, 'Panned correctly');
                equal(viewport.getZoom(), 2, 'Zoomed correctly');
                start();
            };

            viewer.addHandler('animation-finish', clickHandler);
            Util.simulateViewerClickWithDrag( {
                viewer: viewer,
                widthFactor: 0.25,
                heightFactor: 0.25,
                dragCount: 0,
                dragDx: 0,
                dragDy: 0
            } );
        } );

        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('FullPage', function() {
        viewer.addHandler("open", function () {
            ok(!viewer.isFullPage(), 'Started out not fullpage');
            ok(!$(viewer.element).hasClass('fullpage'),
                'No fullpage class on div');

            var checkEnteringPreFullPage = function(event) {
                viewer.removeHandler('pre-full-page', checkEnteringPreFullPage);
                ok(event.fullPage, 'Switching to fullpage');
                ok(!viewer.isFullPage(), 'Not yet fullpage');
            };

            var checkEnteringFullPage = function(event) {
                viewer.removeHandler('full-page', checkEnteringFullPage);
                ok(event.fullPage, 'Switched to fullpage');
                ok(viewer.isFullPage(), 'Enabled fullpage');
                ok($(viewer.element).hasClass('fullpage'),
                    'Fullpage class added to div');

                var checkExitingPreFullPage = function(event) {
                    viewer.removeHandler('pre-full-page', checkExitingPreFullPage);
                    ok(!event.fullPage, 'Exiting fullpage');
                    ok(viewer.isFullPage(), 'Still fullpage');
                };

                var checkExitingFullPage = function(event) {
                    viewer.removeHandler('full-page', checkExitingFullPage);
                    ok(!event.fullPage, 'Exiting fullpage');
                    ok(!viewer.isFullPage(), 'Disabled fullpage');
                    ok(!$(viewer.element).hasClass('fullpage'),
                        'Fullpage class removed from div');
                    start();
                };

                viewer.addHandler("pre-full-page", checkExitingPreFullPage);
                viewer.addHandler("full-page", checkExitingFullPage);
                viewer.setFullPage(false);
            };
            viewer.addHandler("pre-full-page", checkEnteringPreFullPage);
            viewer.addHandler("full-page", checkEnteringFullPage);
            viewer.setFullPage(true);
        });

        viewer.open('/test/data/testpattern.dzi');
    });

    asyncTest('FullScreen', function() {

        if (!OpenSeadragon.supportsFullScreen) {
            expect(0);
            start();
            return;
        }

        viewer.addHandler("open", function () {
            ok(!OpenSeadragon.isFullScreen(), 'Started out not fullscreen');

            var checkEnteringPreFullScreen = function(event) {
                viewer.removeHandler('pre-full-screen', checkEnteringPreFullScreen);
                ok(event.fullScreen, 'Switching to fullscreen');
                ok(!OpenSeadragon.isFullScreen(), 'Not yet fullscreen');
            };

            // The fullscreen mode is always denied during tests so we are
            // exiting directly.
            var checkExitingFullScreen = function(event) {
                viewer.removeHandler('full-screen', checkExitingFullScreen);
                ok(!event.fullScreen, 'Exiting fullscreen');
                ok(!OpenSeadragon.isFullScreen(), 'Disabled fullscreen');
                start();
            };
            viewer.addHandler("pre-full-screen", checkEnteringPreFullScreen);
            viewer.addHandler("full-screen", checkExitingFullScreen);
            viewer.setFullScreen(true);
        });

        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('Close', function() {
        viewer.addHandler("open", function () {
            var closeHandler = function() {
                viewer.removeHandler('close', closeHandler);
                ok(!viewer.source, 'no source');
                ok(true, 'Close event was sent');
                setTimeout(function() {
                    ok(!viewer._updateRequestId, 'timer is off');
                    start();
                }, 100);
            };

            viewer.addHandler('close', closeHandler);
            viewer.close();
        });
        viewer.open('/test/data/testpattern.dzi');
    });

    // ----------
    asyncTest('Destroy', function() {
        viewer.addHandler("open", function () {
            // Check that the DOM has been modified
            notEqual(0, $('#example').children().length);

            var closeCalled = false;
            var closeHandler = function() {
                viewer.removeHandler('close', closeHandler);
                closeCalled = true;
            };

            viewer.addHandler('close', closeHandler);
            viewer.destroy();

            // Check that the DOM has been cleaned up
            equal(0, $('#example').children().length);
            equal(null, viewer.canvas);
            equal(null, viewer.keyboardCommandArea);
            equal(null, viewer.container);
            equal(null, viewer.element);
            equal(true, closeCalled);
            viewer = null;
            start();
        });
        viewer.open('/test/data/testpattern.dzi');
    });

    // The Wikipedia logo has CORS enabled
    var corsImg = 'http://upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png';

    // PhantomJS always taint the canvas, so we only run some tests on browsers
    // supporting CORS images.
    function browserSupportsImgCrossOrigin(callback) {
        var img = new Image();
        img.crossOrigin = 'anonymous';
        img.onload = function() {
            var canvas = document.createElement("canvas");
            var ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0);
            callback(!OpenSeadragon.isCanvasTainted(canvas));
        };
        img.src = corsImg;
    }

    asyncTest( 'CrossOriginPolicyMissing', function () {

        viewer.crossOriginPolicy = false;
        viewer.open( {
            type: 'legacy-image-pyramid',
            levels: [ {
                    url: corsImg,
                    width: 135,
                    height: 155
                } ]
        } );
        viewer.addHandler('tile-drawn', function() {
            ok(OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas),
                "Canvas should be tainted.");
            start();
        });

    } );

    asyncTest( 'CrossOriginPolicyAnonymous', function () {

        browserSupportsImgCrossOrigin(function(supported) {
            if (!supported) {
                expect(0);
                start();
            } else {
                viewer.crossOriginPolicy = 'Anonymous';
                viewer.open( {
                    type: 'legacy-image-pyramid',
                    levels: [ {
                            url: corsImg,
                            width: 135,
                            height: 155
                        } ]
                } );
                viewer.addHandler('tile-drawn', function() {
                    ok(!OpenSeadragon.isCanvasTainted(viewer.drawer.context.canvas),
                        "Canvas should not be tainted.");
                    start();
                });
            }
        });

    } );

})();
<|endoftext|>