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

describe('Model', function() {

	var WithSchemaField,
	    WithTranslation,
	    data,
	    _id;

	data = {
		firstname : 'Jelle',
		lastname  : 'De Loecker',
		nicknames : ['skerit', 'Jellie'],
		birthdate : new Date('1987-10-29'),
		male      : true
	};

	function testDocument(document, data) {
		assert.strictEqual(document.firstname,     data.firstname);
		assert.strictEqual(document.lastname,      data.lastname);
		assert.deepStrictEqual(document.nicknames, data.nicknames);
		assert.strictEqual(document.birthdate+'',  data.birthdate+'');
		assert.strictEqual(document.male,          data.male);
		assert.strictEqual(String(document._id).isObjectId(), true);
	}

	/**
	 * Inheritance testing
	 */
	describe('inheritance', function() {
		it('lets you inherit from the main Model class', function() {
			global.Person = Function.inherits('Alchemy.Model', function Person(options) {
				Person.super.call(this, options);
			});

			assert.strictEqual(Person.super, Classes.Alchemy.Model.Model, true);

			global.Event = Function.inherits('Alchemy.Model', function Event(options) {
				Person.super.call(this, options);
			});

			assert.strictEqual(Event.super, Classes.Alchemy.Model.Model, true);

			global.Product = Function.inherits('Alchemy.Model', function Product(options) {
				Product.super.call(this, options);
			});
		});
	});

	describe('.cache_duration', function() {
		it('gets the current cache_duration', function() {
			assert.strictEqual(Person.cache_duration, undefined);
		});

		it('gets the cache duration from the settings', function() {
			alchemy.settings.model_query_cache_duration = '1 second';

			assert.strictEqual(Person.cache_duration, '1 second');

			alchemy.settings.model_query_cache_duration = '2 seconds';
			assert.strictEqual(Person.cache_duration, '1 second', 'Changing the settings should not have affected the existing duration');

			Person.cache_duration = null;
			assert.strictEqual(Person.cache_duration, '2 seconds', 'After setting cache_duration to null, it should revert back to the settings value');
		});

		it('sets the duration for future caches', function() {

			// Warning: this has ZERO effect after the cache has been made
			Person.cache_duration = '3 seconds';
			assert.strictEqual(Person.cache_duration, '3 seconds');

			Person.cache_duration = '1 second';
			assert.strictEqual(Person.cache_duration, '1 second');
		});
	});

	describe('.cache', function() {
		var Animal;

		it('returns false if there is no duration set', function() {

			Animal = Function.inherits('Alchemy.Model', function Animal(options) {
				Animal.super.call(this, options);
			});

			Animal.cache_duration = null;
			assert.strictEqual(Animal.cache_duration, '2 seconds');
			Animal.cache_duration = false;
			assert.strictEqual(Animal.cache_duration, false);

			assert.strictEqual(Animal.cache, false);
		});

		it('returns the cache object', function() {
			Person.cache_duration = '10 ms';
			Animal.cache_duration = '10 ms';
			assert.strictEqual(typeof Animal.cache, 'object');
		});
	});

	/**
	 * The schema
	 */
	describe('.schema', function() {
		it('returns the Model\'s schema', function() {
			assert.strictEqual(Person.schema instanceof Classes.Alchemy.Schema, true);
		});

		it('returns false when getting the schema of the base Model class', function() {
			assert.strictEqual(Model.schema, false);
		});
	});

	/**
	 * Get the Document class for this model
	 */
	describe('.Document', function() {
		it('should create a Document class for this specific Model', function() {

			var PersonDocument = Person.Document;

			assert.strictEqual(PersonDocument,      Person.Document, 'Should return the same constructor instance');
			assert.strictEqual(PersonDocument.name, Person.name,     'The Document class should have the same name as the Model');
			assert.notStrictEqual(PersonDocument,   Person,          'The model & the document are different things');
		});
	});

	/**
	 * Get the ClientDocument class for this model
	 */
	describe('.ClientDocument', function() {
		it('should create a ClientDocument class for this specific Model', function() {

			var PersonDocument = Person.ClientDocument;

			assert.strictEqual(PersonDocument,      Person.ClientDocument, 'Should return the same constructor instance');
			assert.strictEqual(PersonDocument.name, Person.name,           'The Document class should have the same name as the Model');
			assert.notStrictEqual(PersonDocument,   Person,                'The model & the document are different things');
			assert.notStrictEqual(PersonDocument,   Person.Document,       'The ClientDocument & the Document are different things');
		});
	});

	/**
	 * Adding fields to the new Model
	 */
	describe('.addField(name, type, options)', function() {
		it('should add fields (during constitution) - Person example', function(done) {
			Person.constitute(function addFields() {

				this.addField('firstname', 'String');
				this.addField('lastname',  'String');
				this.addField('nicknames', 'String', {array: true});
				this.addField('birthdate', 'Date');
				this.addField('male',      'Boolean');

				done();
			});
		});

		it('should add fields (during constitution) - Event example', function(done) {
			Event.constitute(function addFields() {
				this.addField('name', 'String');
				this.hasAndBelongsToMany('Invited', 'Person');

				done();
			});
		});

		it('should add fields (during constitution) - Product example', function(done) {
			Product.constitute(function addFields() {
				this.addField('name', 'String');

				done();
			});
		});

		it('should be able to add schema as fields', function(next) {
			next = Function.regulate(next);

			WithSchemaField = Function.inherits('Alchemy.Model', function WithSchemaField(options) {
				WithSchemaField.super.call(this, options);
			});

			WithSchemaField.constitute(function addFields() {
				var schema = new Classes.Alchemy.Schema(this);

				schema.addField('subname', 'String');
				schema.addField('subvalue', 'String');

				this.addField('subschema', schema);

				var schema_two = new Classes.Alchemy.Schema(this);

				schema_two.addField('entryname', 'String');

				this.addField('entries', schema, {array: true});

				next();
			});
		});

		it('should be able to add translatable fields', function(next) {
			next = Function.regulate(next);

			WithTranslation = Function.inherits('Alchemy.Model', function WithTranslation(options) {
				WithTranslation.super.call(this, options);
			});

			WithTranslation.constitute(function addFields() {
				this.addField('name', 'String');
				this.addField('title', 'String', {translatable: true});

				next();
			});
		});
	});

	/**
	 * Add Document methods
	 */
	describe('.setDocumentMethod(fnc)', function() {
		it('adds a new method to the linked Document class', function() {
			Person.setDocumentMethod(function getFamiliarName() {
				var result;

				if (this.firstname) {
					return this.firstname;
				} else if (this.nicknames && this.nicknames[0]) {
					return this.nicknames[0];
				} else if (this.lastname) {
					return this.lastname;
				} else {
					return 'Unknown';
				}
			});

			assert.strictEqual(Person.Document.prototype.getFamiliarName.name, 'getFamiliarName');
			assert.strictEqual(Person.ClientDocument.prototype.getFamiliarName, undefined);
		});
	});

	/**
	 * Add Document properties
	 */
	describe('.setDocumentProperty(getter)', function() {
		it('adds a new property getter to the linked Document class', function() {
			Person.setDocumentProperty(function upper_firstname() {

				if (!this.firstname) {
					return '';
				}

				return this.firstname.toUpperCase();
			});

			var person = new Person.Document();

			assert.strictEqual(person.upper_firstname, '');

			person.firstname = 'test';

			assert.strictEqual(person.upper_firstname, 'TEST');
		});
	});

	/**
	 * The name of a model
	 */
	describe('#name', function() {
		it('should return the name of the model class', function() {
			var person = Model.get('Person');
			assert.strictEqual(person.name, Person.name);
		});

		it('should allow you to override the name per instance', function() {
			var person = Model.get('Person'),
			    second = Model.get('Person');

			assert.strictEqual(person.name, 'Person');
			assert.strictEqual(second.name, person.name);

			// Now set a different name
			second.name = 'SecondPerson';

			assert.strictEqual(second.name, 'SecondPerson');
			assert.strictEqual(person.name, 'Person');
			assert.strictEqual(Model.get('Person').name, 'Person');
		});
	});

	/**
	 * Getting an instance of the model
	 */
	describe('#get(model_name)', function() {
		it('should create a new instance of the wanted model', function() {

			var person = Model.get('Person');

			assert.strictEqual(person instanceof Person, true);
			assert.strictEqual(person instanceof Classes.Alchemy.Model.Model, true);

		});
	});

	/**
	 * Saving data
	 */
	describe('#save(data, callback)', function() {

		it('should save the data and call back with a DocumentList', function(done) {

			Model.get('Person').save(data, function saved(err, list) {

				if (err) {
					return done(err);
				}

				assert.strictEqual(list.length, 1);

				let document = list[0];

				testDocument(document, data);

				// Save the _id for next tests
				_id = document._id;

				// Save this for later tests
				global.person_doc = document;

				done();
			});
		});
	});

	/**
	 * Getting data
	 */
	describe('#find(\'first\', options, callback)', function() {
		it('should find 1 document by ObjectId instance', function(done) {
			Model.get('Person').find('first', {conditions: {_id: _id}}, function gotDocument(err, document) {

				if (err) {
					return done(err);
				}

				try {

					assert.notStrictEqual(document instanceof Classes.Alchemy.DocumentList, true, 'Should not have returned a DocumentList');
					assert.strictEqual(document instanceof Classes.Alchemy.Document.Document, true, 'Should have returned a Document');

					assert.strictEqual(String(document._id), String(_id));

					testDocument(document, data);
				} catch (err) {
					return done(err);
				}

				done();
			});
		});

		it('should find 1 document by ObjectId string', function(done) {
			Model.get('Person').find('first', {conditions: {_id: String(_id)}}, function gotDocument(err, document) {

				if (err) {
					return done(err);
				}

				try {
					assert.notStrictEqual(document instanceof Classes.Alchemy.DocumentList, true, 'Should not have returned a DocumentList');
					assert.strictEqual(document instanceof Classes.Alchemy.Document.Document, true, 'Should have returned a Document');

					assert.strictEqual(String(document._id), String(_id));

					testDocument(document, data);
				} catch (err) {
					return done(err);
				}

				done();
			});
		});
	});

	describe('#findById(object_id, callback)', function() {
		it('should find a single document by ObjectId instance', function(done) {
			Model.get('Person').findById(_id, function gotPerson(err, person) {

				if (err) {
					return done(err);
				}

				assert.strictEqual(String(_id), String(person._id));
				assert.strictEqual(person instanceof Classes.Alchemy.Document.Document, true);
				done();
			});
		});

		it('should find a single document by ObjectId string', function(done) {
			Model.get('Person').findById(String(_id), function gotPerson(err, person) {

				if (err) {
					return done(err);
				}

				assert.strictEqual(String(_id), String(person._id));
				assert.strictEqual(person instanceof Classes.Alchemy.Document.Document, true);
				done();
			});
		});
	});

	describe('#ensureIds(list, callback)', function() {

		var product,
		    list;

		list = [
			{
				_id   : '52efff000073570002000000',
				name  : 'screen'
			},
			{
				_id   : '52efff000073570002000001',
				name  : 'mouse'
			},
			{
				_id   : '52efff000073570002000002',
				name  : 'keyboard'
			}
		];

		it('should make sure the given ids & records exist in the database', async function() {

			product = Model.get('Product');

			await product.ensureIds(list, async function done(err) {

				if (err) {
					return done(err);
				}

				let prod;

				prod = await product.findById('52efff000073570002000000');
				assert.strictEqual(prod.name, 'screen');

				prod = await product.findById('52efff000073570002000001');
				assert.strictEqual(prod.name, 'mouse');

				prod = await product.findById('52efff000073570002000002');
				assert.strictEqual(prod.name, 'keyboard');

				let prods = await product.find('all');
				assert.strictEqual(prods.length, 3);

				let removed = await prod.remove();

				assert.strictEqual(removed, true);

				prod = await product.findById('52efff000073570002000002');
				assert.strictEqual(prod, null);

				prods = await product.find('all');
				assert.strictEqual(prods.length, 2, 'There should only be 3 products after one was removed');

				// Ensure them again
				await product.ensureIds(list);

				prods = await product.find('all');
				assert.strictEqual(prods.length, 3, 'There should only be 3 products: only the missing one should have been added');

				prod = await product.findById('52efff000073570002000002');
				assert.strictEqual(prod.name, 'keyboard');
			});
		});
	});
});<commit_msg>Add belongsTo field test<commit_after>var assert = require('assert');

describe('Model', function() {

	var WithSchemaField,
	    WithTranslation,
	    data,
	    _id;

	data = {
		firstname : 'Jelle',
		lastname  : 'De Loecker',
		nicknames : ['skerit', 'Jellie'],
		birthdate : new Date('1987-10-29'),
		male      : true
	};

	function testDocument(document, data) {
		assert.strictEqual(document.firstname,     data.firstname);
		assert.strictEqual(document.lastname,      data.lastname);
		assert.deepStrictEqual(document.nicknames, data.nicknames);
		assert.strictEqual(document.birthdate+'',  data.birthdate+'');
		assert.strictEqual(document.male,          data.male);
		assert.strictEqual(String(document._id).isObjectId(), true);
	}

	/**
	 * Inheritance testing
	 */
	describe('inheritance', function() {
		it('lets you inherit from the main Model class', function() {
			global.Person = Function.inherits('Alchemy.Model', function Person(options) {
				Person.super.call(this, options);
			});

			assert.strictEqual(Person.super, Classes.Alchemy.Model.Model, true);

			global.Event = Function.inherits('Alchemy.Model', function Event(options) {
				Person.super.call(this, options);
			});

			assert.strictEqual(Event.super, Classes.Alchemy.Model.Model, true);

			global.Product = Function.inherits('Alchemy.Model', function Product(options) {
				Product.super.call(this, options);
			});
		});
	});

	describe('.cache_duration', function() {
		it('gets the current cache_duration', function() {
			assert.strictEqual(Person.cache_duration, undefined);
		});

		it('gets the cache duration from the settings', function() {
			alchemy.settings.model_query_cache_duration = '1 second';

			assert.strictEqual(Person.cache_duration, '1 second');

			alchemy.settings.model_query_cache_duration = '2 seconds';
			assert.strictEqual(Person.cache_duration, '1 second', 'Changing the settings should not have affected the existing duration');

			Person.cache_duration = null;
			assert.strictEqual(Person.cache_duration, '2 seconds', 'After setting cache_duration to null, it should revert back to the settings value');
		});

		it('sets the duration for future caches', function() {

			// Warning: this has ZERO effect after the cache has been made
			Person.cache_duration = '3 seconds';
			assert.strictEqual(Person.cache_duration, '3 seconds');

			Person.cache_duration = '1 second';
			assert.strictEqual(Person.cache_duration, '1 second');
		});
	});

	describe('.cache', function() {
		var Animal;

		it('returns false if there is no duration set', function() {

			Animal = Function.inherits('Alchemy.Model', function Animal(options) {
				Animal.super.call(this, options);
			});

			Animal.cache_duration = null;
			assert.strictEqual(Animal.cache_duration, '2 seconds');
			Animal.cache_duration = false;
			assert.strictEqual(Animal.cache_duration, false);

			assert.strictEqual(Animal.cache, false);
		});

		it('returns the cache object', function() {
			Person.cache_duration = '10 ms';
			Animal.cache_duration = '10 ms';
			assert.strictEqual(typeof Animal.cache, 'object');
		});
	});

	/**
	 * The schema
	 */
	describe('.schema', function() {
		it('returns the Model\'s schema', function() {
			assert.strictEqual(Person.schema instanceof Classes.Alchemy.Schema, true);
		});

		it('returns false when getting the schema of the base Model class', function() {
			assert.strictEqual(Model.schema, false);
		});
	});

	/**
	 * Get the Document class for this model
	 */
	describe('.Document', function() {
		it('should create a Document class for this specific Model', function() {

			var PersonDocument = Person.Document;

			assert.strictEqual(PersonDocument,      Person.Document, 'Should return the same constructor instance');
			assert.strictEqual(PersonDocument.name, Person.name,     'The Document class should have the same name as the Model');
			assert.notStrictEqual(PersonDocument,   Person,          'The model & the document are different things');
		});
	});

	/**
	 * Get the ClientDocument class for this model
	 */
	describe('.ClientDocument', function() {
		it('should create a ClientDocument class for this specific Model', function() {

			var PersonDocument = Person.ClientDocument;

			assert.strictEqual(PersonDocument,      Person.ClientDocument, 'Should return the same constructor instance');
			assert.strictEqual(PersonDocument.name, Person.name,           'The Document class should have the same name as the Model');
			assert.notStrictEqual(PersonDocument,   Person,                'The model & the document are different things');
			assert.notStrictEqual(PersonDocument,   Person.Document,       'The ClientDocument & the Document are different things');
		});
	});

	/**
	 * Adding fields to the new Model
	 */
	describe('.addField(name, type, options)', function() {
		it('should add fields (during constitution) - Person example', function(done) {
			Person.constitute(function addFields() {

				this.addField('firstname', 'String');
				this.addField('lastname',  'String');
				this.addField('nicknames', 'String', {array: true});
				this.addField('birthdate', 'Date');
				this.addField('male',      'Boolean');

				done();
			});
		});

		it('should add fields (during constitution) - Event example', function(done) {
			Event.constitute(function addFields() {
				this.addField('name', 'String');
				this.hasAndBelongsToMany('Invited', 'Person');

				done();
			});
		});

		it('should add fields (during constitution) - Product example', function(done) {
			Product.constitute(function addFields() {
				this.addField('name', 'String');

				done();
			});
		});

		it('should be able to add schema as fields', function(next) {
			next = Function.regulate(next);

			WithSchemaField = Function.inherits('Alchemy.Model', function WithSchemaField(options) {
				WithSchemaField.super.call(this, options);
			});

			WithSchemaField.constitute(function addFields() {
				var schema = new Classes.Alchemy.Schema(this);

				schema.addField('subname', 'String');
				schema.addField('subvalue', 'String');

				this.addField('subschema', schema);

				var schema_two = new Classes.Alchemy.Schema(this);

				schema_two.addField('entryname', 'String');

				this.addField('entries', schema, {array: true});

				next();
			});
		});

		it('should be able to add translatable fields', function(next) {
			next = Function.regulate(next);

			WithTranslation = Function.inherits('Alchemy.Model', function WithTranslation(options) {
				WithTranslation.super.call(this, options);
			});

			WithTranslation.constitute(function addFields() {
				this.addField('name', 'String');
				this.addField('title', 'String', {translatable: true});

				next();
			});
		});
	});

	/**
	 * Add a belongsTo relation
	 */
	describe('.belongsTo(alias, model)', function() {
		var Group,
		    Member;

		it('adds a new relation', function(next) {

			Function.series(function(next) {

				Group = Function.inherits('Alchemy.Model', function PeopleGroup(options) {
					PeopleGroup.super.call(this, options);
				});

				Group.constitute(function addFields() {

					// Group name
					this.addField('name', 'String');

					next();
				});
			}, function(next) {

				Member = Function.inherits('Alchemy.Model', function Member(options) {
					Member.super.call(this, options);
				});

				Member.constitute(function addFields() {

					// Member name
					this.addField('name', 'String');

					// The group it belongs to
					this.belongsTo('PeopleGroup');

					next();
				});
			}, function done(err) {

				if (err) {
					return next(err);
				}

				let people_group_id = Member.getField('people_group_id');

				assert.strictEqual(people_group_id instanceof Classes.Alchemy.BelongsToFieldType, true, 'Should have made a people_group_id field');
				assert.strictEqual(people_group_id.parent_schema.name, 'Member');
				next();
			});
		});
	});

	/**
	 * Add Document methods
	 */
	describe('.setDocumentMethod(fnc)', function() {
		it('adds a new method to the linked Document class', function() {
			Person.setDocumentMethod(function getFamiliarName() {
				var result;

				if (this.firstname) {
					return this.firstname;
				} else if (this.nicknames && this.nicknames[0]) {
					return this.nicknames[0];
				} else if (this.lastname) {
					return this.lastname;
				} else {
					return 'Unknown';
				}
			});

			assert.strictEqual(Person.Document.prototype.getFamiliarName.name, 'getFamiliarName');
			assert.strictEqual(Person.ClientDocument.prototype.getFamiliarName, undefined);
		});
	});

	/**
	 * Add Document properties
	 */
	describe('.setDocumentProperty(getter)', function() {
		it('adds a new property getter to the linked Document class', function() {
			Person.setDocumentProperty(function upper_firstname() {

				if (!this.firstname) {
					return '';
				}

				return this.firstname.toUpperCase();
			});

			var person = new Person.Document();

			assert.strictEqual(person.upper_firstname, '');

			person.firstname = 'test';

			assert.strictEqual(person.upper_firstname, 'TEST');
		});
	});

	/**
	 * The name of a model
	 */
	describe('#name', function() {
		it('should return the name of the model class', function() {
			var person = Model.get('Person');
			assert.strictEqual(person.name, Person.name);
		});

		it('should allow you to override the name per instance', function() {
			var person = Model.get('Person'),
			    second = Model.get('Person');

			assert.strictEqual(person.name, 'Person');
			assert.strictEqual(second.name, person.name);

			// Now set a different name
			second.name = 'SecondPerson';

			assert.strictEqual(second.name, 'SecondPerson');
			assert.strictEqual(person.name, 'Person');
			assert.strictEqual(Model.get('Person').name, 'Person');
		});
	});

	/**
	 * Getting an instance of the model
	 */
	describe('#get(model_name)', function() {
		it('should create a new instance of the wanted model', function() {

			var person = Model.get('Person');

			assert.strictEqual(person instanceof Person, true);
			assert.strictEqual(person instanceof Classes.Alchemy.Model.Model, true);

		});
	});

	/**
	 * Saving data
	 */
	describe('#save(data, callback)', function() {

		it('should save the data and call back with a DocumentList', function(done) {

			Model.get('Person').save(data, function saved(err, list) {

				if (err) {
					return done(err);
				}

				assert.strictEqual(list.length, 1);

				let document = list[0];

				testDocument(document, data);

				// Save the _id for next tests
				_id = document._id;

				// Save this for later tests
				global.person_doc = document;

				done();
			});
		});
	});

	/**
	 * Getting data
	 */
	describe('#find(\'first\', options, callback)', function() {
		it('should find 1 document by ObjectId instance', function(done) {
			Model.get('Person').find('first', {conditions: {_id: _id}}, function gotDocument(err, document) {

				if (err) {
					return done(err);
				}

				try {

					assert.notStrictEqual(document instanceof Classes.Alchemy.DocumentList, true, 'Should not have returned a DocumentList');
					assert.strictEqual(document instanceof Classes.Alchemy.Document.Document, true, 'Should have returned a Document');

					assert.strictEqual(String(document._id), String(_id));

					testDocument(document, data);
				} catch (err) {
					return done(err);
				}

				done();
			});
		});

		it('should find 1 document by ObjectId string', function(done) {
			Model.get('Person').find('first', {conditions: {_id: String(_id)}}, function gotDocument(err, document) {

				if (err) {
					return done(err);
				}

				try {
					assert.notStrictEqual(document instanceof Classes.Alchemy.DocumentList, true, 'Should not have returned a DocumentList');
					assert.strictEqual(document instanceof Classes.Alchemy.Document.Document, true, 'Should have returned a Document');

					assert.strictEqual(String(document._id), String(_id));

					testDocument(document, data);
				} catch (err) {
					return done(err);
				}

				done();
			});
		});
	});

	describe('#findById(object_id, callback)', function() {
		it('should find a single document by ObjectId instance', function(done) {
			Model.get('Person').findById(_id, function gotPerson(err, person) {

				if (err) {
					return done(err);
				}

				assert.strictEqual(String(_id), String(person._id));
				assert.strictEqual(person instanceof Classes.Alchemy.Document.Document, true);
				done();
			});
		});

		it('should find a single document by ObjectId string', function(done) {
			Model.get('Person').findById(String(_id), function gotPerson(err, person) {

				if (err) {
					return done(err);
				}

				assert.strictEqual(String(_id), String(person._id));
				assert.strictEqual(person instanceof Classes.Alchemy.Document.Document, true);
				done();
			});
		});
	});

	describe('#ensureIds(list, callback)', function() {

		var product,
		    list;

		list = [
			{
				_id   : '52efff000073570002000000',
				name  : 'screen'
			},
			{
				_id   : '52efff000073570002000001',
				name  : 'mouse'
			},
			{
				_id   : '52efff000073570002000002',
				name  : 'keyboard'
			}
		];

		it('should make sure the given ids & records exist in the database', async function() {

			product = Model.get('Product');

			await product.ensureIds(list, async function done(err) {

				if (err) {
					return done(err);
				}

				let prod;

				prod = await product.findById('52efff000073570002000000');
				assert.strictEqual(prod.name, 'screen');

				prod = await product.findById('52efff000073570002000001');
				assert.strictEqual(prod.name, 'mouse');

				prod = await product.findById('52efff000073570002000002');
				assert.strictEqual(prod.name, 'keyboard');

				let prods = await product.find('all');
				assert.strictEqual(prods.length, 3);

				let removed = await prod.remove();

				assert.strictEqual(removed, true);

				prod = await product.findById('52efff000073570002000002');
				assert.strictEqual(prod, null);

				prods = await product.find('all');
				assert.strictEqual(prods.length, 2, 'There should only be 3 products after one was removed');

				// Ensure them again
				await product.ensureIds(list);

				prods = await product.find('all');
				assert.strictEqual(prods.length, 3, 'There should only be 3 products: only the missing one should have been added');

				prod = await product.findById('52efff000073570002000002');
				assert.strictEqual(prod.name, 'keyboard');
			});
		});
	});
});<|endoftext|>


<commit_before>import React from 'react';
import ReactTestUtils from 'react-addons-test-utils';
import ReactDOM from 'react-dom';

import Form from '../src/Form';

function getFieldsModel() {
  return [
    /*  0 */{type: 'string', id: 'id', 'label': '主键', hidden: true},
    /*  1 */{type: 'string', id: 'danjubianhao', label: '单据编号'},
    /*  2 */{type: 'string', id: 'name2', label: '名称2', hidden: true},
    /*  3 */{type: 'string', id: 'name3', label: '名称3', hidden: true},
    /*  4 */{type: 'string', id: 'name4', label: '名称4', hidden: true},
    /*  5 */{type: 'enum', id: 'danjuleixing', label: '单据类型', placeholder: '请选择单据类型',
               data: [
                 {key: '2631', value: '差旅费借款单'},
                 {key: '2632', value: '会议费借款单'},
                 {key: 'D3', value: '付款单'}
               ]
             },
    /*  6 */{type: 'string', id: 'danjuzhuangtai', label: '单据状态'},
    /*  7 */{type: 'double', id: 'jine', label: '金额'},
    /*  8 */{type: 'date', id: 'danjuriqi', label: '单据日期'},
    /*  9 */{type: 'boolean', id: 'qiyong', label: '启用'},
    /* 10 */{type: 'ref', id: 'zuzhi', label: '组织',
              referConfig: {
                referConditions: {
                  refCode: 'dept',
                  refType: 'tree',
                  rootName: '部门'
                },
                referDataUrl: 'http://10.3.14.239/ficloud/refbase_ctr/queryRefJSON'
              }
            },
    /* 11 */{type: 'ref', id: 'zhuguan', label: '主管',
              referConfig: {
                referConditions: {
                  refCode: 'dept',
                  refType: 'tree',
                  rootName: '部门'
                },
                referDataUrl: 'http://10.3.14.239/ficloud/refbase_ctr/queryRefJSON'
              }
            },
    /* 12 */{type: 'ref', id: 'shangjibumen', label: '上级部门',
              referConfig: {
                referConditions: {
                  refCode: 'dept',
                  refType: 'tree',
                  rootName: '部门'
                },
                referDataUrl: 'http://10.3.14.239/ficloud/refbase_ctr/queryRefJSON'
              }
            },
    /* 13 */{type: 'ref', id: 'shangjibumen2', label: '上级部门2' }
  ];
}

function getDefaultFormData() {
  return {
    id: '22EA0EB9-FABA-4224-B290-4D041A1DF773',
    danjubianhao: '123',
    name2: '名称2',
    name3: '名称3',
    name4: '名称4',
    danjuleixing: 'D3',
    danjuzhuangtai: '保存',
    jine: '12.00',
    danjuriqi: new Date('2017-02-14').toISOString(),
    qiyong: false,
    zuzhi: {
      id: '02EDD0F9-F384-43BF-9398-5E5781DAC5D0',
      code: '0502',
      name: '二车间',
    },
    zhuguan: null,
    shangjibumen: {
      id: '',
      code: '',
      name: ''
    },
    shangjibumen2: null
  };
}

describe('<Form>', () => {

  // helper function

  function getForm(instance) {
    return ReactDOM.findDOMNode(instance);
  }

  function getInput(instance, index) {
    const node = ReactDOM.findDOMNode(instance);
    return node.querySelectorAll('input')[index];
  }

  function getSubmitButton(component) {
    let formNode = getForm(component);
    return formNode.querySelector('button[type=submit]');
  }

  /**
   * 只运行一个测试
   * it.only('', () => {});
   */

  it('应该正常显示，当传入空数组和空对象的时候', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={[]}
        defaultData={{}}
      />
    );
    // 应该只有一个form-group，这个form-group包含了“取消”和“保存”两个按钮
    assert.equal(getForm(instance).querySelectorAll('.form-group').length, 1);
  });

  it('应该正常显示，当fieldsModel有定义，但是defaultData为空的时候', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={{}}
      />
    );
    // 一个14个字段定义，4个隐藏，10个可显示字段，还有一个
    // form-group，这个form-group包含了“取消”和“保存”两个按钮
    assert.equal(getForm(instance).querySelectorAll('.form-group').length, 11);
  });

  it('uses "form" by default', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={getDefaultFormData()}
      />
    );

    assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'FORM');
  });

  it('has "admin-form" class', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={getDefaultFormData()}
        className="admin-form"
      />
    );
    // 默认的form是左右布局，左侧是label，右侧是input
    assert.equal(ReactDOM.findDOMNode(instance).className, 'admin-form form-horizontal');
  });

  it('Should merge additional classes passed in', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={getDefaultFormData()}
      className="bob"/>
    );
    assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bbob\b/));
  });

  it('应该按照fieldModel和defaultData正确显示出所有form元素，并且不显示隐藏元素', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={getDefaultFormData()}
      />
    );

    // input的值应该和输入的默认值相同。
    assert.equal(getInput(instance, 0).value, getDefaultFormData().danjubianhao);
    assert.equal(getInput(instance, 2).value, getDefaultFormData().jine);

    // form.querySelectorAll('.form-group')[1].querySelectorAll('option').forEach(opt => {
    //   if (opt.value === 'D3') {
    //     assert.equal(opt.selected, true);
    //   }
    // });
  });

  it('应该将下拉菜单正确设置为输入的默认值', () => {
    let component = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={[
          {type: 'enum', id: 'danjuleixing', label: '单据类型',
            data: [
              {key: '2631', value: '差旅费借款单'},
              {key: '2632', value: '会议费借款单'},
              {key: 'D3', value: '付款单'}
            ]
          }
        ]}
        defaultData={{
          danjuleixing: 'D3'
        }}
      />
    );
    assert.equal(component.state.formData.danjuleixing, 'D3');

    // select（下拉框）的值应该和输入的默认值相同
    let form = getForm(component);
    let formGroups = form.querySelectorAll('.form-group');
    let options = formGroups[0].querySelectorAll('option');
    console.log(options)
    options.forEach(opt => {
      if (opt.value === 'D3') {
        assert.equal(opt.selected, true);
      } else {
        assert.equal(opt.selected, false);
      }
    });
  });

  it('Should change input value', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={getDefaultFormData()}
      />
    );

    // 修改表单中第一个文本框“单据编号”中的值
    const node = ReactDOM.findDOMNode(instance);
    // const firstInputNode = node.querySelector('input');
    const firstInputNode = node.querySelectorAll('input')[0];
    firstInputNode.value = 'test';
    ReactTestUtils.Simulate.change(firstInputNode);
    assert.equal(firstInputNode.value, 'test');
    assert.equal(instance.state.formData.danjubianhao, 'test');
  });

  it('表单中必选项初始内容为空，直接点击提交按钮，应该主动验证', () => {
    let component = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={[
          {type: 'string', id: 'name0', label: '名称0',
            validators: [
              {type: 'required'}
            ]
          },
          {type: 'string', id: 'name1', label: '名称1',
            validators: [
              {type: 'required'}
            ]
          }
        ]}
        defaultData={{name0: '', name1: ''}}
      />
    );

    // 获取DOM element
    let text0 = getInput(component, 0);
    let text1 = getInput(component, 1);
    let submitButton = getSubmitButton(component);

    // 初始验证状态应该是未知
    assert.strictEqual(component.state.fieldsValidationState.name0, null);
    assert.strictEqual(component.state.fieldsValidationState.name1, null);

    // 初始的时候，保存按钮状态不应该是disabled
    assert.strictEqual(submitButton.disabled, false);
    assert.strictEqual(component.state.submitButtonDisabled, false,
      '按钮状态应该是启用的');

    ReactTestUtils.Simulate.click(submitButton);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'error');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'error');
    assert.strictEqual(submitButton.disabled, true,
      '模拟鼠标点击提交按钮，提交按钮状态应该变成disabled');

    // 在表单中第一个文本框中输入内容，第一个文本框应该验证成功
    text0.value = 'test';
    ReactTestUtils.Simulate.change(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'success');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'error');
    assert.strictEqual(submitButton.disabled, true);
    assert.strictEqual(component.state.submitButtonDisabled, true,
      '按钮状态应该是禁用的');

    // 在表单中第二个文本框中输入内容，第二个文本框应该验证成功
    text1 = getInput(component, 1);
    text1.value = 'test';
    ReactTestUtils.Simulate.change(text1);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'success');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'success');
    assert.strictEqual(component.state.submitButtonDisabled, false,
      '按钮状态应该是启用的');
    assert.strictEqual(submitButton.disabled, false,
      '在表单中第二个文本框输入内容，提交按钮不应该是禁用状态');

    // 清空表单中第一个文本框中的内容，第一个文本框应该验证失败
    text0 = getInput(component, 0);
    text0.value = '';
    ReactTestUtils.Simulate.change(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'error');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'success');
    assert.strictEqual(submitButton.disabled, true,
      '提交按钮的状态应该是禁用');

  });

  it('表单中必选项初始内容为空，然后开始修改', () => {
    let component = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={[
          {type: 'string', id: 'name0', label: '名称0',
            validators: [
              {type: 'required'}
            ]
          },
          {type: 'string', id: 'name1', label: '名称1',
            validators: [
              {type: 'required'}
            ]
          }
        ]}
        defaultData={{name0: '', name1: ''}}
      />
    );
    // 获取DOM element
    let text0 = getInput(component, 0);
    let text1 = getInput(component, 1);
    let submitButton = getSubmitButton(component);

    // 初始验证状态应该是未知
    assert.strictEqual(component.state.fieldsValidationState.name0, null);
    assert.strictEqual(component.state.fieldsValidationState.name1, null);

    // 初始的时候，保存按钮状态不应该是disabled
    assert.strictEqual(submitButton.disabled, false);
    assert.strictEqual(component.state.submitButtonDisabled, false,
      '按钮状态应该是启用的');

    ReactTestUtils.Simulate.focus(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, null);
    assert.strictEqual(component.state.fieldsValidationState.name1, null);
    assert.strictEqual(submitButton.disabled, false,
      '当focus在第一个文本框，提交按钮状态不应该disabled');

    // 在表单中第一个文本框中输入内容，第一个文本框应该验证成功
    text0.value = 'test';
    ReactTestUtils.Simulate.change(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'success');
    assert.strictEqual(component.state.fieldsValidationState.name1, null);
    assert.strictEqual(submitButton.disabled, false);
    assert.strictEqual(component.state.submitButtonDisabled, false,
      '按钮状态应该是禁用的');

    // 在表单中第二个文本框中输入内容，第二个文本框应该验证成功
    text1 = getInput(component, 1);
    text1.value = 'test';
    ReactTestUtils.Simulate.change(text1);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'success');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'success');
    assert.strictEqual(component.state.submitButtonDisabled, false,
      '按钮状态应该是启用的'); // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    assert.strictEqual(submitButton.disabled, false,
      '在表单中第二个文本框输入内容，提交按钮不应该是禁用状态');

    // 清空表单中第一个文本框中的内容，第一个文本框应该验证失败
    text0 = getInput(component, 0);
    text0.value = '';
    ReactTestUtils.Simulate.change(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'error');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'success');
    assert.strictEqual(submitButton.disabled, true,
      '提交按钮的状态应该是禁用');

  });

  it('表单中必填项初始有内容，后被清空，应该验证失败', () => {
    let text0;
    let component = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={[
          {type: 'string', id: 'name0', label: '名称0',
            validators: [
              {type: 'required'}
            ]
          },
          {type: 'string', id: 'name1', label: '名称1',
            validators: [
              {type: 'required'}
            ]
          }
        ]}
        defaultData={{name0: 'test0', name1: 'test1'}}
      />
    );
    let submitButton = getSubmitButton(component);

    // 初始验证状态应该是验证状态未知
    assert.strictEqual(component.state.fieldsValidationState.name0, null);
    assert.strictEqual(component.state.fieldsValidationState.name1, null);
    assert.strictEqual(submitButton.disabled, false,
      '初始状态提交按钮应该未被禁用');

    // 清空表单中第一个文本框中的内容，第一个文本框应该验证失败
    text0 = getInput(component, 0);
    text0.value = '';
    ReactTestUtils.Simulate.change(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'error');
    assert.strictEqual(component.state.fieldsValidationState.name1, null);
    assert.strictEqual(submitButton.disabled, true,
      '提交按钮的状态应该是禁用');
  });

});
<commit_msg>debug<commit_after>import React from 'react';
import ReactTestUtils from 'react-addons-test-utils';
import ReactDOM from 'react-dom';

import Form from '../src/Form';

function getFieldsModel() {
  return [
    /*  0 */{type: 'string', id: 'id', 'label': '主键', hidden: true},
    /*  1 */{type: 'string', id: 'danjubianhao', label: '单据编号'},
    /*  2 */{type: 'string', id: 'name2', label: '名称2', hidden: true},
    /*  3 */{type: 'string', id: 'name3', label: '名称3', hidden: true},
    /*  4 */{type: 'string', id: 'name4', label: '名称4', hidden: true},
    /*  5 */{type: 'enum', id: 'danjuleixing', label: '单据类型', placeholder: '请选择单据类型',
               data: [
                 {key: '2631', value: '差旅费借款单'},
                 {key: '2632', value: '会议费借款单'},
                 {key: 'D3', value: '付款单'}
               ]
             },
    /*  6 */{type: 'string', id: 'danjuzhuangtai', label: '单据状态'},
    /*  7 */{type: 'double', id: 'jine', label: '金额'},
    /*  8 */{type: 'date', id: 'danjuriqi', label: '单据日期'},
    /*  9 */{type: 'boolean', id: 'qiyong', label: '启用'},
    /* 10 */{type: 'ref', id: 'zuzhi', label: '组织',
              referConfig: {
                referConditions: {
                  refCode: 'dept',
                  refType: 'tree',
                  rootName: '部门'
                },
                referDataUrl: 'http://10.3.14.239/ficloud/refbase_ctr/queryRefJSON'
              }
            },
    /* 11 */{type: 'ref', id: 'zhuguan', label: '主管',
              referConfig: {
                referConditions: {
                  refCode: 'dept',
                  refType: 'tree',
                  rootName: '部门'
                },
                referDataUrl: 'http://10.3.14.239/ficloud/refbase_ctr/queryRefJSON'
              }
            },
    /* 12 */{type: 'ref', id: 'shangjibumen', label: '上级部门',
              referConfig: {
                referConditions: {
                  refCode: 'dept',
                  refType: 'tree',
                  rootName: '部门'
                },
                referDataUrl: 'http://10.3.14.239/ficloud/refbase_ctr/queryRefJSON'
              }
            },
    /* 13 */{type: 'ref', id: 'shangjibumen2', label: '上级部门2' }
  ];
}

function getDefaultFormData() {
  return {
    id: '22EA0EB9-FABA-4224-B290-4D041A1DF773',
    danjubianhao: '123',
    name2: '名称2',
    name3: '名称3',
    name4: '名称4',
    danjuleixing: 'D3',
    danjuzhuangtai: '保存',
    jine: '12.00',
    danjuriqi: new Date('2017-02-14').toISOString(),
    qiyong: false,
    zuzhi: {
      id: '02EDD0F9-F384-43BF-9398-5E5781DAC5D0',
      code: '0502',
      name: '二车间',
    },
    zhuguan: null,
    shangjibumen: {
      id: '',
      code: '',
      name: ''
    },
    shangjibumen2: null
  };
}

describe('<Form>', () => {

  // helper function

  function getForm(instance) {
    return ReactDOM.findDOMNode(instance);
  }

  function getInput(instance, index) {
    const node = ReactDOM.findDOMNode(instance);
    return node.querySelectorAll('input')[index];
  }

  function getSubmitButton(component) {
    let formNode = getForm(component);
    return formNode.querySelector('button[type=submit]');
  }

  /**
   * 只运行一个测试
   * it.only('', () => {});
   */

  it('应该正常显示，当传入空数组和空对象的时候', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={[]}
        defaultData={{}}
      />
    );
    // 应该只有一个form-group，这个form-group包含了“取消”和“保存”两个按钮
    assert.equal(getForm(instance).querySelectorAll('.form-group').length, 1);
  });

  it('应该正常显示，当fieldsModel有定义，但是defaultData为空的时候', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={{}}
      />
    );
    // 一个14个字段定义，4个隐藏，10个可显示字段，还有一个
    // form-group，这个form-group包含了“取消”和“保存”两个按钮
    assert.equal(getForm(instance).querySelectorAll('.form-group').length, 11);
  });

  it('uses "form" by default', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={getDefaultFormData()}
      />
    );

    assert.equal(ReactDOM.findDOMNode(instance).nodeName, 'FORM');
  });

  it('has "admin-form" class', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={getDefaultFormData()}
        className="admin-form"
      />
    );
    // 默认的form是左右布局，左侧是label，右侧是input
    assert.equal(ReactDOM.findDOMNode(instance).className, 'admin-form form-horizontal');
  });

  it('Should merge additional classes passed in', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={getDefaultFormData()}
      className="bob"/>
    );
    assert.ok(ReactDOM.findDOMNode(instance).className.match(/\bbob\b/));
  });

  it('应该按照fieldModel和defaultData正确显示出所有form元素，并且不显示隐藏元素', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={getDefaultFormData()}
      />
    );

    // input的值应该和输入的默认值相同。
    assert.equal(getInput(instance, 0).value, getDefaultFormData().danjubianhao);
    assert.equal(getInput(instance, 2).value, getDefaultFormData().jine);

    // form.querySelectorAll('.form-group')[1].querySelectorAll('option').forEach(opt => {
    //   if (opt.value === 'D3') {
    //     assert.equal(opt.selected, true);
    //   }
    // });
  });

  it('应该将下拉菜单正确设置为输入的默认值', () => {
    let component = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={[
          {type: 'enum', id: 'danjuleixing', label: '单据类型',
            data: [
              {key: '2631', value: '差旅费借款单'},
              {key: '2632', value: '会议费借款单'},
              {key: 'D3', value: '付款单'}
            ]
          }
        ]}
        defaultData={{
          danjuleixing: 'D3'
        }}
      />
    );
    assert.equal(component.state.formData.danjuleixing, 'D3');

    // select（下拉框）的值应该和输入的默认值相同
    let form = getForm(component);
    let formGroups = form.querySelectorAll('.form-group');
    let options = formGroups[0].querySelectorAll('option');
    console.log(options);
    options.forEach(opt => {
      if (opt.value === 'D3') {
        assert.equal(opt.selected, true);
      } else {
        assert.equal(opt.selected, false);
      }
    });
  });

  it('Should change input value', () => {
    let instance = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={getFieldsModel()}
        defaultData={getDefaultFormData()}
      />
    );

    // 修改表单中第一个文本框“单据编号”中的值
    const node = ReactDOM.findDOMNode(instance);
    // const firstInputNode = node.querySelector('input');
    const firstInputNode = node.querySelectorAll('input')[0];
    firstInputNode.value = 'test';
    ReactTestUtils.Simulate.change(firstInputNode);
    assert.equal(firstInputNode.value, 'test');
    assert.equal(instance.state.formData.danjubianhao, 'test');
  });

  it('表单中必选项初始内容为空，直接点击提交按钮，应该主动验证', () => {
    let component = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={[
          {type: 'string', id: 'name0', label: '名称0',
            validators: [
              {type: 'required'}
            ]
          },
          {type: 'string', id: 'name1', label: '名称1',
            validators: [
              {type: 'required'}
            ]
          }
        ]}
        defaultData={{name0: '', name1: ''}}
      />
    );

    // 获取DOM element
    let text0 = getInput(component, 0);
    let text1 = getInput(component, 1);
    let submitButton = getSubmitButton(component);

    // 初始验证状态应该是未知
    assert.strictEqual(component.state.fieldsValidationState.name0, null);
    assert.strictEqual(component.state.fieldsValidationState.name1, null);

    // 初始的时候，保存按钮状态不应该是disabled
    assert.strictEqual(submitButton.disabled, false);
    assert.strictEqual(component.state.submitButtonDisabled, false,
      '按钮状态应该是启用的');

    ReactTestUtils.Simulate.click(submitButton);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'error');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'error');
    assert.strictEqual(submitButton.disabled, true,
      '模拟鼠标点击提交按钮，提交按钮状态应该变成disabled');

    // 在表单中第一个文本框中输入内容，第一个文本框应该验证成功
    text0.value = 'test';
    ReactTestUtils.Simulate.change(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'success');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'error');
    assert.strictEqual(submitButton.disabled, true);
    assert.strictEqual(component.state.submitButtonDisabled, true,
      '按钮状态应该是禁用的');

    // 在表单中第二个文本框中输入内容，第二个文本框应该验证成功
    text1 = getInput(component, 1);
    text1.value = 'test';
    ReactTestUtils.Simulate.change(text1);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'success');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'success');
    assert.strictEqual(component.state.submitButtonDisabled, false,
      '按钮状态应该是启用的');
    assert.strictEqual(submitButton.disabled, false,
      '在表单中第二个文本框输入内容，提交按钮不应该是禁用状态');

    // 清空表单中第一个文本框中的内容，第一个文本框应该验证失败
    text0 = getInput(component, 0);
    text0.value = '';
    ReactTestUtils.Simulate.change(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'error');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'success');
    assert.strictEqual(submitButton.disabled, true,
      '提交按钮的状态应该是禁用');

  });

  it('表单中必选项初始内容为空，然后开始修改', () => {
    let component = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={[
          {type: 'string', id: 'name0', label: '名称0',
            validators: [
              {type: 'required'}
            ]
          },
          {type: 'string', id: 'name1', label: '名称1',
            validators: [
              {type: 'required'}
            ]
          }
        ]}
        defaultData={{name0: '', name1: ''}}
      />
    );
    // 获取DOM element
    let text0 = getInput(component, 0);
    let text1 = getInput(component, 1);
    let submitButton = getSubmitButton(component);

    // 初始验证状态应该是未知
    assert.strictEqual(component.state.fieldsValidationState.name0, null);
    assert.strictEqual(component.state.fieldsValidationState.name1, null);

    // 初始的时候，保存按钮状态不应该是disabled
    assert.strictEqual(submitButton.disabled, false);
    assert.strictEqual(component.state.submitButtonDisabled, false,
      '按钮状态应该是启用的');

    ReactTestUtils.Simulate.focus(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, null);
    assert.strictEqual(component.state.fieldsValidationState.name1, null);
    assert.strictEqual(submitButton.disabled, false,
      '当focus在第一个文本框，提交按钮状态不应该disabled');

    // 在表单中第一个文本框中输入内容，第一个文本框应该验证成功
    text0.value = 'test';
    ReactTestUtils.Simulate.change(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'success');
    assert.strictEqual(component.state.fieldsValidationState.name1, null);
    assert.strictEqual(submitButton.disabled, false);
    assert.strictEqual(component.state.submitButtonDisabled, false,
      '按钮状态应该是禁用的');

    // 在表单中第二个文本框中输入内容，第二个文本框应该验证成功
    text1 = getInput(component, 1);
    text1.value = 'test';
    ReactTestUtils.Simulate.change(text1);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'success');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'success');
    assert.strictEqual(component.state.submitButtonDisabled, false,
      '按钮状态应该是启用的'); // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    assert.strictEqual(submitButton.disabled, false,
      '在表单中第二个文本框输入内容，提交按钮不应该是禁用状态');

    // 清空表单中第一个文本框中的内容，第一个文本框应该验证失败
    text0 = getInput(component, 0);
    text0.value = '';
    ReactTestUtils.Simulate.change(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'error');
    assert.strictEqual(component.state.fieldsValidationState.name1, 'success');
    assert.strictEqual(submitButton.disabled, true,
      '提交按钮的状态应该是禁用');

  });

  it('表单中必填项初始有内容，后被清空，应该验证失败', () => {
    let text0;
    let component = ReactTestUtils.renderIntoDocument(
      <Form
        fieldsModel={[
          {type: 'string', id: 'name0', label: '名称0',
            validators: [
              {type: 'required'}
            ]
          },
          {type: 'string', id: 'name1', label: '名称1',
            validators: [
              {type: 'required'}
            ]
          }
        ]}
        defaultData={{name0: 'test0', name1: 'test1'}}
      />
    );
    let submitButton = getSubmitButton(component);

    // 初始验证状态应该是验证状态未知
    assert.strictEqual(component.state.fieldsValidationState.name0, null);
    assert.strictEqual(component.state.fieldsValidationState.name1, null);
    assert.strictEqual(submitButton.disabled, false,
      '初始状态提交按钮应该未被禁用');

    // 清空表单中第一个文本框中的内容，第一个文本框应该验证失败
    text0 = getInput(component, 0);
    text0.value = '';
    ReactTestUtils.Simulate.change(text0);
    assert.strictEqual(component.state.fieldsValidationState.name0, 'error');
    assert.strictEqual(component.state.fieldsValidationState.name1, null);
    assert.strictEqual(submitButton.disabled, true,
      '提交按钮的状态应该是禁用');
  });

});
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

define(function (require, exports) {
    "use strict";

    var Promise = require("bluebird"),
        Immutable = require("immutable");

    var os = require("adapter/os");

    var events = require("../events"),
        locks = require("../locks"),
        layers = require("js/actions/layers"),
        menu = require("./menu"),
        collection = require("js/util/collection"),
        headlights = require("js/util/headlights"),
        history = require("js/actions/history"),
        policyActions = require("js/actions/policy"),
        dom = require("js/util/dom");

    /**
     * Native menu command IDs for Photoshop edit commands.
     *
     * @const
     * @private
     * @type {number}
     */
    var CUT_NATIVE_MENU_COMMMAND_ID = 103,
        COPY_NATIVE_MENU_COMMMAND_ID = 104,
        PASTE_NATIVE_MENU_COMMMAND_ID = 105,
        SELECT_ALL_NATIVE_MENU_COMMMAND_ID = 1017;

    var LAYER_CLIPBOARD_FORMAT = "com.adobe.photoshop.spaces.design.layers";

    /**
     * Execute a native cut command.
     *
     * @private
     * @return {Promise}
     */
    var nativeCut = function () {
        return this.transfer(menu.nativeModal, {
            commandID: CUT_NATIVE_MENU_COMMMAND_ID
        })
        .catch(function () {
            // Ignore errors from menu.nativeModal
        });
    };
    nativeCut.modal = true;
    nativeCut.reads = [];
    nativeCut.writes = [];
    nativeCut.transfers = [menu.nativeModal];

    /**
     * Execute a native copy command.
     *
     * @private
     * @return {Promise}
     */
    var nativeCopy = function () {
        return this.transfer(menu.nativeModal, {
            commandID: COPY_NATIVE_MENU_COMMMAND_ID
        })
        .catch(function () {
            // Ignore errors from menu.nativeModal
        });
    };
    nativeCopy.modal = true;
    nativeCopy.reads = [];
    nativeCopy.writes = [];
    nativeCopy.transfers = [menu.nativeModal];

    /**
     * Execute a native paste command.
     *
     * @private
     * @return {Promise}
     */
    var nativePaste = function () {
        // FIXME: the suspend/restore policies hack is for pasting smart object from other sources (e.g. Illustrator).
        // The cause is similar to the place-object menu commands: DS is not receiving "toolModalStateChanged" events
        // until the object is committed.
        var policyStore = this.flux.store("policy"),
            suspendPromise;

        if (policyStore.areAllSuspended()) {
            suspendPromise = Promise.resolve();
        } else {
            suspendPromise = this.transfer(policyActions.suspendAllPolicies);
        }

        return suspendPromise
            .bind(this)
            .then(function () {
                return this.transfer(menu.nativeModal, {
                    commandID: PASTE_NATIVE_MENU_COMMMAND_ID
                });
            })
            .finally(function () {
                if (this.flux.store("policy").areAllSuspended()) {
                    return this.transfer(policyActions.restoreAllPolicies);
                }
            });
    };
    nativePaste.modal = true;
    nativePaste.reads = [];
    nativePaste.writes = [];
    nativePaste.transfers = [menu.nativeModal, policyActions.suspendAllPolicies, policyActions.restoreAllPolicies];

    /**
     * Execute a native selectAll command.
     *
     * @private
     * @param {boolean} waitForCompletion Flag for nativeModal
     * @return {Promise}
     */
    var nativeSelectAll = function (waitForCompletion) {
        waitForCompletion = waitForCompletion || false;

        return this.transfer(menu.nativeModal, {
            commandID: SELECT_ALL_NATIVE_MENU_COMMMAND_ID,
            waitForCompletion: waitForCompletion
        })
        .catch(function () {
            // Ignore errors from menu.nativeModal
        });
    };
    nativeSelectAll.modal = true;
    nativeSelectAll.reads = [];
    nativeSelectAll.writes = [];
    nativeSelectAll.transfers = [menu.nativeModal];

    /**
     * Execute either a cut or copy operation, depending on the value of the parameter.
     *
     * @private
     * @param {boolean} cut If true, perform a cut operation; otherwise, a copy.
     * @return {Promise}
     */
    var _cutOrCopy = function (cut) {
        return os.hasKeyboardFocus()
            .bind(this)
            .then(function (cefHasFocus) {
                var el = window.document.activeElement,
                    data;

                if (cefHasFocus && dom.isInput(el)) {
                    if (dom.isTextInput(el)) {
                        data = el.value.substring(el.selectionStart, el.selectionEnd);
                        if (cut) {
                            el.setRangeText("");
                        }
                    } else {
                        data = el.value;
                    }
                } else {
                    // Even if CEF doesn't have focus, a disabled input could have a selection
                    var selection = window.document.getSelection();
                    if (selection.type === "Range") {
                        data = selection.toString();
                    }
                }

                if (typeof data === "string") {
                    var cutCopyEvent = new window.Event(cut ? "cut" : "copy", { bubbles: true });
                    el.dispatchEvent(cutCopyEvent);

                    return os.clipboardWrite(data);
                }

                // If we're on modal state (type edit), we should go with native copy/cut
                if (this.flux.store("tool").getModalToolState()) {
                    if (cut) {
                        return this.transfer(nativeCut);
                    } else {
                        return this.transfer(nativeCopy);
                    }
                } else if (!cut) {
                    var applicationStore = this.flux.store("application"),
                        document = applicationStore.getCurrentDocument();

                    if (!document || document.unsupported) {
                        return;
                    }

                    var layerIDs = collection.pluck(document.layers.selectedNormalized, "id"),
                        payload = {
                            document: document.id,
                            layers: layerIDs
                        },
                        rawPayload = JSON.stringify(payload);

                    headlights.logEvent("edit", "layers", "copy-layers");
                    return os.clipboardWrite(rawPayload, LAYER_CLIPBOARD_FORMAT);
                }
            });
    };

    /**
     * Execute a cut operation on the currently active HTML element.
     *
     * @private
     * @return {Promise}
     */
    var cut = function () {
        return _cutOrCopy.call(this, true);
    };
    cut.modal = true;
    cut.reads = [locks.JS_TOOL, locks.PS_APP];
    cut.writes = [locks.JS_DOC, locks.PS_DOC, locks.OS_CLIPBOARD];
    cut.transfers = [nativeCut];

    /**
     * Execute a copy operation on the currently active HTML element.
     *
     * @private
     * @return {Promise}
     */
    var copy = function () {
        return _cutOrCopy.call(this, false);
    };
    copy.modal = true;
    copy.reads = [locks.JS_DOC, locks.JS_TOOL, locks.PS_APP];
    copy.writes = [locks.OS_CLIPBOARD];
    copy.transfers = [nativeCopy];

    /**
     * Execute a paste operation on the currently active HTML element.
     *
     * @private
     * @return {Promise}
     */
    var paste = function () {
        return os.hasKeyboardFocus()
            .bind(this)
            .then(function (cefHasFocus) {
                var el = window.document.activeElement;
                if (cefHasFocus && dom.isInput(el)) {
                    return os.clipboardRead()
                        .then(function (result) {
                            var data = result.data,
                                format = result.format;

                            if (format !== "string") {
                                return;
                            }

                            if (dom.isTextInput(el)) {
                                var selectionStart = el.selectionStart;
                                el.setRangeText(data);
                                el.setSelectionRange(selectionStart + data.length, selectionStart + data.length);
                            } else {
                                el.value = data;
                            }

                            var pasteEvent = new window.Event("paste", { bubbles: true });
                            el.dispatchEvent(pasteEvent);
                        });
                } else {
                    return os.clipboardRead([LAYER_CLIPBOARD_FORMAT])
                        .bind(this)
                        .then(function (result) {
                            var format = result.format;
                            if (format !== LAYER_CLIPBOARD_FORMAT) {
                                return this.transfer(nativePaste);
                            }

                            var applicationStore = this.flux.store("application"),
                                document = applicationStore.getCurrentDocument();

                            if (!document || document.unsupported) {
                                return;
                            }

                            var data = result.data,
                                payload = JSON.parse(data),
                                documentID = payload.document,
                                documentStore = this.flux.store("document"),
                                fromDocument = documentStore.getDocument(documentID);

                            if (!fromDocument || fromDocument.unsupported) {
                                return;
                            }

                            var layerIDs = payload.layers,
                                fromLayers = Immutable.List(layerIDs.reduce(function (layers, layerID) {
                                    var layer = fromDocument.layers.byID(layerID);
                                    if (layer) {
                                        layers.push(layer);
                                    }
                                    return layers;
                                }, []));

                            headlights.logEvent("edit", "layers", "paste-layers");
                            return this.transfer(layers.duplicate, document, fromDocument, fromLayers);
                        });
                }
            });
    };
    paste.modal = true;
    paste.reads = [locks.JS_DOC, locks.JS_APP, locks.OS_CLIPBOARD, locks.PS_APP];
    paste.writes = [];
    paste.transfers = [layers.duplicate, nativePaste];

    /**
     * Execute a select operation on the currently active HTML element.
     *
     * @private
     * @return {Promise}
     */
    var selectAll = function () {
        return os.hasKeyboardFocus()
            .bind(this)
            .then(function (cefHasFocus) {
                var el = window.document.activeElement;
                if (cefHasFocus && dom.isInput(el)) {
                    if (dom.isTextInput(el)) {
                        el.setSelectionRange(0, el.value.length);
                    }
                } else {
                    var toolStore = this.flux.store("tool");
                    if (toolStore.getModalToolState()) {
                        return this.transfer(nativeSelectAll);
                    } else {
                        return this.transfer(layers.selectAll);
                    }
                }
            });
    };
    selectAll.modal = true;
    selectAll.reads = [locks.JS_TOOL, locks.PS_APP];
    selectAll.writes = [];
    selectAll.transfers = [layers.selectAll, nativeSelectAll];

    /**
     * Step Backwards by transferring to the appropriate history action
     *
     * @private
     * @return {Promise}
     */
    var undo = function () {
        var currentDocument = this.flux.store("application").getCurrentDocument();
        if (!currentDocument) {
            return Promise.resolve();
        } else {
            return Promise.join(
                this.dispatchAsync(events.ui.TOGGLE_OVERLAYS, { enabled: false }),
                this.transfer(history.decrementHistory, currentDocument.id),
                function () {
                    return this.dispatchAsync(events.ui.TOGGLE_OVERLAYS, { enabled: true });
                }.bind(this));
        }
    };
    undo.reads = [locks.JS_APP, locks.JS_DOC];
    undo.writes = [locks.JS_UI];
    undo.transfers = [history.decrementHistory];
    undo.modal = true;

    /**
     * Step Forward by transferring to the appropriate history action
     *
     * @private
     * @return {Promise}
     */
    var redo = function () {
        var currentDocument = this.flux.store("application").getCurrentDocument();
        if (!currentDocument) {
            return Promise.resolve();
        } else {
            return Promise.join(
                this.dispatchAsync(events.ui.TOGGLE_OVERLAYS, { enabled: false }),
                this.transfer(history.incrementHistory, currentDocument.id),
                function () {
                    return this.dispatchAsync(events.ui.TOGGLE_OVERLAYS, { enabled: true });
                }.bind(this));
        }
    };
    redo.reads = [locks.JS_APP, locks.JS_DOC];
    redo.writes = [locks.JS_UI];
    redo.transfers = [history.incrementHistory];
    undo.modal = true;

    exports.nativeCut = nativeCut;
    exports.nativeCopy = nativeCopy;
    exports.nativePaste = nativePaste;
    exports.nativeSelectAll = nativeSelectAll;
    exports.cut = cut;
    exports.copy = copy;
    exports.paste = paste;
    exports.selectAll = selectAll;
    exports.undo = undo;
    exports.redo = redo;
});
<commit_msg>Ater nativePaste, only restore policies if we're not in a modal state.  Also add 'waitForCompletion' to the native command<commit_after>/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

define(function (require, exports) {
    "use strict";

    var Promise = require("bluebird"),
        Immutable = require("immutable");

    var os = require("adapter/os");

    var events = require("../events"),
        locks = require("../locks"),
        layers = require("js/actions/layers"),
        menu = require("./menu"),
        collection = require("js/util/collection"),
        headlights = require("js/util/headlights"),
        history = require("js/actions/history"),
        policyActions = require("js/actions/policy"),
        dom = require("js/util/dom");

    /**
     * Native menu command IDs for Photoshop edit commands.
     *
     * @const
     * @private
     * @type {number}
     */
    var CUT_NATIVE_MENU_COMMMAND_ID = 103,
        COPY_NATIVE_MENU_COMMMAND_ID = 104,
        PASTE_NATIVE_MENU_COMMMAND_ID = 105,
        SELECT_ALL_NATIVE_MENU_COMMMAND_ID = 1017;

    var LAYER_CLIPBOARD_FORMAT = "com.adobe.photoshop.spaces.design.layers";

    /**
     * Execute a native cut command.
     *
     * @private
     * @return {Promise}
     */
    var nativeCut = function () {
        return this.transfer(menu.nativeModal, {
            commandID: CUT_NATIVE_MENU_COMMMAND_ID
        })
        .catch(function () {
            // Ignore errors from menu.nativeModal
        });
    };
    nativeCut.modal = true;
    nativeCut.reads = [];
    nativeCut.writes = [];
    nativeCut.transfers = [menu.nativeModal];

    /**
     * Execute a native copy command.
     *
     * @private
     * @return {Promise}
     */
    var nativeCopy = function () {
        return this.transfer(menu.nativeModal, {
            commandID: COPY_NATIVE_MENU_COMMMAND_ID
        })
        .catch(function () {
            // Ignore errors from menu.nativeModal
        });
    };
    nativeCopy.modal = true;
    nativeCopy.reads = [];
    nativeCopy.writes = [];
    nativeCopy.transfers = [menu.nativeModal];

    /**
     * Execute a native paste command.
     *
     * @private
     * @return {Promise}
     */
    var nativePaste = function () {
        // FIXME: this suspend policies hack is for pasting smart object from other sources (e.g. Illustrator).
        // The cause is similar to the place-object menu commands: DS is not receiving "toolModalStateChanged" events
        // until the object is committed.
        // To avoid restoring policies while in a modal state, text in particular, first check the tool store
        // modal state before restoring policies.  Super. Hack.
        var policyStore = this.flux.store("policy"),
            suspendPromise;

        if (policyStore.areAllSuspended()) {
            suspendPromise = Promise.resolve();
        } else {
            suspendPromise = this.transfer(policyActions.suspendAllPolicies);
        }

        return suspendPromise
            .bind(this)
            .then(function () {
                return this.transfer(menu.nativeModal, {
                    commandID: PASTE_NATIVE_MENU_COMMMAND_ID,
                    waitForCompletion: true
                });
            })
            .finally(function () {
                var toolStore = this.flux.store("tool"),
                    policyStore = this.flux.store("policy");

                if (!toolStore.getModalToolState() && policyStore.areAllSuspended()) {
                    return this.transfer(policyActions.restoreAllPolicies);
                }
            });
    };
    nativePaste.modal = true;
    nativePaste.reads = [];
    nativePaste.writes = [];
    nativePaste.transfers = [menu.nativeModal, policyActions.suspendAllPolicies, policyActions.restoreAllPolicies];

    /**
     * Execute a native selectAll command.
     *
     * @private
     * @param {boolean} waitForCompletion Flag for nativeModal
     * @return {Promise}
     */
    var nativeSelectAll = function (waitForCompletion) {
        waitForCompletion = waitForCompletion || false;

        return this.transfer(menu.nativeModal, {
            commandID: SELECT_ALL_NATIVE_MENU_COMMMAND_ID,
            waitForCompletion: waitForCompletion
        })
        .catch(function () {
            // Ignore errors from menu.nativeModal
        });
    };
    nativeSelectAll.modal = true;
    nativeSelectAll.reads = [];
    nativeSelectAll.writes = [];
    nativeSelectAll.transfers = [menu.nativeModal];

    /**
     * Execute either a cut or copy operation, depending on the value of the parameter.
     *
     * @private
     * @param {boolean} cut If true, perform a cut operation; otherwise, a copy.
     * @return {Promise}
     */
    var _cutOrCopy = function (cut) {
        return os.hasKeyboardFocus()
            .bind(this)
            .then(function (cefHasFocus) {
                var el = window.document.activeElement,
                    data;

                if (cefHasFocus && dom.isInput(el)) {
                    if (dom.isTextInput(el)) {
                        data = el.value.substring(el.selectionStart, el.selectionEnd);
                        if (cut) {
                            el.setRangeText("");
                        }
                    } else {
                        data = el.value;
                    }
                } else {
                    // Even if CEF doesn't have focus, a disabled input could have a selection
                    var selection = window.document.getSelection();
                    if (selection.type === "Range") {
                        data = selection.toString();
                    }
                }

                if (typeof data === "string") {
                    var cutCopyEvent = new window.Event(cut ? "cut" : "copy", { bubbles: true });
                    el.dispatchEvent(cutCopyEvent);

                    return os.clipboardWrite(data);
                }

                // If we're on modal state (type edit), we should go with native copy/cut
                if (this.flux.store("tool").getModalToolState()) {
                    if (cut) {
                        return this.transfer(nativeCut);
                    } else {
                        return this.transfer(nativeCopy);
                    }
                } else if (!cut) {
                    var applicationStore = this.flux.store("application"),
                        document = applicationStore.getCurrentDocument();

                    if (!document || document.unsupported) {
                        return;
                    }

                    var layerIDs = collection.pluck(document.layers.selectedNormalized, "id"),
                        payload = {
                            document: document.id,
                            layers: layerIDs
                        },
                        rawPayload = JSON.stringify(payload);

                    headlights.logEvent("edit", "layers", "copy-layers");
                    return os.clipboardWrite(rawPayload, LAYER_CLIPBOARD_FORMAT);
                }
            });
    };

    /**
     * Execute a cut operation on the currently active HTML element.
     *
     * @private
     * @return {Promise}
     */
    var cut = function () {
        return _cutOrCopy.call(this, true);
    };
    cut.modal = true;
    cut.reads = [locks.JS_TOOL, locks.PS_APP];
    cut.writes = [locks.JS_DOC, locks.PS_DOC, locks.OS_CLIPBOARD];
    cut.transfers = [nativeCut];

    /**
     * Execute a copy operation on the currently active HTML element.
     *
     * @private
     * @return {Promise}
     */
    var copy = function () {
        return _cutOrCopy.call(this, false);
    };
    copy.modal = true;
    copy.reads = [locks.JS_DOC, locks.JS_TOOL, locks.PS_APP];
    copy.writes = [locks.OS_CLIPBOARD];
    copy.transfers = [nativeCopy];

    /**
     * Execute a paste operation on the currently active HTML element.
     *
     * @private
     * @return {Promise}
     */
    var paste = function () {
        return os.hasKeyboardFocus()
            .bind(this)
            .then(function (cefHasFocus) {
                var el = window.document.activeElement;
                if (cefHasFocus && dom.isInput(el)) {
                    return os.clipboardRead()
                        .then(function (result) {
                            var data = result.data,
                                format = result.format;

                            if (format !== "string") {
                                return;
                            }

                            if (dom.isTextInput(el)) {
                                var selectionStart = el.selectionStart;
                                el.setRangeText(data);
                                el.setSelectionRange(selectionStart + data.length, selectionStart + data.length);
                            } else {
                                el.value = data;
                            }

                            var pasteEvent = new window.Event("paste", { bubbles: true });
                            el.dispatchEvent(pasteEvent);
                        });
                } else {
                    return os.clipboardRead([LAYER_CLIPBOARD_FORMAT])
                        .bind(this)
                        .then(function (result) {
                            var format = result.format;
                            if (format !== LAYER_CLIPBOARD_FORMAT) {
                                return this.transfer(nativePaste);
                            }

                            var applicationStore = this.flux.store("application"),
                                document = applicationStore.getCurrentDocument();

                            if (!document || document.unsupported) {
                                return;
                            }

                            var data = result.data,
                                payload = JSON.parse(data),
                                documentID = payload.document,
                                documentStore = this.flux.store("document"),
                                fromDocument = documentStore.getDocument(documentID);

                            if (!fromDocument || fromDocument.unsupported) {
                                return;
                            }

                            var layerIDs = payload.layers,
                                fromLayers = Immutable.List(layerIDs.reduce(function (layers, layerID) {
                                    var layer = fromDocument.layers.byID(layerID);
                                    if (layer) {
                                        layers.push(layer);
                                    }
                                    return layers;
                                }, []));

                            headlights.logEvent("edit", "layers", "paste-layers");
                            return this.transfer(layers.duplicate, document, fromDocument, fromLayers);
                        });
                }
            });
    };
    paste.modal = true;
    paste.reads = [locks.JS_DOC, locks.JS_APP, locks.OS_CLIPBOARD, locks.PS_APP];
    paste.writes = [];
    paste.transfers = [layers.duplicate, nativePaste];

    /**
     * Execute a select operation on the currently active HTML element.
     *
     * @private
     * @return {Promise}
     */
    var selectAll = function () {
        return os.hasKeyboardFocus()
            .bind(this)
            .then(function (cefHasFocus) {
                var el = window.document.activeElement;
                if (cefHasFocus && dom.isInput(el)) {
                    if (dom.isTextInput(el)) {
                        el.setSelectionRange(0, el.value.length);
                    }
                } else {
                    var toolStore = this.flux.store("tool");
                    if (toolStore.getModalToolState()) {
                        return this.transfer(nativeSelectAll);
                    } else {
                        return this.transfer(layers.selectAll);
                    }
                }
            });
    };
    selectAll.modal = true;
    selectAll.reads = [locks.JS_TOOL, locks.PS_APP];
    selectAll.writes = [];
    selectAll.transfers = [layers.selectAll, nativeSelectAll];

    /**
     * Step Backwards by transferring to the appropriate history action
     *
     * @private
     * @return {Promise}
     */
    var undo = function () {
        var currentDocument = this.flux.store("application").getCurrentDocument();
        if (!currentDocument) {
            return Promise.resolve();
        } else {
            return Promise.join(
                this.dispatchAsync(events.ui.TOGGLE_OVERLAYS, { enabled: false }),
                this.transfer(history.decrementHistory, currentDocument.id),
                function () {
                    return this.dispatchAsync(events.ui.TOGGLE_OVERLAYS, { enabled: true });
                }.bind(this));
        }
    };
    undo.reads = [locks.JS_APP, locks.JS_DOC];
    undo.writes = [locks.JS_UI];
    undo.transfers = [history.decrementHistory];
    undo.modal = true;

    /**
     * Step Forward by transferring to the appropriate history action
     *
     * @private
     * @return {Promise}
     */
    var redo = function () {
        var currentDocument = this.flux.store("application").getCurrentDocument();
        if (!currentDocument) {
            return Promise.resolve();
        } else {
            return Promise.join(
                this.dispatchAsync(events.ui.TOGGLE_OVERLAYS, { enabled: false }),
                this.transfer(history.incrementHistory, currentDocument.id),
                function () {
                    return this.dispatchAsync(events.ui.TOGGLE_OVERLAYS, { enabled: true });
                }.bind(this));
        }
    };
    redo.reads = [locks.JS_APP, locks.JS_DOC];
    redo.writes = [locks.JS_UI];
    redo.transfers = [history.incrementHistory];
    undo.modal = true;

    exports.nativeCut = nativeCut;
    exports.nativeCopy = nativeCopy;
    exports.nativePaste = nativePaste;
    exports.nativeSelectAll = nativeSelectAll;
    exports.cut = cut;
    exports.copy = copy;
    exports.paste = paste;
    exports.selectAll = selectAll;
    exports.undo = undo;
    exports.redo = redo;
});
<|endoftext|>


<commit_before>'use strict';

const EventEmitter = require('events');
const ServiceProvider = require('./ServiceProvider');
const _pick = require('lodash.pick');

class ServiceEmitter extends EventEmitter {

  constructor (opts) {

    // Default value for `opts`.
    opts = opts || {};

    super(); // Call `EventEmitter` constructor.

    /**
     * Will hold object returned by `setInterval` to control service intervals.
     *
     * @private
     * @type {Object}
     */
    this._intervalObject = undefined;

    /**
     * Delay in milliseconds for intervals.
     *
     * @private
     * @constant
     * @type {Number}
     */
    this._delay = opts.delay || 1000;

    /**
     * Flag to know if we have to call {@link this.provider.requestData()}
     * automatically or not, default set to true.
     *
     * @private
     * @constant
     * @type {Boolean}
     */
    this._autoRequest = opts.autoRequest || true;

    /**
     * Keeps last data set by provider.
     *
     * @private
     * @type {Object}
     */
    this._data = undefined;

    /**
     * Some implementation of `ServiceProvider`.
     *
     * @private
     * @type {ServiceProvider}
     */
    this.provider = undefined;

    // Listen for new event listeners.
    this.on('newListener', (event, listener) => {

      // First we make sure `provider` is defined.
      if (this.provider === undefined) {
        throw new Error('"provider" should be defined before listening any event.');
      }

      // We do not handle 'removeListener' here.
      if (event === 'removeListener') { return; }

      // Only 'data' and 'error' events allowed.
      if (event !== 'data' && event !== 'error') {
        throw new Error('For now, only listening "data" and "error" events are allow.');
      }

      // event 'data' is our keyword event name for main process.
      if (event === 'data') {

        // When got first 'data' listener...
        if (!this.dataHasListeners()) {

          // ...set provider to ON.
          this.provider.turnON();

          // Listen for provider 'data' event to be able to transmit the message
          // to custom service emitters.
          this.provider.on('data', data => {

            // Implementation of `onProviderData` should return a `Boolean`
            // indicating if data received from provider is valid or not, if
            // valid then proceed to update `this._data`.
            if (this.onProviderData(data)) {
              this._data = data;
            }
          });

          // Listen provider 'error' event to be able to transmit error to
          // custom service emitters.
          this.provider.on('error', err => this.onProviderError(err));
        }

        // Make sure we are not overriding `_intervalObject`.
        if (this._intervalObject === undefined) {
          this._intervalObject = setInterval(() => {

            // If `this._autoRequest` is true and provider is registered then
            // request data to provider.
            if (this._autoRequest && this.isProviderRegistered()) {
              this.provider.requestData();
            }

            // Call `this.interval()` for custom emitters to know.
            this.interval();

          }, this._delay);
        }
      }

      // Call `onNewListener` for custom behavior.
      this.onNewListener(event, listener);
    });

    // Listen removeListener event.
    this.on('removeListener', (event, listener) => {

      // If no one listens 'data' event then...
      if (!this.dataHasListeners()) {

        // Reset interval.
        clearInterval(this._intervalObject);
        this._intervalObject = undefined;

        // Reset data.
        this._data = undefined;

        // Make sure provider is registered before setting it OFF.
        if (this.isProviderRegistered()) {
          this.provider.turnOFF();

          // Remove all provider listeners, 'data', 'error' and 'wantsData'.
          this.provider.removeAllListeners();
        }
      }

      // Call `onRemoveListener` for custom behavior.
      this.onRemoveListener(event, listener);
    });
  }

  /**
   * Checks if any listeners listen 'data' event.
   *
   * @return {Boolean} `true` if at least one client listens `data` event.
   */
  dataHasListeners () {
    return this.listenerCount('data') > 0;
  }

  /**
   * Checks if listener provided is listening event name.
   *
   * @param {String} event - Event name to check.
   * @param {Function} listener - Listener to check.
   * @return {Boolean} Whether listener listens event.
   */
  isListeningTo (event, listener) {
    return this.listeners(event).some(_listener => {
      return _listener === listener;
    });
  }

  /**
   * Public method for ServiceProvider registration.
   *
   * @param {ServiceProvider} provider - One of the available providers.
   */
  registerProvider (provider) {

    if (!(provider instanceof ServiceProvider)) {
      throw new Error('"provider" must be an instance of `ServiceProvider`.');
    }

    if (this.isProviderRegistered()) {
      throw new Error('"provider" is already registered!');
    }

    // Do the actual registration.
    this.provider = provider;
  }

  /**
   * Verifies if {@link this.provider} is already registered.
   *
   * @return {Boolean} `true` is registered, `false` otherwise.
   */
  isProviderRegistered () {
    return this.provider && this.provider instanceof ServiceProvider;
  }

  /**
   * Get current provider data with optional pick filter.
   *
   * @param {Array} [pick] - Array of properties to return in Object.
   * @return {Object} Current provider data Object.
   */
  getCurrentData (pick) {
    return !pick ? this._data : _pick(this._data, pick);
  }

  /**
   * Empty `interval` function to be implemented by custom emitter.
   *
   * @abstract
   * @desc This function will be called in each cycle of main interval.
   */
  interval () { }

  /**
   * Empty `onNewListener` function to be implemented by custom emitter.
   *
   * @abstract
   * @desc This function will be called on new event listener.
   * @param {String} event - Event name.
   * @param {Function} listener - Listener Function.
   */
  onNewListener (event, listener) { }

  /**
   * Empty `onRemoveListener` function to be implemented by custom emitter.
   *
   * @abstract
   * @desc This function will be called on remotion of event listener.
   * @param {String} event - Event name.
   * @param {Function} listener - Listener Function.
   */
  onRemoveListener (event, listener) { }

  /**
   * Empty `onProviderData` function to be implemented by custom emitter.
   *
   * @abstract
   * @desc This function will be called when provider emits new data.
   * @param {Object} data - Data to emit.
   * @return {Boolean} Indicates if data received from provider is valid or not.
   *
   * Note: The returned value by the implementation of this method will be used
   * to decide if we should update {@link this._data} with data received or not.
   */
  onProviderData (data) { }

  /**
   * Empty `onProviderError` function to be implemented by custom emitter.
   *
   * @abstract
   * @desc This function will be called when provider emits an error.
   * @param {Object} err - Error to emit.
   */
  onProviderError (err) { }
}

module.exports = ServiceEmitter;
<commit_msg>use #isProviderRegistered() when checking new listeners<commit_after>'use strict';

const EventEmitter = require('events');
const ServiceProvider = require('./ServiceProvider');
const _pick = require('lodash.pick');

class ServiceEmitter extends EventEmitter {

  constructor (opts) {

    // Default value for `opts`.
    opts = opts || {};

    super(); // Call `EventEmitter` constructor.

    /**
     * Will hold object returned by `setInterval` to control service intervals.
     *
     * @private
     * @type {Object}
     */
    this._intervalObject = undefined;

    /**
     * Delay in milliseconds for intervals.
     *
     * @private
     * @constant
     * @type {Number}
     */
    this._delay = opts.delay || 1000;

    /**
     * Flag to know if we have to call {@link this.provider.requestData()}
     * automatically or not, default set to true.
     *
     * @private
     * @constant
     * @type {Boolean}
     */
    this._autoRequest = opts.autoRequest || true;

    /**
     * Keeps last data set by provider.
     *
     * @private
     * @type {Object}
     */
    this._data = undefined;

    /**
     * Some implementation of `ServiceProvider`.
     *
     * @private
     * @type {ServiceProvider}
     */
    this.provider = undefined;

    // Listen for new event listeners.
    this.on('newListener', (event, listener) => {

      // First we make sure `provider` is defined.
      if (this.isProviderRegistered()) {
        throw new Error('"provider" should be registered before listening any event.');
      }

      // We do not handle 'removeListener' here.
      if (event === 'removeListener') { return; }

      // Only 'data' and 'error' events allowed.
      if (event !== 'data' && event !== 'error') {
        throw new Error('For now, only listening "data" and "error" events are allow.');
      }

      // event 'data' is our keyword event name for main process.
      if (event === 'data') {

        // When got first 'data' listener...
        if (!this.dataHasListeners()) {

          // ...set provider to ON.
          this.provider.turnON();

          // Listen for provider 'data' event to be able to transmit the message
          // to custom service emitters.
          this.provider.on('data', data => {

            // Implementation of `onProviderData` should return a `Boolean`
            // indicating if data received from provider is valid or not, if
            // valid then proceed to update `this._data`.
            if (this.onProviderData(data)) {
              this._data = data;
            }
          });

          // Listen provider 'error' event to be able to transmit error to
          // custom service emitters.
          this.provider.on('error', err => this.onProviderError(err));
        }

        // Make sure we are not overriding `_intervalObject`.
        if (this._intervalObject === undefined) {
          this._intervalObject = setInterval(() => {

            // If `this._autoRequest` is true and provider is registered then
            // request data to provider.
            if (this._autoRequest && this.isProviderRegistered()) {
              this.provider.requestData();
            }

            // Call `this.interval()` for custom emitters to know.
            this.interval();

          }, this._delay);
        }
      }

      // Call `onNewListener` for custom behavior.
      this.onNewListener(event, listener);
    });

    // Listen removeListener event.
    this.on('removeListener', (event, listener) => {

      // If no one listens 'data' event then...
      if (!this.dataHasListeners()) {

        // Reset interval.
        clearInterval(this._intervalObject);
        this._intervalObject = undefined;

        // Reset data.
        this._data = undefined;

        // Make sure provider is registered before setting it OFF.
        if (this.isProviderRegistered()) {
          this.provider.turnOFF();

          // Remove all provider listeners, 'data', 'error' and 'wantsData'.
          this.provider.removeAllListeners();
        }
      }

      // Call `onRemoveListener` for custom behavior.
      this.onRemoveListener(event, listener);
    });
  }

  /**
   * Checks if any listeners listen 'data' event.
   *
   * @return {Boolean} `true` if at least one client listens `data` event.
   */
  dataHasListeners () {
    return this.listenerCount('data') > 0;
  }

  /**
   * Checks if listener provided is listening event name.
   *
   * @param {String} event - Event name to check.
   * @param {Function} listener - Listener to check.
   * @return {Boolean} Whether listener listens event.
   */
  isListeningTo (event, listener) {
    return this.listeners(event).some(_listener => {
      return _listener === listener;
    });
  }

  /**
   * Public method for ServiceProvider registration.
   *
   * @param {ServiceProvider} provider - One of the available providers.
   */
  registerProvider (provider) {

    if (!(provider instanceof ServiceProvider)) {
      throw new Error('"provider" must be an instance of `ServiceProvider`.');
    }

    if (this.isProviderRegistered()) {
      throw new Error('"provider" is already registered!');
    }

    // Do the actual registration.
    this.provider = provider;
  }

  /**
   * Verifies if {@link this.provider} is already registered.
   *
   * @return {Boolean} `true` is registered, `false` otherwise.
   */
  isProviderRegistered () {
    return this.provider && this.provider instanceof ServiceProvider;
  }

  /**
   * Get current provider data with optional pick filter.
   *
   * @param {Array} [pick] - Array of properties to return in Object.
   * @return {Object} Current provider data Object.
   */
  getCurrentData (pick) {
    return !pick ? this._data : _pick(this._data, pick);
  }

  /**
   * Empty `interval` function to be implemented by custom emitter.
   *
   * @abstract
   * @desc This function will be called in each cycle of main interval.
   */
  interval () { }

  /**
   * Empty `onNewListener` function to be implemented by custom emitter.
   *
   * @abstract
   * @desc This function will be called on new event listener.
   * @param {String} event - Event name.
   * @param {Function} listener - Listener Function.
   */
  onNewListener (event, listener) { }

  /**
   * Empty `onRemoveListener` function to be implemented by custom emitter.
   *
   * @abstract
   * @desc This function will be called on remotion of event listener.
   * @param {String} event - Event name.
   * @param {Function} listener - Listener Function.
   */
  onRemoveListener (event, listener) { }

  /**
   * Empty `onProviderData` function to be implemented by custom emitter.
   *
   * @abstract
   * @desc This function will be called when provider emits new data.
   * @param {Object} data - Data to emit.
   * @return {Boolean} Indicates if data received from provider is valid or not.
   *
   * Note: The returned value by the implementation of this method will be used
   * to decide if we should update {@link this._data} with data received or not.
   */
  onProviderData (data) { }

  /**
   * Empty `onProviderError` function to be implemented by custom emitter.
   *
   * @abstract
   * @desc This function will be called when provider emits an error.
   * @param {Object} err - Error to emit.
   */
  onProviderError (err) { }
}

module.exports = ServiceEmitter;
<|endoftext|>