/* global AFRAME, assert, process, sinon, setup, suite, teardown, test, HTMLElement */ var AEntity = require('core/a-entity'); var ANode = require('core/a-node'); var extend = require('utils').extend; var registerComponent = require('core/component').registerComponent; var components = require('core/component').components; var THREE = require('index').THREE; var helpers = require('../helpers'); var elFactory = helpers.elFactory; var mixinFactory = helpers.mixinFactory; var TestComponent = { schema: { a: {default: 0}, b: {default: 1} }, init: function () { }, update: function () { }, remove: function () { }, play: function () { }, pause: function () { }, tick: function () { }, tock: function () { } }; suite('a-entity', function () { var el = el; setup(function (done) { elFactory().then(_el => { el = _el; done(); }); }); teardown(function () { components.test = undefined; }); test('createdCallback', function () { assert.ok(el.isNode); assert.ok(el.isEntity); }); test('adds itself to parent when attached', function (done) { const parentEl = el; const el2 = document.createElement('a-entity'); el2.object3D = new THREE.Mesh(); parentEl.appendChild(el2); el2.addEventListener('loaded', function () { assert.equal(parentEl.object3D.children[0].uuid, el2.object3D.uuid); assert.ok(el2.parentEl); assert.ok(el2.parentNode); done(); }); }); test('emits event when child attached', function (done) { const parentEl = el; const el2 = document.createElement('a-entity'); el2.object3D = new THREE.Mesh(); parentEl.addEventListener('child-attached', function (event) { assert.equal(event.detail.el, el2); done(); }); parentEl.appendChild(el2); }); test('emits `componentremoved` event when element itself has been removed', function (done) { el.setAttribute('geometry', 'primitive:plane'); el.addEventListener('componentremoved', function (event) { assert.equal(event.detail.name, 'geometry'); done(); }); el.removeAttribute('geometry'); }); test('can be detached and re-attached safely', function (done) { const parentEl = el; const el2 = document.createElement('a-entity'); el2.object3D = new THREE.Mesh(); el2.setAttribute('geometry', 'primitive:plane'); el2.addEventListener('loaded', afterFirstAttachment); parentEl.appendChild(el2); function afterFirstAttachment () { el2.removeEventListener('loaded', afterFirstAttachment); assert.equal(parentEl.object3D.children[0].uuid, el2.object3D.uuid); assert.ok(el2.parentEl); assert.ok(el2.parentNode); assert.ok(el2.components.geometry); assert.isTrue(el2.hasLoaded); parentEl.removeChild(el2); setTimeout(afterDetachment); } function afterDetachment () { assert.equal(parentEl.object3D.children.length, 0); assert.notOk(el2.parentEl); assert.notOk(el2.parentNode); assert.ok(el2.components.geometry); assert.isFalse(el2.hasLoaded); el2.addEventListener('loaded', afterSecondAttachment); parentEl.appendChild(el2); } function afterSecondAttachment () { el2.removeEventListener('loaded', afterSecondAttachment); assert.equal(parentEl.object3D.children[0].uuid, el2.object3D.uuid); assert.ok(el2.parentEl); assert.ok(el2.parentNode); assert.ok(el2.components.geometry); assert.isTrue(el2.hasLoaded); done(); } }); suite('attachedCallback', function () { test('initializes 3D object', function (done) { elFactory().then(el => { assert.isDefined(el.object3D); done(); }); }); test('calls load method', function (done) { this.sinon.spy(AEntity.prototype, 'load'); elFactory().then(el => { sinon.assert.called(AEntity.prototype.load); done(); }); }); test('waits for children to load', function (done) { var scene = document.createElement('a-scene'); var entity = document.createElement('a-entity'); var entityChild1 = document.createElement('a-entity'); var entityChild2 = document.createElement('a-entity'); entity.appendChild(entityChild1); entity.appendChild(entityChild2); scene.appendChild(entity); document.body.appendChild(scene); entity.addEventListener('loaded', function () { assert.ok(entityChild1.hasLoaded); assert.ok(entityChild2.hasLoaded); document.body.removeChild(scene); done(); }); }); test('is playing when loaded', function (done) { const el2 = document.createElement('a-entity'); el2.addEventListener('loaded', function () { assert.ok(el2.isPlaying); done(); }); el.sceneEl.appendChild(el2); }); test('plays when entity is attached after scene load', function (done) { const el2 = document.createElement('a-entity'); this.sinon.spy(AEntity.prototype, 'play'); el2.addEventListener('play', function () { assert.ok(el2.hasLoaded); sinon.assert.called(AEntity.prototype.play); done(); }); el.sceneEl.appendChild(el2); }); test('waits for <a-assets>', function (done) { var assetsEl; var el; var sceneEl; // Create DOM. sceneEl = document.createElement('a-scene'); assetsEl = document.createElement('a-assets'); assetsEl.setAttribute('timeout', 20); el = document.createElement('a-entity'); el.addEventListener('loaded', function () { assert.ok(assetsEl.hasLoaded); assert.ok(el.hasLoaded); document.body.removeChild(sceneEl); done(); }); sceneEl.appendChild(assetsEl); sceneEl.appendChild(el); document.body.appendChild(sceneEl); ANode.prototype.load.call(assetsEl); }); }); suite('addState', function () { test('adds state', function () { el.states = []; el.addState('happy'); assert.ok(el.states[0] === 'happy'); }); test('it does not add an existing state', function () { el.states = ['happy']; el.addState('happy'); assert.ok(el.states.length === 1); }); }); suite('removeState', function () { test('removes existing state', function () { el.states = ['happy']; el.removeState('happy'); assert.ok(el.states.length === 0); }); test('removes non existing state', function () { el.states = ['happy']; el.removeState('sad'); assert.ok(el.states.length === 1); }); test('removes existing state among multiple states', function () { el.states = ['happy', 'excited']; el.removeState('excited'); assert.equal(el.states.length, 1); assert.ok(el.states[0] === 'happy'); }); }); suite('is', function () { test('returns true if entity is in the given state', function () { el.states = ['happy']; el.is('happy'); assert.ok(el.is('happy')); }); test('returns false if entity is not in the given state', function () { el.states = ['happy']; el.is('happy'); assert.ok(el.is('happy')); }); }); suite('setAttribute', function () { test('can set a component with a string', function () { var material; el.setAttribute('material', 'color: #F0F; metalness: 0.75'); material = el.getAttribute('material'); assert.equal(material.color, '#F0F'); assert.equal(material.metalness, 0.75); }); test('can set a component with an object', function () { var material; var value = {color: '#F0F', metalness: 0.75}; el.setAttribute('material', value); material = el.getAttribute('material'); assert.equal(material.color, '#F0F'); assert.equal(material.metalness, 0.75); }); test('can clobber component attributes with an object and flag', function () { var material; el.setAttribute('material', 'color: #F0F; roughness: 0.25'); el.setAttribute('material', {color: '#000'}, true); material = el.getAttribute('material'); assert.equal(material.color, '#000'); assert.equal(material.roughness, 0.5); assert.equal(el.getDOMAttribute('material').roughness, undefined); }); test('can set a single component via a single attribute', function () { el.setAttribute('material', 'color', '#F0F'); assert.equal(el.getAttribute('material').color, '#F0F'); }); test('can update a single component attribute', function () { var material; el.setAttribute('material', 'color: #F0F; roughness: 0.25'); assert.equal(el.getAttribute('material').roughness, 0.25); el.setAttribute('material', 'roughness', 0.75); material = el.getAttribute('material'); assert.equal(material.color, '#F0F'); assert.equal(material.roughness, 0.75); }); test('can update a single component attribute with a string', function () { var material; el.setAttribute('material', 'color: #F0F; roughness: 0.25'); assert.equal(el.getAttribute('material').roughness, 0.25); el.setAttribute('material', 'roughness: 0.75'); material = el.getAttribute('material'); assert.equal(material.color, '#F0F'); assert.equal(material.roughness, 0.75); }); test('can clobber component attributes with a string and flag', function () { var material; el.setAttribute('material', 'color: #F0F; roughness: 0.25'); el.setAttribute('material', 'color: #000', true); material = el.getAttribute('material'); assert.equal(material.color, '#000'); assert.equal(material.roughness, 0.5); assert.equal(el.getDOMAttribute('material').roughness, undefined); }); test('transforms object to string before setting on DOM', function () { var positionObj = {x: 10, y: 20, z: 30}; el.setAttribute('position', positionObj); assert.ok(el.outerHTML.indexOf('position=""') !== -1); }); test('can update component data', function () { el.setAttribute('position', '10 20 30'); assert.shallowDeepEqual(el.getAttribute('position'), {x: 10, y: 20, z: 30}); el.setAttribute('position', {x: 30, y: 20, z: 10}); assert.shallowDeepEqual(el.getAttribute('position'), {x: 30, y: 20, z: 10}); }); test('can partially update multiple properties of a component', function () { var geometry; el.setAttribute('geometry', {primitive: 'box'}); el.setAttribute('geometry', {depth: 2.5}); el.setAttribute('geometry', {height: 1.5, width: 3}); geometry = el.getAttribute('geometry'); assert.equal(geometry.primitive, 'box'); assert.equal(geometry.depth, 2.5); assert.equal(geometry.height, 1.5); assert.equal(geometry.width, 3); }); test('partial updates of array properties assign by reference', function () { // Arrays are assigned by reference and mutable. var sourceArray = [1, 2, 3]; registerComponent('test', { schema: {array: {type: 'array'}} }); el.setAttribute('test', {array: sourceArray}); assert.strictEqual(el.getAttribute('test').array, sourceArray); }); test('partial updates of array-type properties do trigger update', function () { // Updates to array do not trigger update handler. var updateSpy; registerComponent('test', { schema: {array: {type: 'array'}}, update: function () { /* no-op */ } }); el.setAttribute('test', {array: [1, 2, 3]}); updateSpy = this.sinon.spy(el.components.test, 'update'); el.setAttribute('test', {array: [4, 5, 6]}); assert.ok(updateSpy.called); }); test('can partially update vec3', function () { el.setAttribute('position', {y: 20}); assert.shallowDeepEqual(el.getAttribute('position'), {x: 0, y: 20, z: 0}); }); test('can update component property with asymmetrical property type', function () { registerComponent('test', { schema: { asym: { default: 1, parse: function (value) { // When setAttribute re-gathers the component data, it should not double-parse. if (value === 2) { throw new Error('This should be 1'); } return value + 1; } }, other: { default: 5 } } }); el.setAttribute('test', 'asym', 1); el.setAttribute('test', 'other', 2); }); test('only stores modified properties in attribute cache', function () { el.setAttribute('geometry', {primitive: 'box'}); assert.deepEqual(el.components.geometry.attrValue, {primitive: 'box'}); el.setAttribute('geometry', {primitive: 'sphere', radius: 10}); assert.deepEqual(el.components.geometry.attrValue, {primitive: 'sphere', radius: 10}); }); test('only caches modified properties when changing schema only', function () { el.setAttribute('geometry', {primitive: 'box'}); assert.deepEqual(el.components.geometry.attrValue, {primitive: 'box'}); el.setAttribute('geometry', {primitive: 'sphere', radius: 10}); assert.deepEqual(el.components.geometry.attrValue, {primitive: 'sphere', radius: 10}); const geometry = el.getAttribute('geometry'); assert.equal(geometry.primitive, 'sphere'); assert.equal(geometry.radius, 10); assert.notOk(geometry.depth); assert.notOk(geometry.height); assert.notOk(geometry.width); }); test('parses individual properties when passing object', function (done) { AFRAME.registerComponent('foo', { schema: { bar: {type: 'asset'}, baz: {type: 'asset'} }, init: function () { assert.equal(this.data.bar, 'test.png'); assert.equal(this.data.baz, 'test.jpg'); delete AFRAME.components.foo; done(); } }); el.setAttribute('foo', { bar: 'url(test.png)', baz: 'url(test.jpg)' }); }); test('merges updates with previous data', function (done) { el.addEventListener('child-attached', evt => { el = evt.detail.el; el.addEventListener('loaded', evt => { var geometry; var setObj; assert.shallowDeepEqual(el.components.geometry.attrValue, {primitive: 'box', width: 5}); // First setAttribute. setObj = {depth: 10, height: 20}; el.setAttribute('geometry', setObj); geometry = el.getAttribute('geometry'); assert.equal(geometry.depth, 10); assert.equal(geometry.height, 20); assert.equal(geometry.width, 5, 'First setAttribute'); // Second setAttribute. el.setAttribute('geometry', {depth: 20, height: 10}); geometry = el.getAttribute('geometry'); assert.shallowDeepEqual(el.components.geometry.attrValue, { depth: 20, height: 10, primitive: 'box', width: 5 }, 'Second attrValue'); assert.equal(geometry.width, 5, 'Second setAttribute'); done(); }); }); // Initial data. el.innerHTML = '<a-entity geometry="primitive: box; width: 5">'; }); }); suite('flushToDOM', function () { test('updates DOM attributes', function () { var material; var materialStr = 'color: #F0F; metalness: 0.75'; el.setAttribute('material', materialStr); material = HTMLElement.prototype.getAttribute.call(el, 'material'); assert.equal(material, ''); el.flushToDOM(); material = HTMLElement.prototype.getAttribute.call(el, 'material'); assert.equal(material, materialStr); }); test('updates DOM attributes of a multiple component', function () { var soundAttrValue; var soundStr = 'src: url(mysoundfile.mp3); autoplay: true'; el.setAttribute('sound__1', {'src': 'url(mysoundfile.mp3)', autoplay: true}); soundAttrValue = HTMLElement.prototype.getAttribute.call(el, 'sound__1'); assert.equal(soundAttrValue, ''); el.flushToDOM(); soundAttrValue = HTMLElement.prototype.getAttribute.call(el, 'sound__1'); assert.equal(soundAttrValue, soundStr); }); test('updates DOM attributes recursively', function (done) { var childEl = document.createElement('a-entity'); var childMaterialStr = 'color:pink'; var materialAttr; var materialStr = 'color: #F0F; metalness: 0.75'; childEl.addEventListener('loaded', function () { materialAttr = HTMLElement.prototype.getAttribute.call(el, 'material'); assert.equal(materialAttr, null); materialAttr = HTMLElement.prototype.getAttribute.call(childEl, 'material'); assert.equal(materialAttr, null); el.setAttribute('material', materialStr); childEl.setAttribute('material', childMaterialStr); el.flushToDOM(true); materialAttr = HTMLElement.prototype.getAttribute.call(el, 'material'); assert.equal(materialAttr, 'color: #F0F; metalness: 0.75'); materialAttr = HTMLElement.prototype.getAttribute.call(childEl, 'material'); assert.equal(childMaterialStr, 'color:pink'); done(); }); el.appendChild(childEl); }); }); suite('detachedCallback', function () { test('removes itself from entity parent', function (done) { elFactory().then(parentEl => { const el = document.createElement('a-entity'); parentEl.appendChild(el); parentEl.removeChild(el); setTimeout(function () { assert.equal(parentEl.object3D.children.length, 0); assert.notOk(el.parentEl); assert.notOk(el.parentNode); done(); }); }); }); test('removes itself from scene parent', function (done) { const sceneEl = el.parentNode; assert.notEqual(sceneEl.object3D.children.indexOf(el.object3D), -1); sceneEl.removeChild(el); setTimeout(function () { assert.equal(sceneEl.object3D.children.indexOf(el.object3D), -1); done(); }); }); test('properly detaches components', function (done) { var parentEl = el.parentNode; components.test = undefined; registerComponent('test', TestComponent); el.setAttribute('test', ''); assert.notEqual(el.sceneEl.behaviors.tick.indexOf(el.components.test), -1); assert.notEqual(el.sceneEl.behaviors.tock.indexOf(el.components.test), -1); parentEl.removeChild(el); process.nextTick(function () { assert.ok('test' in el.components); assert.equal(el.sceneEl.behaviors.tick.indexOf(el.components.test), -1); assert.equal(el.sceneEl.behaviors.tock.indexOf(el.components.test), -1); done(); }); }); test('handles detaching with with uninitialized components', function () { var box = document.createElement('a-entity'); box.setAttribute('geometry', {primitive: 'box'}); el.sceneEl.appendChild(box); el.sceneEl.removeChild(box); // Just check it doesn't error. }); }); suite('load', function () { test('does not try to load if not attached', function () { var el = document.createElement('a-entity'); var nodeLoadSpy = this.sinon.spy(ANode.prototype, 'load'); el.load(); assert.notOk(nodeLoadSpy.called); }); test('does not try to initialize during load callback if not attached', function (done) { const el = document.createElement('a-entity'); const childEl = document.createElement('a-entity'); el.setAttribute('id', 'parent'); childEl.setAttribute('id', 'child'); el.parentEl = true; el.appendChild(childEl); el.load(); el.parentEl = null; childEl.emit('loaded'); let nodeLoadSpy = this.sinon.spy(AEntity.prototype, 'updateComponents'); setTimeout(function () { assert.notOk(nodeLoadSpy.called); done(); }); }); test('wait for all the children nodes that are not yet nodes to load', function (done) { var a = document.createElement('a-entity'); var b = document.createElement('a-entity'); var aLoaded = false; var bLoaded = false; el.appendChild(a); el.appendChild(b); process.nextTick(function () { a.isNode = false; a.hasLoaded = false; b.isNode = false; b.hasLoaded = false; el.hasLoaded = false; el.addEventListener('loaded', function () { assert.ok(el.hasLoaded); assert.ok(aLoaded); assert.ok(bLoaded); done(); }); a.addEventListener('loaded', function () { aLoaded = true; }); b.addEventListener('loaded', function () { bLoaded = true; }); el.load(); assert.notOk(el.hasLoaded); a.load(); b.load(); }); }); test('wait for all the children primitives that are not yet nodes to load', function (done) { var a = document.createElement('a-sphere'); var b = document.createElement('a-box'); var aLoaded = false; var bLoaded = false; el.appendChild(a); el.appendChild(b); process.nextTick(function () { a.isNode = false; a.hasLoaded = false; b.isNode = false; b.hasLoaded = false; el.hasLoaded = false; el.addEventListener('loaded', function () { assert.ok(el.hasLoaded); assert.ok(aLoaded); assert.ok(bLoaded); done(); }); a.addEventListener('loaded', function () { aLoaded = true; }); b.addEventListener('loaded', function () { bLoaded = true; }); el.load(); assert.notOk(el.hasLoaded); a.load(); b.load(); }); }); }); suite('getDOMAttribute', function () { test('returns parsed component data', function () { var componentData; el.setAttribute('geometry', 'primitive: box; width: 5'); componentData = el.getDOMAttribute('geometry'); assert.equal(componentData.width, 5); assert.notOk('height' in componentData); }); test('returns empty object if component is at defaults', function () { el.setAttribute('material', ''); assert.shallowDeepEqual(el.getDOMAttribute('material'), {}); }); test('returns partial component data', function () { var componentData; el.setAttribute('geometry', 'primitive: box; width: 5'); componentData = el.getDOMAttribute('geometry'); assert.equal(componentData.width, 5); assert.notOk('height' in componentData); }); test('falls back to HTML getAttribute if not a component', function () { el.setAttribute('class', 'pied piper'); assert.equal(el.getDOMAttribute('class'), 'pied piper'); }); test('retrieves data from a multiple component', function () { el.setAttribute('sound__1', {'src': 'url(mysoundfile.mp3)', autoplay: true}); el.setAttribute('sound__2', {'src': 'url(mysoundfile.mp3)', autoplay: false}); assert.ok(el.getDOMAttribute('sound__1')); assert.ok(el.getDOMAttribute('sound__2')); assert.notOk(el.getDOMAttribute('sound')); assert.equal(el.getDOMAttribute('sound__1').autoplay, true); }); test('retrieves default value for single property component when ' + 'the element attribute is set to empty string', function () { el.sceneEl.setAttribute('debug', ''); assert.equal(el.sceneEl.getDOMAttribute('debug'), true); }); }); suite('getChildEntities', function () { test('returns child entities', function (done) { var entity = document.createElement('a-entity'); var animationChild = document.createElement('a-animation'); var entityChild1 = document.createElement('a-entity'); var entityChild2 = document.createElement('a-entity'); entity.appendChild(animationChild); entity.appendChild(entityChild1); entity.appendChild(entityChild2); entity.addEventListener('loaded', function () { var childEntities = entity.getChildEntities(); assert.equal(childEntities.length, 2); assert.equal(childEntities.indexOf(animationChild), -1); done(); }); document.body.appendChild(entity); }); }); suite('getObject3D', function () { test('returns requested object3D', function () { el.setAttribute('geometry', 'primitive: box; width: 5'); assert.ok(el.getObject3D('mesh')); }); test('it returns undefined for a non existing object3D', function () { assert.notOk(el.getObject3D('dummy')); }); }); suite('setObject3D', function () { test('sets an object3D for a given type', function () { var object3D = new THREE.Group(); el.setObject3D('mesh', object3D); assert.equal(el.getObject3D('mesh'), object3D); assert.equal(object3D.el, el); }); test('binds el to object3D children', function () { var parentObject = new THREE.Object3D(); var childObject = new THREE.Object3D(); parentObject.add(childObject); el.setObject3D('mesh', parentObject); assert.equal(el.getObject3D('mesh').children[0].el, el); }); test('emits an event', function (done) { var mesh = new THREE.Mesh(); el.addEventListener('object3dset', evt => { assert.equal(evt.detail.object, mesh); assert.equal(evt.detail.type, 'mesh'); done(); }); el.setObject3D('mesh', mesh); }); test('throws an error if object is not a THREE.Object3D', function () { assert.throws(() => { el.setObject3D('mesh', function () {}); }, Error); }); }); suite('removeObject3D', () => { test('removes object3D', function () { el.setObject3D('mesh', new THREE.Mesh()); el.removeObject3D('mesh', new THREE.Mesh()); assert.notOk(el.getObject3D('mesh')); assert.notOk('mesh' in el.object3DMap); }); test('handles trying to remove object3D that is not set', function () { var removeSpy = this.sinon.spy(el.object3D, 'remove'); el.removeObject3D('foo'); assert.notOk(removeSpy.called); }); test('emits an event', function (done) { el.setObject3D('mesh', new THREE.Mesh()); el.addEventListener('object3dremove', evt => { assert.equal(evt.detail.type, 'mesh'); done(); }); el.removeObject3D('mesh'); }); }); suite('getAttribute', function () { test('returns full component data', function () { var componentData; el.setAttribute('geometry', 'primitive: box; width: 5'); componentData = el.getAttribute('geometry'); assert.equal(componentData.primitive, 'box'); assert.equal(componentData.width, 5); assert.ok('height' in componentData); }); test('returns full data of a multiple component', function () { var componentData; el.setAttribute('sound__test', 'src: url(mysoundfile.mp3)'); componentData = el.getAttribute('sound__test'); assert.equal(componentData.src, 'mysoundfile.mp3'); assert.equal(componentData.autoplay, false); assert.ok('loop' in componentData); }); test('falls back to HTML getAttribute if not a component', function () { el.setAttribute('class', 'pied piper'); assert.equal(el.getAttribute('class'), 'pied piper'); }); test('returns the component data object', function () { var data; el.setAttribute('geometry', {primitive: 'sphere', radius: 10}); data = el.getAttribute('geometry'); assert.ok(el.components.geometry.data === data); }); test('returns position previously set with setAttribute', function () { el.setAttribute('position', {x: 1, y: 2, z: 3}); assert.shallowDeepEqual(el.getAttribute('position'), {x: 1, y: 2, z: 3}); }); test('returns position set by modifying the object3D position', function () { el.object3D.position.set(1, 2, 3); assert.shallowDeepEqual(el.getAttribute('position'), {x: 1, y: 2, z: 3}); }); test('returns rotation previously set with setAttribute', function () { el.setAttribute('rotation', {x: 10, y: 45, z: 50}); assert.shallowDeepEqual(el.getAttribute('rotation'), {x: 10, y: 45, z: 50}); }); test('returns rotation previously set by modifying the object3D rotation', function () { el.object3D.rotation.set(Math.PI, Math.PI / 2, Math.PI / 4); assert.shallowDeepEqual(el.getAttribute('rotation'), {x: 180, y: 90, z: 45}); }); test('returns rotation previously set by modifying the object3D quaternion', function () { var quaternion = new THREE.Quaternion(); var euler = new THREE.Euler(); var rotation; euler.order = 'YXZ'; euler.set(Math.PI / 2, Math.PI, 0); quaternion.setFromEuler(euler); el.object3D.quaternion.copy(quaternion); rotation = el.getAttribute('rotation'); assert.equal(Math.round(rotation.x), 90); }); test('returns scale previously set with setAttribute', function () { el.setAttribute('scale', {x: 1, y: 2, z: 3}); assert.shallowDeepEqual(el.getAttribute('scale'), {x: 1, y: 2, z: 3}); }); test('returns scale set by modifying the object3D scale', function () { el.object3D.scale.set(1, 2, 3); assert.shallowDeepEqual(el.getAttribute('scale'), {x: 1, y: 2, z: 3}); }); test('returns visible previously set with setAttribute', function () { el.setAttribute('visible', false); assert.equal(el.getAttribute('visible'), false); el.setAttribute('visible', true); assert.equal(el.getAttribute('visible'), true); }); test('returns visible set by modifying the object3D visible', function () { el.object3D.visible = false; assert.equal(el.getAttribute('visible'), false); el.object3D.visible = true; assert.equal(el.getAttribute('visible'), true); }); }); suite('removeAttribute', function () { test('can remove a normal attribute', function () { el.setAttribute('id', 'id-entity'); assert.equal(el.getAttribute('id'), 'id-entity'); el.removeAttribute('id'); assert.notOk(el.getAttribute('id')); }); test('can remove a component', function () { el.setAttribute('material', 'color: #F0F'); assert.ok(el.components.material); el.removeAttribute('material'); assert.equal(el.getAttribute('material'), null); assert.notOk(el.components.material); }); test('can remove a multiple component', function () { el.setAttribute('sound__test', 'src: url(mysoundfile.mp3)'); assert.ok(el.components.sound__test); el.removeAttribute('sound__test'); assert.equal(el.getAttribute('sound__test'), null); assert.notOk(el.components.sound__test); }); test('can remove mixed-in component', function () { var mixinId = 'geometry'; mixinFactory(mixinId, {geometry: 'primitive: box'}); el.setAttribute('mixin', mixinId); el.setAttribute('geometry', 'primitive: sphere'); assert.ok('geometry' in el.components); el.removeAttribute('geometry'); assert.equal(el.getAttribute('geometry'), null); // Geometry still exists since it is mixed in. assert.notOk('geometry' in el.components); }); test('resets a component property', function () { el.setAttribute('material', 'color: #F0F'); assert.equal(el.getAttribute('material').color, '#F0F'); el.removeAttribute('material', 'color'); assert.equal(el.getAttribute('material').color, '#FFF'); }); test('does not remove mixed-in component', function () { mixinFactory('foo', {position: '1 2 3'}); mixinFactory('bar', {scale: '1 2 3'}); el.setAttribute('mixin', 'foo bar'); assert.shallowDeepEqual(el.getAttribute('position'), {x: 1, y: 2, z: 3}, 'Position mixin'); assert.shallowDeepEqual(el.getAttribute('scale'), {x: 1, y: 2, z: 3}, 'Scale mixin'); el.removeAttribute('mixin'); assert.shallowDeepEqual(el.getAttribute('position'), {x: 0, y: 0, z: 0}, 'Position without mixin'); assert.shallowDeepEqual(el.getAttribute('scale'), {x: 1, y: 1, z: 1}, 'Scale without mixin'); }); }); suite('initComponent', function () { test('initializes component', function () { el.initComponent('material', 'color: #F0F; transparent: true', false); assert.ok(el.components.material); }); test('does not initialized non-registered component', function () { var nativeSetAttribute = HTMLElement.prototype.setAttribute; this.sinon.stub(el, 'setAttribute', nativeSetAttribute); el.setAttribute('fake-component', 'color: #F0F;'); el.initComponent('fake-component'); assert.notOk(el.components.fakeComponent); }); test('initializes dependency component and can set attribute', function () { el.initComponent('material', '', true); assert.shallowDeepEqual(el.getAttribute('material'), {}); }); test('initializes defined dependency component with setAttributes', function (done) { const el2 = document.createElement('a-entity'); delete components.root; registerComponent('root', { dependencies: ['dependency'] }); registerComponent('dependency', { schema: {foo: {type: 'string'}}, init: function () { assert.equal(this.data.foo, 'bar'); delete components.root; delete components.dependency; done(); } }); // Create entity all at once with defined dependency component and component. el2.setAttribute('dependency', 'foo: bar'); el2.setAttribute('root', ''); el.appendChild(el2); }); test('initializes defined dependency component with HTML', function (done) { delete components.root; registerComponent('root', { dependencies: ['dependency'] }); registerComponent('dependency', { schema: {foo: {type: 'string'}}, init: function () { assert.equal(this.data.foo, 'bar'); delete components.root; delete components.dependency; done(); } }); el.innerHTML = '<a-entity root dependency="foo: bar">'; }); test('initializes defined dependency component with null data w/ HTML', function (done) { delete components.root; registerComponent('root', { dependencies: ['dependency'], init: function () { assert.equal(this.el.components.dependency.data.foo, 'bar'); delete components.root; delete components.dependency; done(); } }); registerComponent('dependency', { schema: {foo: {default: 'bar'}}, init: function () { assert.equal(this.data.foo, 'bar'); } }); el.innerHTML = '<a-entity root dependency>'; }); test('initializes defined dependency component with HTML reverse', function (done) { registerComponent('root', { dependencies: ['dependency'] }); registerComponent('dependency', { schema: {foo: {type: 'string'}}, init: function () { assert.equal(this.data.foo, 'bar'); delete components.root; delete components.dependency; done(); } }); el.innerHTML = '<a-entity dependency="foo: bar" root>'; }); test('can access dependency component data', function (done) { delete components.root; registerComponent('root', { dependencies: ['dependency'], init: function () { assert.equal(this.el.components.dependency.data.foo, 'bar'); assert.equal(this.el.components.dependency.qux, 'baz'); delete components.root; delete components.dependency; done(); } }); registerComponent('dependency', { schema: {foo: {type: 'string'}}, init: function () { this.qux = 'baz'; } }); el.innerHTML = '<a-entity dependency="foo: bar" root>'; }); test('initializes dependency component and current attribute honored', function () { var materialAttribute = 'color: #F0F; transparent: true'; var nativeSetAttribute = HTMLElement.prototype.setAttribute; var nativeGetAttribute = HTMLElement.prototype.getAttribute; this.sinon.stub(el, 'setAttribute', nativeSetAttribute); this.sinon.stub(el, 'getAttribute', nativeGetAttribute); el.setAttribute('material', materialAttribute); el.initComponent('material', '', true); assert.equal(el.getAttribute('material'), materialAttribute); }); test('does not initialize with id if the component is not multiple', function () { assert.throws(function setAttribute () { el.setAttribute('geometry__1', {primitive: 'box'}); }, Error); assert.notOk(el.components.geometry__1); }); test('initializes components with id if the component opts into multiple', function () { el.setAttribute('sound__1', {'src': 'url(mysoundfile.mp3)'}); el.setAttribute('sound__2', {'src': 'url(mysoundfile.mp3)'}); assert.ok(el.components.sound__1); assert.ok(el.components.sound__2); assert.ok(el.components.sound__1 instanceof components.sound.Component); assert.ok(el.components.sound__2 instanceof components.sound.Component); }); test('waits for DOM data to init before setAttribute data', function (done) { // Test component. AFRAME.registerComponent('test', { schema: { foo: {default: 5}, bar: {default: 'red'}, qux: {default: true} }, init: function () { var data = this.data; assert.equal(data.foo, 10); assert.equal(data.bar, 'red'); assert.equal(data.qux, true); }, update: function (oldData) { var data = this.data; if (oldData && Object.keys(oldData).length) { // Second update via setAttribute. assert.equal(data.foo, 10); assert.equal(data.bar, 'orange'); assert.equal(data.qux, true); delete AFRAME.components['test-setter']; done(); } else { // First update via initialization. assert.equal(data.foo, 10); assert.equal(data.bar, 'red'); assert.equal(data.qux, true); } } }); // Component that will do the setAttribute, without dependency. AFRAME.registerComponent('test-setter', { init: function () { this.el.setAttribute('test', {bar: 'orange'}); } }); // Create the entity. el.innerHTML = '<a-entity test-setter test="foo: 10">'; }); }); suite('removeComponent', function () { test('removes a behavior', function () { var sceneEl = el.sceneEl; var component; el.play(); el.setAttribute('raycaster', ''); component = el.components['raycaster']; assert.notEqual(sceneEl.behaviors.tick.indexOf(component), -1); el.removeAttribute('raycaster'); assert.equal(sceneEl.behaviors.tick.indexOf(component), -1); }); test('waits for component to initialize', function (done) { var box = document.createElement('a-entity'); var component; var removeSpy; box.setAttribute('geometry', {primitive: 'box'}); component = box.components.geometry; removeSpy = this.sinon.stub(component, 'remove', () => {}); box.removeComponent('geometry'); assert.notOk(removeSpy.called); component.initialized = true; box.emit('componentinitialized', {name: 'geometry'}); setTimeout(() => { assert.ok(removeSpy.called); done(); }); }); }); suite('updateComponent', function () { test('initialize a component', function () { assert.equal(el.components.material, undefined); el.updateComponent('material', {color: 'blue'}); assert.equal(el.getAttribute('material').color, 'blue'); }); test('update an existing component', function () { var component = new components.material.Component(el, {color: 'red'}); el.components.material = component; assert.equal(el.getAttribute('material').color, 'red'); el.updateComponent('material', {color: 'blue'}); assert.equal(component, el.components.material); assert.equal(el.getAttribute('material').color, 'blue'); }); test('removes a component', function () { el.components.material = new components.material.Component(el, {color: 'red'}); assert.equal(el.getAttribute('material').color, 'red'); el.components.material.attrValue = null; el.updateComponent('material', null); assert.equal(el.components.material, undefined); }); }); suite('updateComponents', function () { setup(function (done) { this.child = el.appendChild(document.createElement('a-entity')); this.child.addEventListener('loaded', function () { done(); }); }); test('nested calls do not leak components to children', function () { registerComponent('test', { init: function () { var children = el.getChildEntities(); if (children.length) { children[0].setAttribute('mixin', 'addGeometry'); } } }); mixinFactory('addTest', {test: ''}); mixinFactory('addGeometry', {geometry: 'shape: sphere'}); el.setAttribute('mixin', 'addTest'); assert.notOk(this.child.components['test']); }); test('initializes object3d components first', function (done) { registerComponent('test', { init: function () { var object3D = this.el.object3D; assert.equal(object3D.position.y, 5); assert.equal(object3D.visible, false); done(); } }); el.innerHTML = '<a-entity class="test" test position="0 5 0" visible="false"></a-entity'; }); }); suite('applyMixin', function () { test('combines mixin and element components with a dynamic schema', function () { var mixinId = 'material'; mixinFactory(mixinId, {material: 'shader: flat'}); el.setAttribute('mixin', mixinId); el.setAttribute('material', 'color: red'); assert.shallowDeepEqual(el.getAttribute('material'), {shader: 'flat', color: 'red'}); }); test('merges component properties from mixin', function (done) { mixinFactory('box', {geometry: 'primitive: box'}); process.nextTick(function () { el.setAttribute('mixin', 'box'); el.setAttribute('geometry', {depth: 5, height: 5, width: 5}); assert.shallowDeepEqual(el.getAttribute('geometry'), { depth: 5, height: 5, primitive: 'box', width: 5 }); done(); }); }); test('applies default vec3 component from mixin', function () { var mixinId = 'position'; mixinFactory(mixinId, {position: '1 2 3'}); el.setAttribute('mixin', mixinId); assert.shallowDeepEqual(el.getAttribute('position'), {x: 1, y: 2, z: 3}); }); test('does not override defined property', function () { el.setAttribute('material', {color: 'red'}); mixinFactory('blue', {material: 'color: blue'}); el.setAttribute('mixin', 'blue'); assert.shallowDeepEqual(el.getAttribute('material').color, 'red'); }); test('does not override defined property on subsequent updates', function () { el.setAttribute('material', {color: 'red'}); mixinFactory('blue', {material: 'color: blue'}); mixinFactory('opacity', {opacity: 0.25}); el.setAttribute('mixin', 'blue'); assert.equal(el.getAttribute('material').color, 'red'); el.setAttribute('material', {opacity: 0.5}); assert.equal(el.getAttribute('material').color, 'red'); assert.equal(el.getAttribute('material').opacity, 0.5); el.setAttribute('mixin', 'blue opacity'); assert.equal(el.getAttribute('material').color, 'red'); assert.equal(el.getAttribute('material').opacity, 0.5); el.setAttribute('material', {side: 'back'}); assert.equal(el.getAttribute('material').color, 'red'); assert.equal(el.getAttribute('material').opacity, 0.5); }); test('applies multiple components from mixin', function () { var mixinId = 'sound'; var soundUrl = 'mysoundfile.mp3'; mixinFactory(mixinId, { sound__1: 'src: url(' + soundUrl + '); autoplay: false', sound__2: 'src: url(' + soundUrl + '); autoplay: true' }); el.setAttribute('mixin', mixinId); assert.equal(el.getAttribute('sound__1').src, soundUrl); assert.equal(el.getAttribute('sound__1').autoplay, false); assert.equal(el.getAttribute('sound__2').src, soundUrl); assert.equal(el.getAttribute('sound__2').autoplay, true); }); test('applies mixin ids separated with spaces, tabs, and new lines', function () { mixinFactory('material', {material: 'shader: flat'}); mixinFactory('position', {position: '1 2 3'}); mixinFactory('rotation', {rotation: '10 20 45'}); el.setAttribute('mixin', ' material\t\nposition \t rotation\n '); el.setAttribute('material', 'color: red'); assert.equal(el.mixinEls.length, 3); assert.shallowDeepEqual(el.getAttribute('material'), {shader: 'flat', color: 'red'}); assert.shallowDeepEqual(el.getAttribute('position'), {x: 1, y: 2, z: 3}); assert.shallowDeepEqual(el.getAttribute('rotation'), {x: 10, y: 20, z: 45}); }); test('clear mixin', function () { mixinFactory('material', {material: 'shader: flat'}); mixinFactory('position', {position: '1 2 3'}); el.setAttribute('mixin', 'material position'); el.setAttribute('material', 'color: red'); assert.shallowDeepEqual(el.getAttribute('material'), {shader: 'flat', color: 'red'}); assert.shallowDeepEqual(el.getAttribute('position'), {x: 1, y: 2, z: 3}); assert.equal(el.mixinEls.length, 2); el.setAttribute('mixin', ''); assert.shallowDeepEqual(el.getAttribute('material'), {color: 'red'}); assert.shallowDeepEqual(el.getAttribute('position'), {x: 0, y: 0, z: 0}); assert.equal(el.mixinEls.length, 0); }); test('remove mixin', function (done) { registerComponent('test', { remove: function () { // Should be called or else will timeout. done(); } }); mixinFactory('test', {test: ''}); setTimeout(() => { el.setAttribute('mixin', 'test'); assert.equal(el.mixinEls.length, 1); el.setAttribute('mixin', ''); assert.equal(el.mixinEls.length, 0); }); }); /** * Fixed a weird case where attributeChangedCallback on mixin was fired during scene init. * That fired mixinUpdate before the entity was loaded (and el.sceneEl was undefined). * And tried to update components before the entity was ready. * This test mimics that state where mixinUpdate called when entity not fully loaded but * component is still initializing. */ test('wait for entity to load on mixin update', function (done) { const TestComponent = AFRAME.registerComponent('test', { update: function () { assert.ok(this.el.sceneEl); done(); } }); elFactory().then(someEl => { const sceneEl = someEl.sceneEl; const mixin = document.createElement('a-mixin'); mixin.setAttribute('id', 'foo'); mixin.setAttribute('test', ''); sceneEl.appendChild(mixin); setTimeout(() => { const el = document.createElement('a-entity'); el.setAttribute('mixin', 'foo'); el.components.test = new TestComponent(el, {}, ''); el.components.test.oldData = 'foo'; el.mixinUpdate('foo'); sceneEl.appendChild(el); }); }); }); }); }); suite('a-entity component lifecycle management', function () { var el; setup(function (done) { elFactory().then(_el => { el = _el; components.test = undefined; this.TestComponent = registerComponent('test', TestComponent); done(); }); }); teardown(function () { components.test = undefined; }); test('calls init on component attach', function () { var TestComponent = this.TestComponent.prototype; this.sinon.spy(TestComponent, 'init'); sinon.assert.notCalled(TestComponent.init); el.setAttribute('test', ''); sinon.assert.called(TestComponent.init); }); test('calls init only once', function () { var TestComponent = this.TestComponent.prototype; this.sinon.spy(TestComponent, 'init'); el.setAttribute('test', ''); sinon.assert.calledOnce(TestComponent.init); el.setAttribute('test', 'a: 5'); sinon.assert.calledOnce(TestComponent.init); }); test('calls update on component attach', function () { var TestComponent = this.TestComponent.prototype; this.sinon.spy(TestComponent, 'update'); sinon.assert.notCalled(TestComponent.update); el.setAttribute('test', ''); sinon.assert.called(TestComponent.update); }); test('calls update on setAttribute', function () { var TestComponent = this.TestComponent.prototype; this.sinon.spy(TestComponent, 'update'); el.setAttribute('test', ''); sinon.assert.calledOnce(TestComponent.update); el.setAttribute('test', 'a: 5'); sinon.assert.calledTwice(TestComponent.update); }); test('does not call update on setAttribute if no change', function () { var TestComponent = this.TestComponent.prototype; this.sinon.spy(TestComponent, 'update'); el.setAttribute('test', 'a: 3'); sinon.assert.calledOnce(TestComponent.update); el.setAttribute('test', 'a: 3'); sinon.assert.calledOnce(TestComponent.update); }); test('parses if mix of unparsed data and reused object from setAttribute', function (done) { var componentData; var childEl; var parentEl = el; var updateObj = {b: 5}; AFRAME.registerComponent('setter', { init: function () { setTimeout(() => { this.el.setAttribute('test', updateObj); updateObj.b = 10; this.el.setAttribute('test', updateObj); }); } }); parentEl.innerHTML = '<a-entity setter test="a: 3">'; setTimeout(() => { childEl = parentEl.children[0]; componentData = childEl.getAttribute('test'); assert.strictEqual(componentData.a, 3); assert.strictEqual(componentData.b, 10); delete AFRAME.components.setter; done(); }, 50); }); test('calls remove on removeAttribute', function () { var TestComponent = this.TestComponent.prototype; this.sinon.spy(TestComponent, 'remove'); el.setAttribute('test', ''); sinon.assert.notCalled(TestComponent.remove); el.removeAttribute('test'); sinon.assert.called(TestComponent.remove); }); test('calls pause on entity pause', function () { var TestComponent = this.TestComponent.prototype; this.sinon.spy(TestComponent, 'pause'); el.play(); el.setAttribute('test', ''); sinon.assert.notCalled(TestComponent.pause); el.pause(); sinon.assert.called(TestComponent.pause); }); test('calls play on entity play', function () { var TestComponent = this.TestComponent.prototype; el.pause(); this.sinon.spy(TestComponent, 'play'); el.setAttribute('test', ''); sinon.assert.notCalled(TestComponent.play); el.play(); sinon.assert.called(TestComponent.play); }); test('removes tick from scene behaviors on entity pause', function () { var testComponentInstance; el.setAttribute('test', ''); testComponentInstance = el.components.test; assert.notEqual(el.sceneEl.behaviors.tick.indexOf(testComponentInstance), -1); el.pause(); assert.equal(el.sceneEl.behaviors.tick.indexOf(testComponentInstance), -1); }); test('adds tick to scene behaviors on entity play', function () { var testComponentInstance; el.setAttribute('test', ''); testComponentInstance = el.components.test; el.sceneEl.behaviors.tick = []; assert.equal(el.sceneEl.behaviors.tick.indexOf(testComponentInstance), -1); el.play(); assert.equal(el.sceneEl.behaviors.tick.indexOf(testComponentInstance), -1); }); test('removes tock from scene behaviors on entity pause', function () { var testComponentInstance; el.setAttribute('test', ''); testComponentInstance = el.components.test; assert.notEqual(el.sceneEl.behaviors.tock.indexOf(testComponentInstance), -1); el.pause(); assert.equal(el.sceneEl.behaviors.tock.indexOf(testComponentInstance), -1); }); test('adds tock to scene behaviors on entity play', function () { var testComponentInstance; el.setAttribute('test', ''); testComponentInstance = el.components.test; el.sceneEl.behaviors.tock = []; assert.equal(el.sceneEl.behaviors.tock.indexOf(testComponentInstance), -1); el.play(); assert.equal(el.sceneEl.behaviors.tock.indexOf(testComponentInstance), -1); }); suite('remove', function () { test('detaches if called with no arguments', function (done) { el.remove(); setTimeout(() => { assert.notOk(el.parentNode); done(); }); }); test('detaches child object3D if called with child', function (done) { const childrenLength = el.parentNode.object3D.children.length; el.parentNode.remove(el); setTimeout(() => { assert.ok(el.parentNode.object3D.children.length < childrenLength); done(); }); }); }); suite('destroy', function () { test('does not destroy components if still attached', function () { el.setAttribute('test', ''); const destroySpy = this.sinon.spy(el.components.test, 'destroy'); el.destroy(); assert.notOk(destroySpy.callCount); }); test('destroys components if destroyed', function () { el.setAttribute('test', ''); el.parentNode.removeChild(el); const destroySpy = this.sinon.spy(el.components.test, 'destroy'); el.destroy(); assert.ok(destroySpy.callCount); }); }); }); suite('a-entity component dependency management', function () { var el; setup(function (done) { var componentNames = ['codependency', 'dependency', 'nested-dependency', 'test']; var componentProto; componentNames.forEach(function clearComponent (componentName) { components[componentName] = undefined; }); /** * root * dependency * nestedDependency * codependency */ componentProto = extend({}, TestComponent); var RootComponent = registerComponent('root', extend(componentProto, { dependencies: ['dependency', 'codependency'] })); this.rootInit = this.sinon.spy(RootComponent.prototype, 'init'); componentProto = extend({}, TestComponent); this.DependencyComponent = registerComponent('dependency', extend(componentProto, { dependencies: ['nested-dependency'] })); this.dependencyInit = this.sinon.spy(this.DependencyComponent.prototype, 'init'); componentProto = extend({}, TestComponent); var CodependencyComponent = registerComponent('codependency', extend(componentProto, { dependencies: [] })); this.codependencyInit = this.sinon.spy(CodependencyComponent.prototype, 'init'); componentProto = extend({}, TestComponent); var NestedDependency = registerComponent('nested-dependency', componentProto); this.nestedDependencyInit = this.sinon.spy(NestedDependency.prototype, 'init'); elFactory().then(_el => { el = _el; done(); }); }); teardown(function () { components.root = undefined; components.codependency = undefined; components.dependency = undefined; components['nested-dependency'] = undefined; }); test('initializes dependency components', function () { el.setAttribute('root', ''); assert.ok('root' in el.components); assert.ok('dependency' in el.components); assert.ok('codependency' in el.components); assert.ok('nested-dependency' in el.components); }); test('only initializes each component once', function () { el.setAttribute('root', ''); assert.equal(this.rootInit.callCount, 1); assert.equal(this.dependencyInit.callCount, 1); assert.equal(this.codependencyInit.callCount, 1); assert.equal(this.nestedDependencyInit.callCount, 1); }); test('initializes dependency components when not yet loaded', function () { el.setAttribute('root', ''); assert.ok('root' in el.components); assert.ok('dependency' in el.components); assert.ok('codependency' in el.components); assert.ok('nested-dependency' in el.components); }); test('initializes components (calling .init()) in the correct order', function (done) { elFactory().then(el => { el.setAttribute('root', ''); sinon.assert.callOrder( this.nestedDependencyInit, this.dependencyInit, this.codependencyInit, this.rootInit ); done(); }); }); test('initializes components (calling .init()) in the correct order via HTML', function (done) { elFactory().then(parentEl => { parentEl.addEventListener('child-attached', evt => { evt.detail.el.addEventListener('loaded', () => { sinon.assert.callOrder( this.nestedDependencyInit, this.dependencyInit, this.codependencyInit, this.rootInit ); done(); }); }); parentEl.innerHTML = '<a-entity root></a-entity>'; }); }); });