/* global AFRAME, assert, process, suite, teardown, test, setup, sinon, HTMLElement, HTMLHeadElement */ var Component = require('core/component'); var components = require('index').components; var helpers = require('../helpers'); var registerComponent = require('index').registerComponent; var CloneComponent = { init: function () { var object3D = this.el.object3D; var clone = object3D.clone(); clone.uuid = 'Bubble Fett'; object3D.add(clone); } }; var entityFactory = helpers.entityFactory; suite('Component', function () { suite('standard components', function () { test('are registered', function () { assert.ok('geometry' in components); assert.ok('material' in components); assert.ok('light' in components); assert.ok('position' in components); assert.ok('rotation' in components); assert.ok('scale' in components); }); }); suite('buildData', function () { setup(function () { components.dummy = undefined; }); test('uses default values', function () { registerComponent('dummy', { schema: { color: {default: 'blue'}, size: {default: 5} } }); const el = document.createElement('a-entity'); el.setAttribute('dummy', ''); const data = el.components.dummy.buildData({}, null); assert.equal(data.color, 'blue'); assert.equal(data.size, 5); }); test('uses default values for single-property schema', function () { registerComponent('dummy', { schema: {default: 'blue'} }); var el = document.createElement('a-entity'); el.setAttribute('dummy', ''); var data = el.components.dummy.buildData(undefined, null); assert.equal(data, 'blue'); }); test('preserves type of default values', function () { registerComponent('dummy', { schema: { list: {default: [1, 2, 3, 4]}, none: {default: null}, string: {default: ''} } }); var el = document.createElement('a-entity'); el.setAttribute('dummy', ''); var data = el.components.dummy.buildData(undefined, null); assert.shallowDeepEqual(data.list, [1, 2, 3, 4]); assert.equal(data.none, null); assert.equal(data.string, ''); }); test('uses mixin values', function () { var data; registerComponent('dummy', { schema: { color: {default: 'red'}, size: {default: 5} } }); var el = document.createElement('a-entity'); var mixinEl = document.createElement('a-mixin'); mixinEl.setAttribute('dummy', 'color: blue; size: 10'); el.mixinEls = [mixinEl]; el.setAttribute('dummy', ''); data = el.components.dummy.buildData({}, null); assert.equal(data.color, 'blue'); assert.equal(data.size, 10); }); test('uses mixin values for single-property schema', function () { var data; registerComponent('dummy', { schema: { default: 'red' } }); var el = document.createElement('a-entity'); var mixinEl = document.createElement('a-mixin'); mixinEl.setAttribute('dummy', 'blue'); el.mixinEls = [mixinEl]; el.setAttribute('dummy', ''); data = el.components.dummy.buildData(undefined, null); assert.equal(data, 'blue'); }); test('uses defined values', function () { var data; registerComponent('dummy', { schema: { color: {default: 'red'}, size: {default: 5} } }); var el = document.createElement('a-entity'); var mixinEl = document.createElement('a-mixin'); mixinEl.setAttribute('dummy', 'color: blue; size: 10'); el.mixinEls = [mixinEl]; el.setAttribute('dummy', ''); data = el.components.dummy.buildData({color: 'green', size: 20}, 'color: green; size: 20'); assert.equal(data.color, 'green'); assert.equal(data.size, 20); }); test('uses defined values for single-property schema', function () { var data; registerComponent('dummy', { schema: {default: 'red'} }); var el = document.createElement('a-entity'); var mixinEl = document.createElement('a-mixin'); mixinEl.setAttribute('dummy', 'blue'); el.mixinEls = [mixinEl]; el.setAttribute('dummy', ''); data = el.components.dummy.buildData('green', 'green'); assert.equal(data, 'green'); }); test('returns default for single-prop schema when attr is empty string', function () { var data; registerComponent('dummy', { schema: {default: 'red'} }); var el = document.createElement('a-entity'); el.setAttribute('dummy', ''); data = el.components.dummy.buildData('red'); assert.equal(data, 'red'); }); test('multiple vec3 properties do not share same default value object', function (done) { var data; var el = entityFactory(); var TestComponent = registerComponent('dummy', { schema: { direction: {type: 'vec3'}, position: {type: 'vec3'} } }); el.addEventListener('loaded', function () { el.setAttribute('dummy', ''); data = el.getAttribute('dummy'); assert.shallowDeepEqual(data.direction, TestComponent.prototype.schema.direction.default); assert.shallowDeepEqual(data.position, TestComponent.prototype.schema.position.default); assert.notEqual(data.direction, data.position); done(); }); }); test('boolean properties are preserved when updating the component data', function () { var data; registerComponent('dummy', { schema: { depthTest: {default: true}, color: {default: 'red'} } }); var el = document.createElement('a-entity'); el.setAttribute('dummy', {color: 'blue', depthTest: false}); data = el.components.dummy.buildData({color: 'red'}); assert.equal(data.depthTest, false); assert.equal(data.color, 'red'); }); test('returns data for single-prop if default is null', function () { var el = document.createElement('a-entity'); registerComponent('test', { schema: {default: null} }); el.setAttribute('test', ''); assert.equal(el.components.test.buildData(), null); assert.equal(el.components.test.buildData(null), null); assert.equal(el.components.test.buildData('foo'), 'foo'); }); test('returns data for multi-prop if default is null with previousData', function () { var el = document.createElement('a-entity'); registerComponent('test', { schema: { foo: {default: null} } }); el.setAttribute('test', ''); el.components.test.attrValue = {foo: null}; assert.equal(el.components.test.buildData().foo, null); assert.equal(el.components.test.buildData({foo: null}).foo, null); assert.equal(el.components.test.buildData({foo: 'foo'}).foo, 'foo'); }); test('clones array property type', function () { var array = ['a']; var data; var el; registerComponent('test', {schema: {default: array}}); el = document.createElement('a-entity'); el.setAttribute('test', ''); data = el.components.test.buildData(); assert.equal(data[0], 'a'); assert.notEqual(data, array); }); test('does not override set data with mixin', function (done) { var el; var mixinEl; el = helpers.entityFactory(); registerComponent('dummy', { schema: { color: {default: 'blue'}, size: {default: 5} } }); mixinEl = document.createElement('a-mixin'); mixinEl.setAttribute('id', 'mixindummy'); mixinEl.setAttribute('dummy', 'size: 1'); el.addEventListener('loaded', () => { el.sceneEl.appendChild(mixinEl); el.setAttribute('mixin', 'mixindummy'); el.setAttribute('dummy', 'size', 20); el.setAttribute('dummy', 'color', 'red'); assert.equal(el.getAttribute('dummy').color, 'red'); assert.equal(el.getAttribute('dummy').size, 20); done(); }); }); test('copies data correctly for single-prop object-based with string input', done => { registerComponent('test', { schema: { default: {}, parse: function (value) { return value; } } }); helpers.elFactory().then(el => { el.setAttribute('test', 'foo'); el.setAttribute('test', 'bar'); assert.equal(el.components.test.data, 'bar'); done(); }); }); test('handles single-property selector type', function (done) { registerComponent('dummy', { schema: {type: 'selector'} }); helpers.elFactory().then(el => { el.setAttribute('dummy', 'a-entity'); assert.ok(el.components.dummy.data.isEntity); done(); }); }); }); suite('updateProperties', function () { var el; setup(function () { el = entityFactory(); components.dummy = undefined; }); test('emits componentchanged for multi-prop', function (done) { el.setAttribute('material', 'color: red'); el.addEventListener('componentchanged', function (evt) { if (evt.detail.name !== 'material') { return; } assert.equal(evt.detail.name, 'material'); assert.equal(el.getAttribute('material').color, 'blue'); assert.ok('id' in evt.detail); done(); }); setTimeout(() => { el.setAttribute('material', 'color: blue'); }); }); test('emits componentchanged for single-prop', function (done) { el.setAttribute('position', {x: 0, y: 0, z: 0}); el.addEventListener('componentchanged', function (evt) { if (evt.detail.name !== 'position') { return; } assert.shallowDeepEqual(el.getAttribute('position'), {x: 1, y: 2, z: 3}); assert.equal(evt.detail.name, 'position'); assert.ok('id' in evt.detail); done(); }); setTimeout(() => { el.setAttribute('position', {x: 1, y: 2, z: 3}); }); }); test('does not emit componentchanged for multi-prop if not changed', function (done) { function componentChanged (evt) { if (evt.detail.name !== 'material') { return; } el.removeEventListener('componentchanged', componentChanged); // Should not reach here. assert.equal(true, false, 'Component should not have emitted changed.'); } function componentInitialized (evt) { if (evt.detail.name !== 'material') { return; } el.addEventListener('componentchanged', componentChanged); el.setAttribute('material', 'color', 'red'); } el.addEventListener('componentinitialized', componentInitialized); el.setAttribute('material', 'color', 'red'); // Have `done()` race with the failing assertion in the event handler. setTimeout(() => { el.removeEventListener('componentchanged', componentChanged); el.removeEventListener('componentinitialized', componentInitialized); done(); }, 100); }); test('does not update for multi-prop if not changed', function (done) { var spy = this.sinon.spy(); AFRAME.registerComponent('test', { schema: { foo: {type: 'string'}, bar: {type: 'string'} }, update: function () { spy(); } }); el.addEventListener('loaded', () => { el.setAttribute('test', {foo: 'foo', bar: 'bar'}); el.setAttribute('test', {foo: 'foo', bar: 'bar'}); el.setAttribute('test', {foo: 'foo', bar: 'bar'}); assert.equal(spy.getCalls().length, 1); done(); }); }); test('does not update for multi-prop if not changed using same object', function (done) { var data = {foo: 'foo', bar: 'bar'}; var spy = this.sinon.spy(); AFRAME.registerComponent('test', { schema: { foo: {type: 'string'}, bar: {type: 'string'} }, update: function () { spy(); } }); el.addEventListener('loaded', () => { el.setAttribute('test', data); el.setAttribute('test', data); el.setAttribute('test', data); assert.equal(spy.getCalls().length, 1); done(); }); }); test('does not emit componentchanged for single-prop if not changed', function (done) { function componentInitialized (evt) { if (evt.detail.name !== 'visible') { return; } el.addEventListener('componentchanged', componentChanged); el.setAttribute('visible', false); } function componentChanged (evt) { if (evt.detail.name !== 'visible') { return; } // Should not reach here. assert.equal(true, false, 'Component should not have emitted changed.'); } el.addEventListener('componentinitialized', componentInitialized); el.setAttribute('visible', false); // Have `done()` race with the failing assertion in the event handler. setTimeout(() => { el.removeEventListener('componentinitialized', componentInitialized); el.removeEventListener('componentchanged', componentChanged); done(); }, 100); }); test('does not emit componentchanged for value if not changed', function (done) { el.addEventListener('componentinitialized', function (evt) { if (evt.detail.name !== 'visible') { return; } el.addEventListener('componentchanged', function (evt) { if (evt.detail.name !== 'visible') { return; } // Should not reach here. assert.equal(true, false, 'Component should not have emitted changed.'); }); // Update. el.setAttribute('visible', false); // Have `done()` race with the failing assertion in the event handler. setTimeout(() => { done(); }, 100); }); // Initialization. el.setAttribute('visible', false); }); test('emits componentinitialized', function (done) { el.addEventListener('componentinitialized', function (evt) { if (evt.detail.name !== 'material') { return; } assert.ok('id' in evt.detail); assert.equal(evt.detail.name, 'material'); done(); }); el.setAttribute('material', ''); }); test('does not clone selector property default into data', function (done) { registerComponent('dummy', { schema: {type: 'selector', default: document.body} }); helpers.elFactory().then(el => { el.setAttribute('dummy', ''); assert.equal(el.components.dummy.data, el.components.dummy.schema.default); el.setAttribute('dummy', 'head'); assert.notEqual(el.components.dummy.data, el.components.dummy.schema.default); done(); }); }); test('clones plain object schema default into data', function () { var el; registerComponent('dummy', { schema: {type: 'vec3', default: {x: 1, y: 1, z: 1}} }); el = document.createElement('a-entity'); el.hasLoaded = true; el.setAttribute('dummy', '2 2 2'); el.components.dummy.updateProperties(''); assert.notEqual(el.components.dummy.data, el.components.dummy.schema.default); assert.deepEqual(el.components.dummy.data, {x: 1, y: 1, z: 1}); }); test('does not clone properties from attrValue into data that are not plain objects', function () { var el; registerComponent('dummy', { schema: { color: {default: 'blue'}, direction: {type: 'vec3'}, el: {type: 'selector', default: 'body'} } }); el = document.createElement('a-entity'); el.hasLoaded = true; el.setAttribute('dummy', ''); assert.notOk(el.components.dummy.attrValue.el); }); test('does not clone props from attrValue into data that are not plain objects', function () { var attrValue; var el; var data; registerComponent('dummy', { schema: { color: {type: 'string'}, direction: {type: 'vec3'}, el: {type: 'selector', default: 'body'} } }); el = document.createElement('a-entity'); el.hasLoaded = true; el.setAttribute('dummy', ''); assert.notOk(el.components.dummy.attrValue.el); // Direction property preserved across updateProperties calls but cloned into a different // object. el.components.dummy.updateProperties({ color: 'green', direction: {x: 1, y: 1, z: 1}, el: document.head }); el.components.dummy.updateProperties({ color: 'red', el: document.head }); data = el.getAttribute('dummy'); attrValue = el.components.dummy.attrValue; assert.notEqual(data, attrValue); assert.equal(data.color, attrValue.color); // HTMLElement not cloned in attrValue, reference is shared instead. assert.equal(data.el, attrValue.el); assert.equal(data.el.constructor, HTMLHeadElement); assert.deepEqual(data.direction, {x: 1, y: 1, z: 1}); assert.deepEqual(attrValue.direction, {x: 1, y: 1, z: 1}); el.components.dummy.updateProperties({ color: 'red', direction: {x: 1, y: 1, z: 1} }); data = el.getAttribute('dummy'); // HTMLElement not cloned in attrValue, reference is shared instead. assert.equal(data.el.constructor, HTMLHeadElement); assert.equal(data.el, el.components.dummy.attrValue.el); }); test('updates data when combining setAttribute and object3D manipulation', function () { var el = document.createElement('a-entity'); el.hasLoaded = true; el.setAttribute('position', '3 3 3'); assert.equal(3, el.object3D.position.x); assert.equal(3, el.object3D.position.y); assert.equal(3, el.object3D.position.z); el.object3D.position.set(5, 5, 5); assert.equal(5, el.object3D.position.x); assert.equal(5, el.object3D.position.y); assert.equal(5, el.object3D.position.z); el.setAttribute('position', '3 3 3'); assert.equal(3, el.object3D.position.x); assert.equal(3, el.object3D.position.y); assert.equal(3, el.object3D.position.z); }); }); suite('resetProperty', function () { var el; setup(function (done) { el = entityFactory(); el.addEventListener('loaded', () => { done(); }); }); test('resets property to default value', function () { AFRAME.registerComponent('test', { schema: { bar: {default: 5}, foo: {default: 5} } }); el.setAttribute('test', {bar: 10, foo: 10}); el.components.test.resetProperty('bar'); assert.equal(el.getAttribute('test').bar, 5); assert.equal(el.getAttribute('test').foo, 10); }); test('resets property to default value for single-prop', function () { AFRAME.registerComponent('test', { schema: {default: 5} }); el.setAttribute('test', 10); el.components.test.resetProperty(); assert.equal(el.getAttribute('test'), 5); }); }); suite('third-party components', function () { var el; setup(function () { el = entityFactory(); delete components.clone; }); test('can be registered', function () { assert.notOk('clone' in components); registerComponent('clone', CloneComponent); assert.ok('clone' in components); }); test('can change behavior of entity', function (done) { registerComponent('clone', CloneComponent); el.addEventListener('loaded', function () { assert.notOk('clone' in el.components); assert.notOk(el.object3D.children.length); el.setAttribute('clone', ''); assert.ok('clone' in el.components); assert.equal(el.object3D.children[0].uuid, 'Bubble Fett'); done(); }); }); test('cannot be registered if it uses the character __ in the name', function () { assert.notOk('my__component' in components); assert.throws(function register () { registerComponent('my__component', CloneComponent); }, Error); assert.notOk('my__component' in components); }); test('can have underscore in component id', function () { AFRAME.registerComponent('test', {multiple: true}); el.setAttribute('test__foo__bar', ''); assert.equal(el.components['test__foo__bar'].id, 'foo__bar'); }); }); suite('schema', function () { test('can be accessed', function () { assert.ok(components.position.schema); }); }); suite('parse', function () { setup(function () { components.dummy = undefined; }); test('parses single value component', function () { var TestComponent = registerComponent('dummy', { schema: {default: '0 0 1', type: 'vec3'} }); var el = document.createElement('a-entity'); var component = new TestComponent(el); var componentObj = component.parse('1 2 3'); assert.deepEqual(componentObj, {x: 1, y: 2, z: 3}); }); test('parses component properties vec3', function () { var TestComponent = registerComponent('dummy', { schema: { position: {type: 'vec3', default: '0 0 1'} } }); var el = document.createElement('a-entity'); var component = new TestComponent(el); var componentObj = component.parse({position: '0 1 0'}); assert.deepEqual(componentObj.position, {x: 0, y: 1, z: 0}); }); }); suite('parseAttrValueForCache', function () { setup(function () { components.dummy = undefined; }); test('parses single value component', function () { var TestComponent = registerComponent('dummy', { schema: {default: '0 0 1', type: 'vec3'} }); var el = document.createElement('a-entity'); var component = new TestComponent(el); var componentObj = component.parseAttrValueForCache('1 2 3'); assert.deepEqual(componentObj, {x: 1, y: 2, z: 3}); }); test('parses component using the style parser for a complex schema', function () { var TestComponent = registerComponent('dummy', { schema: { position: {type: 'vec3', default: '0 0 1'}, color: {default: 'red'} } }); var el = document.createElement('a-entity'); var component = new TestComponent(el); var componentObj = component.parseAttrValueForCache({position: '0 1 0', color: 'red'}); assert.deepEqual(componentObj, {position: '0 1 0', color: 'red'}); }); test('does not parse properties that parse to another string', function () { var TestComponent = registerComponent('dummy', { schema: { url: {type: 'src', default: ''} } }); var el = document.createElement('a-entity'); var component = new TestComponent(el); var componentObj = component.parseAttrValueForCache({url: 'url(www.mozilla.com)'}); assert.equal(componentObj.url, 'url(www.mozilla.com)'); }); }); suite('stringify', function () { setup(function () { components.dummy = undefined; }); test('stringifies single value component', function () { var TestComponent = registerComponent('dummy', { schema: {default: '0 0 1', type: 'vec3'} }); var el = document.createElement('a-entity'); var component = new TestComponent(el); var componentString = component.stringify({x: 1, y: 2, z: 3}); assert.deepEqual(componentString, '1 2 3'); }); test('stringifies component property vec3', function () { var TestComponent = registerComponent('dummy', { schema: { position: {type: 'vec3', default: '0 0 1'} } }); var el = document.createElement('a-entity'); var component = new TestComponent(el); var componentObj = {position: {x: 1, y: 2, z: 3}}; var componentString = component.stringify(componentObj); assert.equal(componentString, 'position: 1 2 3'); }); }); suite('extendSchema', function () { setup(function () { components.dummy = undefined; }); test('extends the schema', function () { var TestComponent = registerComponent('dummy', { schema: {color: {default: 'red'}} }); var el = document.createElement('a-entity'); var component = new TestComponent(el); component.extendSchema({size: {default: 5}}); assert.ok(component.schema.size); assert.ok(component.schema.color); }); }); suite('updateProperties', function () { setup(function (done) { var el = this.el = entityFactory(); components.dummy = undefined; if (el.hasLoaded) { done(); } el.addEventListener('loaded', function () { done(); }); }); test('updates the schema of a component', function () { var TestComponent = registerComponent('dummy', { schema: {color: {default: 'red'}}, updateSchema: function (data) { this.extendSchema({energy: {default: 100}}); } }); var component = new TestComponent(this.el); component.updateProperties(null); assert.equal(component.schema.color.default, 'red'); assert.equal(component.schema.energy.default, 100); assert.equal(component.data.color, 'red'); }); test('updates the properties with the new value', function () { var TestComponent = registerComponent('dummy', { schema: {color: {default: 'red'}} }); var component = new TestComponent(this.el); component.updateProperties(null); assert.equal(component.data.color, 'red'); }); test('updates the properties with a null value', function () { var TestComponent = registerComponent('dummy', { schema: {color: {default: 'red'}} }); var component = new TestComponent(this.el); component.updateProperties({color: 'blue'}); assert.equal(component.data.color, 'blue'); }); }); suite('init', function () { setup(function (done) { components.dummy = undefined; var el = this.el = entityFactory(); if (el.hasLoaded) { done(); } el.addEventListener('loaded', function () { done(); }); }); test('init is called once if the init routine sets the component', function () { var initCanaryStub = sinon.stub(); var el = this.el; registerComponent('dummy', { schema: {color: {default: 'red'}}, init: function () { this.initCanary(); this.el.setAttribute('dummy', {color: 'green'}); }, initCanary: initCanaryStub }); el.setAttribute('dummy', {color: 'blue'}); assert.equal(el.getAttribute('dummy').color, 'green'); sinon.assert.calledOnce(initCanaryStub); }); }); suite('update', function () { setup(function (done) { components.dummy = undefined; var el = this.el = entityFactory(); if (el.hasLoaded) { done(); } el.addEventListener('loaded', function () { done(); }); }); test('not called if component data does not change', function () { var updateStub = sinon.stub(); var TestComponent = registerComponent('dummy', { schema: {color: {default: 'red'}} }); var component = new TestComponent(this.el); component.update = updateStub; component.updateProperties({color: 'blue'}); component.updateProperties({color: 'blue'}); assert.ok(updateStub.calledOnce); }); test('supports array properties', function () { var updateStub = sinon.stub(); var TestComponent = registerComponent('dummy', { schema: {list: {default: ['a']}} }); var component = new TestComponent(this.el); component.update = updateStub; component.updateProperties({list: ['b']}); component.updateProperties({list: ['b']}); sinon.assert.calledOnce(updateStub); }); test('emit componentchanged when update calls setAttribute', function (done) { var component; var TestComponent; TestComponent = registerComponent('dummy', { schema: {color: {default: 'red'}}, update: function () { this.el.setAttribute('dummy', 'color', 'blue'); } }); this.el.addEventListener('componentchanged', evt => { assert.equal(evt.detail.name, 'dummy'); assert.equal(this.el.getAttribute('dummy').color, 'blue'); done(); }); component = new TestComponent(this.el); assert.equal(component.data.color, 'blue'); }); test('makes oldData empty object on first call when single property component with an object as default initializes', function () { var updateStub = sinon.stub(); registerComponent('dummy', { schema: {type: 'vec3'}, update: updateStub }); this.el.setAttribute('dummy', ''); sinon.assert.calledOnce(updateStub); // old Data passed to the update method assert.deepEqual(updateStub.getCalls()[0].args[0], {}); }); test('makes oldData empty object on first call when multiple property component initializes', function () { var updateStub = sinon.stub(); registerComponent('dummy', { schema: { color: {default: 'red'}, size: {default: 0} }, update: updateStub }); this.el.setAttribute('dummy', ''); sinon.assert.calledOnce(updateStub); // old Data passed to the update method assert.deepEqual(updateStub.getCalls()[0].args[0], {}); }); test('oldData is undefined on the first call when a single property component initializes', function () { var updateStub = sinon.stub(); registerComponent('dummy', { schema: {default: 0}, update: updateStub }); this.el.setAttribute('dummy', ''); sinon.assert.calledOnce(updateStub); // old Data passed to the update method assert.equal(updateStub.getCalls()[0].args[0], undefined); }); test('called when modifying component with value returned from getAttribute', function () { var el = this.el; var direction; var updateStub = sinon.stub(); registerComponent('dummy', { schema: {type: 'vec3', default: {x: 1, y: 1, z: 1}}, update: updateStub }); el.setAttribute('dummy', ''); direction = el.getAttribute('dummy'); assert.deepEqual(direction, {x: 1, y: 1, z: 1}); direction.x += 1; direction.y += 1; direction.z += 1; el.setAttribute('dummy', direction); sinon.assert.calledTwice(updateStub); // oldData passed to the update method. assert.equal(updateStub.getCalls()[0].args[0].x, undefined); assert.equal(updateStub.getCalls()[0].args[0].y, undefined); assert.equal(updateStub.getCalls()[0].args[0].z, undefined); assert.deepEqual(updateStub.getCalls()[1].args[0], {x: 1, y: 1, z: 1}); assert.deepEqual(el.components.dummy.data, {x: 2, y: 2, z: 2}); }); test('properly passes oldData and data properly on recursive calls to setAttribute', function () { var el = this.el; registerComponent('dummy', { schema: { color: {default: 'red'}, size: {default: 0} }, update: function (oldData) { if (this.data.color === 'red') { this.el.setAttribute('dummy', 'color', 'blue'); } if (oldData.color === 'red') { this.el.setAttribute('dummy', 'color', 'green'); } } }); el.setAttribute('dummy', 'color: red'); assert.equal(el.getAttribute('dummy').color, 'green'); }); }); suite('flushToDOM', function () { setup(function () { components.dummy = undefined; }); test('updates component DOM attribute', function () { registerComponent('dummy', { schema: {color: {default: 'red'}} }); var el = document.createElement('a-entity'); el.setAttribute('dummy', {color: 'blue'}); assert.equal(HTMLElement.prototype.getAttribute.call(el, 'dummy'), ''); el.components.dummy.flushToDOM(); assert.equal(HTMLElement.prototype.getAttribute.call(el, 'dummy'), 'color: blue'); }); test('init and update are not called for a not loaded entity', function () { var updateStub = sinon.stub(); var initStub = sinon.stub(); var el = document.createElement('a-entity'); registerComponent('dummy', { schema: {color: {default: 'red'}}, init: initStub, update: updateStub }); assert.notOk(el.hasLoaded); el.setAttribute('dummy', {color: 'blue'}); assert.equal(HTMLElement.prototype.getAttribute.call(el, 'dummy'), ''); el.components.dummy.flushToDOM(); sinon.assert.notCalled(initStub); sinon.assert.notCalled(updateStub); assert.equal(HTMLElement.prototype.getAttribute.call(el, 'dummy'), 'color: blue'); }); test('flushes false boolean', function () { var el = document.createElement('a-entity'); registerComponent('dummy', { schema: {isDurrr: {default: true}} }); el.setAttribute('dummy', {isDurrr: false}); el.components.dummy.flushToDOM(); assert.equal(HTMLElement.prototype.getAttribute.call(el, 'dummy'), 'isDurrr: false'); }); }); suite('play', function () { setup(function () { components.dummy = undefined; var playStub = this.playStub = sinon.stub(); registerComponent('dummy', {play: playStub}); }); test('not called if entity is not playing', function () { var el = document.createElement('a-entity'); var dummyComponent; el.isPlaying = false; el.setAttribute('dummy', ''); dummyComponent = el.components.dummy; dummyComponent.initialized = true; dummyComponent.play(); sinon.assert.notCalled(this.playStub); }); test('not called if component is not initialized', function () { var el = document.createElement('a-entity'); el.isPlaying = true; el.setAttribute('dummy', ''); el.components.dummy.play(); sinon.assert.notCalled(this.playStub); }); test('not called if component is already playing', function () { var el = document.createElement('a-entity'); var dummyComponent; el.isPlaying = true; el.setAttribute('dummy', ''); dummyComponent = el.components.dummy; dummyComponent.initialized = true; dummyComponent.play(); dummyComponent.play(); sinon.assert.calledOnce(this.playStub); }); }); suite('pause', function () { setup(function () { components.dummy = undefined; var pauseStub = this.pauseStub = sinon.stub(); registerComponent('dummy', { schema: {color: {default: 'red'}}, pause: pauseStub }); }); test('not called if component is not playing', function () { var el = document.createElement('a-entity'); el.setAttribute('dummy', ''); el.components.dummy.pause(); sinon.assert.notCalled(this.pauseStub); }); test('called if component is playing', function () { var el = document.createElement('a-entity'); var dummyComponent; el.setAttribute('dummy', ''); el.isPlaying = true; dummyComponent = el.components.dummy; dummyComponent.initialized = true; dummyComponent.isPlaying = true; dummyComponent.pause(); sinon.assert.called(this.pauseStub); }); test('not called if component is already paused', function () { var el = document.createElement('a-entity'); var dummyComponent; el.setAttribute('dummy', ''); dummyComponent = el.components.dummy; dummyComponent.isPlaying = true; dummyComponent.pause(); dummyComponent.pause(); sinon.assert.calledOnce(this.pauseStub); }); }); test('applies default array property types with no defined value', function (done) { registerComponent('test', { schema: { arr: {default: ['foo']} }, update: function () { assert.equal(this.data.arr[0], 'foo'); done(); } }); const el = entityFactory(); el.addEventListener('loaded', () => { el.setAttribute('test', ''); }); }); suite('events', () => { let component; let el; let fooSpy; setup(function (done) { fooSpy = this.sinon.spy(); registerComponent('test', { events: { foo: function (evt) { assert.ok(evt); assert.ok(this === component); fooSpy(); } } }); helpers.elFactory().then(_el => { el = _el; el.setAttribute('test', ''); component = el.components.test; done(); }); }); test('calls handler on event', function (done) { el.emit('foo'); setTimeout(() => { assert.equal(fooSpy.callCount, 1); done(); }); }); test('detaches on component pause', function (done) { component.pause(); el.emit('foo'); setTimeout(() => { assert.notOk(fooSpy.called); done(); }); }); test('detaches on component remove', function (done) { el.removeAttribute('test'); el.emit('foo'); setTimeout(() => { assert.notOk(fooSpy.called); done(); }); }); test('detaches on entity pause', function (done) { el.pause(); el.emit('foo'); setTimeout(() => { assert.notOk(fooSpy.called); done(); }); }); test('detaches on entity remove', function (done) { el.parentNode.removeChild(el); setTimeout(() => { el.emit('foo'); setTimeout(() => { assert.notOk(fooSpy.called); done(); }); }); }); test('does not collide with other component instances', function (done) { registerComponent('test2', { events: { foo: function (evt) { assert.equal(this.el.id, 'foo'); }, bar: function (evt) { assert.equal(this.el.id, 'bar'); } } }); el.id = 'foo'; el.setAttribute('test2', ''); const el2 = document.createElement('a-entity'); el2.id = 'bar'; el2.setAttribute('test2', ''); el.appendChild(el2); setTimeout(() => { el.emit('foo', null, false); el2.emit('bar', null, false); setTimeout(() => { done(); }); }); }); }); }); suite('registerComponent warnings', function () { var sceneEl; var script; setup(function (done) { var el = entityFactory(); setTimeout(() => { sceneEl = el.sceneEl; script = document.createElement('script'); script.innerHTML = `AFRAME.registerComponent('testorder', {});`; done(); }); }); teardown(function () { delete AFRAME.components.testorder; delete Component.registrationOrderWarnings.testorder; if (script && script.parentNode) { script.parentNode.removeChild(script); } }); test('does not throw warning if component registered in head', function (done) { assert.notOk(Component.registrationOrderWarnings.testorder, 'waht'); document.head.appendChild(script); setTimeout(() => { assert.notOk(Component.registrationOrderWarnings.testorder); done(); }); }); test('does not throw warning if component registered before scene', function (done) { assert.notOk(Component.registrationOrderWarnings.testorder, 'foo'); document.body.insertBefore(script, sceneEl); setTimeout(() => { assert.notOk(Component.registrationOrderWarnings.testorder); done(); }); }); test('does not throw warning if component registered after scene loaded', function (done) { assert.notOk(Component.registrationOrderWarnings.testorder, 'blah'); sceneEl.addEventListener('loaded', () => { document.body.appendChild(script); setTimeout(() => { assert.notOk(Component.registrationOrderWarnings.testorder); done(); }); }); }); test('throws warning if component registered after scene', function (done) { assert.notOk(Component.registrationOrderWarnings.testorder); document.body.appendChild(script); setTimeout(() => { assert.ok(Component.registrationOrderWarnings.testorder); done(); }); }); test('throws warning if component registered within scene', function (done) { assert.notOk(Component.registrationOrderWarnings.testorder); sceneEl.appendChild(script); setTimeout(() => { assert.ok(Component.registrationOrderWarnings.testorder); done(); }); }); });