X-Git-Url: https://git.r.bdr.sh/rbdr/serpentity/blobdiff_plain/e936d6df77f8fe351fdde3fd9be829f9bc3aae97..6b97387630cedb2ef098659d65e77b1c790d62a0:/test/integration.js diff --git a/test/integration.js b/test/integration.js index 7d88432..d874742 100644 --- a/test/integration.js +++ b/test/integration.js @@ -1,162 +1,344 @@ -'use strict'; - -let test = function test (Serpentity) { - - /* eslint no-console: 0 */ - - ///////////////// - // Load the stuff - ///////////////// - console.log('\n## Loading'); - console.log('Serpentity: ' + (typeof Serpentity !== 'undefined' ? 'LOAD OK' : 'FAIL')); - console.log('Serpentity.Entity: ' + (typeof Serpentity !== 'undefined' && Serpentity.Entity ? 'LOAD OK' : 'FAIL')); - console.log('Serpentity.Component: ' + (typeof Serpentity !== 'undefined' && Serpentity.Component ? 'LOAD OK' : 'FAIL')); - console.log('Serpentity.System: ' + (typeof Serpentity !== 'undefined' && Serpentity.System ? 'LOAD OK' : 'FAIL')); - console.log('Serpentity.Node: ' + (typeof Serpentity !== 'undefined' && Serpentity.Node ? 'LOAD OK' : 'FAIL')); - console.log('Serpentity.NodeCollection: ' + (typeof Serpentity !== 'undefined' && Serpentity.NodeCollection ? 'LOAD OK' : 'FAIL')); - - ////////////////////// - // Create test classes - ////////////////////// - console.log('\n## Creating Test Classes'); - let TestSystem = class TestSystem extends Serpentity.System { - added (engine) { - this.testNodes = engine.getNodes(TestNode); - console.log('Engine is serpentity: ' + (engine instanceof Serpentity ? 'OK' : 'FAIL')); - console.log('System added callback: EXEC OK'); - } +import { describe, it, beforeEach, mock } from 'node:test'; +import assert from 'node:assert'; +import Serpentity, { Component, Entity, Node, System } from '../lib/serpentity.js'; - removed (engine) { - this.testNodes = null; - console.log('Engine is serpentity: ' + (engine instanceof Serpentity ? 'OK' : 'FAIL')); - console.log('System removed callback: EXEC OK'); - } +const internals = { + context: {}, + system: class TestSystem extends System { + added(engine) { - update (dt) { - this.testNodes.forEach(function (node) { - console.log('Running Node: ' + (node.test.testMessage === 'test' ? 'SYSTEM OK' : 'FAIL')); - }); - console.log('dt is number: ' + (typeof dt === 'number' ? 'OK' : 'FAIL')); - console.log('System update callback: EXEC OK'); - } - }; - let testSystem = new TestSystem(); - - let LowProTestSystem = class LowProTestSystem extends Serpentity.System { - added (engine) { - this.testNodes = engine.getNodes(TestNode); - console.log('Engine is serpentity: ' + (engine instanceof Serpentity ? 'OK' : 'FAIL')); - console.log('System added callback: EXEC OK'); - } + this.testNodes = engine.getNodes(internals.node); + this.addedCalled = true; + this.addedEngine = engine; + this.emittedEvents = []; + this.beforeCall = null; + this.afterCall = null; - removed (engine) { - this.testNodes = null; - console.log('Engine is serpentity: ' + (engine instanceof Serpentity ? 'OK' : 'FAIL')); - console.log('System removed callback: EXEC OK'); - } + this.changeObserver = (event) => { + this.emittedEvents.push(event) + }; - update (dt) { - this.testNodes.forEach(function (node) { - console.log('Running Low Priority Node: ' + (node.test.testMessage === 'test' ? 'SYSTEM OK' : 'FAIL')); - }); - console.log('dt is number: ' + (typeof dt === 'number' ? 'OK' : 'FAIL')); - console.log('System update callback: EXEC OK'); - } - }; - let lowProTestSystem = new LowProTestSystem(); - console.log('LowProTestSystem: CREATE OK'); - - let MidProTestSystem = class MidProTestSystem extends Serpentity.System { - added (engine) { - this.testNodes = engine.getNodes(TestNode); - console.log('Engine is serpentity: ' + (engine instanceof Serpentity ? 'OK' : 'FAIL')); - console.log('System added callback: EXEC OK'); + this.nodeAddedObserver = ({ node }) => { + node.test.addEventListener('change', this.changeObserver); + }; + + this.nodeRemovedObserver = ({ node }) => { + node.test.removeEventListener('change', this.changeObserver); + }; + + for (const node of this.testNodes) { + node.test.addEventListener('change', this.changeObserver); + } + + this.testNodes.addEventListener('nodeAdded', this.nodeAddedObserver); + this.testNodes.addEventListener('nodeRemoved', this.nodeRemovedObserver); + + super.added(); // not needed, but takes care of coverage :P } - removed (engine) { + removed(engine) { + + this.testNodes.removeEventListener('nodeAdded', this.nodeAddedObserver); + this.nodeAddedObserver = null; + + this.testNodes.removeEventListener('nodeRemoved', this.nodeRemovedObserver); + this.nodeRemovedObserver = null; + + for (const node of this.testNodes) { + node.test.removeEventListener('change', this.changeObserver); + } + this.changeObserver = null; + this.testNodes = null; - console.log('Engine is serpentity: ' + (engine instanceof Serpentity ? 'OK' : 'FAIL')); - console.log('System removed callback: EXEC OK'); + this.removedCalled = true; + this.removedEngine = engine; + this.emittedEvents = null; + this.beforeCall = null; + this.afterCall = null; + super.removed(); // not needed, but takes care of coverage :P } - update (dt) { - this.testNodes.forEach(function (node) { - console.log('Running Mid Priority Node: ' + (node.test.testMessage === 'test' ? 'SYSTEM OK' : 'FAIL')); - }); - console.log('dt is number: ' + (typeof dt === 'number' ? 'OK' : 'FAIL')); - console.log('System update callback: EXEC OK'); - } - }; - var midProTestSystem = new MidProTestSystem(); - console.log('MidProTestSystem: CREATE OK'); + update(dt) { + + this.updateCalled = Date.now(); + this.updateDt = dt; + for (const node of this.testNodes) { + this.beforeCall = this.beforeCall === null ? node.test.called : this.beforeCall; + node.test.called = true; + this.afterCall = this.afterCall === null ? node.test.called : this.afterCall; + node.entity.called = true; + } + + while (Date.now() === this.updateCalled) { /* pass some time */ } + super.update(); // not needed, but takes care of coverage :P + } + }, + component: class TestComponent extends Component { + constructor(config) { - let TestComponent = class TestComponent extends Serpentity.Component { - constructor (config) { super(config); - this.testMessage = this.testMessage || 'test'; + this.called = false; } - }; - console.log('TestComponent: CREATE OK'); + }, + node: class TestNode extends Node {}, + delta: 10 +}; - let TestNode = class TestNode extends Serpentity.Node {}; - TestNode.types = { - test : TestComponent - }; - console.log('TestNode: CREATE OK'); +// adds a component to the node +internals.node.types = { + test: internals.component +}; - console.log('\n## Adding system to the engine'); +describe('Loading', () => { - let engine = new Serpentity(); - console.log('engine: CREATE OK'); + it('Should export the main class', () => { - engine.addSystem(testSystem, 0); + assert(typeof Serpentity === 'function'); + }); - console.log('\n## Running update without any entities'); - engine.update(10); + it('Should export the Entity class', () => { - console.log('\n## Adding system to the engine and updating'); - let entity = new Serpentity.Entity(); - entity.addComponent(new TestComponent()); - engine.addEntity(entity); - engine.update(10); + assert(typeof Entity === 'function'); + }); - console.log('\n## Adding Low Priority System'); - engine.addSystem(lowProTestSystem, 10); - engine.update(10); + it('Should export the Component class', () => { - console.log('\n## Adding Mid Priority System'); - engine.addSystem(midProTestSystem, 5); - engine.update(10); + assert(typeof Component === 'function'); + }); - console.log('\n## Removing the system and readding'); - engine.removeSystem(testSystem); - engine.update(10); - engine.addSystem(testSystem, 0); - engine.update(10); + it('Should export the System class', () => { - console.log('\n## Adding a second entity'); - entity = new Serpentity.Entity(); - entity.addComponent(new TestComponent()); - engine.addEntity(entity); - engine.update(10); + assert(typeof System === 'function'); + }); - console.log('\n## Removing entity'); - engine.removeEntity(entity); - engine.update(10); + it('Should export the Node class', () => { - console.log('\n## Removing system'); - engine.removeSystem(testSystem); - engine.update(10); + assert(typeof Node === 'function'); + }); +}); -}; +describe('Engine', () => { + + beforeEach(() => { + + internals.context.engine = new Serpentity(); + + internals.context.regularSystem = new internals.system(); + internals.context.highPrioritySystem = new internals.system(); + internals.context.lowPrioritySystem = new internals.system(); + + internals.context.firstEntity = new Entity(); + internals.context.firstEntity.addComponent(new internals.component()); + internals.context.secondEntity = new Entity(); + internals.context.secondEntity.addComponent(new internals.component()); + internals.context.emptyEntity = new Entity(); + + // Add entity before the systems + internals.context.engine.addEntity(internals.context.firstEntity); + + internals.context.engine.addSystem(internals.context.regularSystem, 100); + internals.context.engine.addSystem(internals.context.highPrioritySystem, 0); + internals.context.engine.addSystem(internals.context.lowPrioritySystem, 1000); + + // Add entity after the systems + internals.context.engine.addEntity(internals.context.secondEntity); + internals.context.engine.addEntity(internals.context.emptyEntity); + }); + + it('should call added callback on added systems', () => { + + // Ensure the added callback is being called + assert(internals.context.regularSystem.addedCalled); + assert(internals.context.highPrioritySystem.addedCalled); + assert(internals.context.lowPrioritySystem.addedCalled); + }); + + it('should send the engine instance in added callback', () => { + + // Ensure the added callback is sending the engine + assert(internals.context.regularSystem.addedEngine instanceof Serpentity); + assert(internals.context.highPrioritySystem.addedEngine instanceof Serpentity); + assert(internals.context.lowPrioritySystem.addedEngine instanceof Serpentity); + }); + + it('should not add duplicate systems', () => { + + const originalSystemsLength = internals.context.engine.systems.length; + const added = internals.context.engine.addSystem(internals.context.regularSystem, 0); + const newSystemsLength = internals.context.engine.systems.length; + + // Ensure we don't add the same system twice + assert(!added); + assert.deepEqual(newSystemsLength, originalSystemsLength); + }); + + it('should call update callback on added systems', () => { + + internals.context.engine.update(internals.delta); + + // Ensure update function called + assert(!!internals.context.regularSystem.updateCalled); + assert(!!internals.context.highPrioritySystem.updateCalled); + assert(!!internals.context.lowPrioritySystem.updateCalled); + }); + + it('should keep proxied object behavior as expected', () => { + + internals.context.engine.update(internals.delta); + + assert(internals.context.highPrioritySystem.beforeCall === false); + assert(internals.context.highPrioritySystem.afterCall === true); + }); + + it('should emit an event for every changed property', () => { + + internals.context.engine.update(internals.delta); + + assert(internals.context.regularSystem.emittedEvents[0].property === 'called'); + assert(internals.context.regularSystem.emittedEvents[0].from === false); + assert(internals.context.regularSystem.emittedEvents[0].to === true); + // 3 systems x 2 entities. + assert(internals.context.regularSystem.emittedEvents.length === 6); + }); + + it('should call update callback in the order of priorities', () => { + + internals.context.engine.update(internals.delta); + + // Ensure order of priorities + assert(internals.context.regularSystem.updateCalled < internals.context.lowPrioritySystem.updateCalled); + assert(internals.context.regularSystem.updateCalled > internals.context.highPrioritySystem.updateCalled); + }); + + it('should send the delta in the update callback', () => { + + internals.context.engine.update(internals.delta); + + // Ensure delta is being sent + assert.deepEqual(internals.context.regularSystem.updateDt, internals.delta); + assert.deepEqual(internals.context.highPrioritySystem.updateDt, internals.delta); + assert.deepEqual(internals.context.lowPrioritySystem.updateDt, internals.delta); + }); + + it('should no longer call removed systems', () => { + + const originalSystemLength = internals.context.engine.systems.length; + const originalRemoved = internals.context.engine.removeSystem(internals.context.lowPrioritySystem); + const intermediateSystemLength = internals.context.engine.systems.length; + const finalRemoved = internals.context.engine.removeSystem(internals.context.lowPrioritySystem); + const finalSystemLength = internals.context.engine.systems.length; + internals.context.engine.update(internals.delta); + + // Check for return value + assert(originalRemoved); + assert(!finalRemoved); + + // Confirm that only removed if found by checking length of systems + // array + assert(originalSystemLength > intermediateSystemLength); + assert.deepEqual(finalSystemLength, intermediateSystemLength); + + // Ensure callback is sent + assert(!internals.context.regularSystem.removedCalled); + assert(!internals.context.highPrioritySystem.removedCalled); + assert(!!internals.context.lowPrioritySystem.removedCalled); + + // Ensure update is no longer sent + assert(!!internals.context.regularSystem.updateCalled); + assert(!!internals.context.highPrioritySystem.updateCalled); + assert(!internals.context.lowPrioritySystem.updateCalled); + }); + + it('should only call nodes in selected node collections', () => { + + internals.context.engine.update(internals.delta); + + // Ensure component is called for each entity + assert(!!internals.context.firstEntity._components[0].called); + assert(!!internals.context.secondEntity._components[0].called); + + // Ensure entity not in node collection not called + assert(!!internals.context.firstEntity.called); + assert(!!internals.context.secondEntity.called); + assert(!internals.context.emptyEntity.called); + }); + + it('should stop showing removed entities', () => { + + internals.context.engine.removeEntity(internals.context.secondEntity); + internals.context.engine.update(internals.delta); + + assert(!!internals.context.firstEntity._components[0].called); + assert(!internals.context.secondEntity._components[0].called); + + assert(!!internals.context.firstEntity.called); + assert(!internals.context.secondEntity.called); + assert(!internals.context.emptyEntity.called); + }); + + it('should not add duplicate components to entities', () => { + + const originalComponentsLength = internals.context.secondEntity._components.length; + const result = internals.context.secondEntity.addComponent(new internals.component()); + const newComponentsLength = internals.context.secondEntity._components.length; + + assert(!result); + assert.deepEqual(newComponentsLength, originalComponentsLength); + }); + + it('should allow access to components by class', () => { + + const firstComponent = internals.context.firstEntity.getComponent(internals.component); + const emptyComponent = internals.context.emptyEntity.getComponent(internals.component); + + assert(firstComponent instanceof internals.component); + assert.deepEqual(emptyComponent, undefined); + }); + + it('should not add duplicate entities', () => { + + const originalEntitiesLength = internals.context.engine.entities.length; + const added = internals.context.engine.addEntity(internals.context.firstEntity); + const finalEntitiesLength = internals.context.engine.entities.length; + + assert(!added); + + assert.deepEqual(finalEntitiesLength, originalEntitiesLength); + }); + + it('should remove entities', () => { + + const originalEntityLength = internals.context.engine.entities.length; + const originalRemoved = internals.context.engine.removeEntity(internals.context.firstEntity); + const intermediateEntityLength = internals.context.engine.entities.length; + const finalRemoved = internals.context.engine.removeEntity(internals.context.firstEntity); + const finalEntityLength = internals.context.engine.entities.length; + internals.context.engine.update(internals.delta); + + // Check for return value + assert(originalRemoved); + assert(!finalRemoved); + + // Confirm that only removed if found by checking length of systems + // array + assert(originalEntityLength > intermediateEntityLength); + assert.deepEqual(finalEntityLength, intermediateEntityLength); + + // Ensure callback is sent + assert(!internals.context.firstEntity.called); + assert(!!internals.context.secondEntity.called); + }); + + it('should not add duplicate entities', () => { + + const originalEntitiesLength = internals.context.engine.entities.length; + const added = internals.context.engine.addEntity(internals.context.firstEntity); + const finalEntitiesLength = internals.context.engine.entities.length; + + assert(!added); -if (typeof require === 'function') { - let Serpentity = require('serpentity'); - test(Serpentity); -} else { - window.addEventListener('load', function () { - test(window.Serpentity); + assert.deepEqual(finalEntitiesLength, originalEntitiesLength); }); -} +});