import Code from '@hapi/code'; // assertion library import Lab from '@hapi/lab'; import Serpentity, { Component, Entity, Node, System } from '../lib/serpentity.js'; const internals = { context: {}, system: class TestSystem extends System { added(engine) { this.testNodes = engine.getNodes(internals.node); this.addedCalled = true; this.addedEngine = engine; } removed(engine) { this.testNodes = null; this.removedCalled = true; this.removedEngine = engine; } update(dt) { this.updateCalled = Date.now(); this.updateDt = dt; for (const node of this.testNodes) { node.test.called = true; node.entity.called = true; } while (Date.now() === this.updateCalled) { /* pass some time */ } } }, component: class TestComponent extends Component { constructor(config) { super(config); this.called = false; } }, node: class TestNode extends Node {}, delta: 10 }; // adds a component to the node internals.node.types = { test: internals.component }; const lab = Lab.script({ schedule: false }); const { beforeEach, experiment, test } = lab; const { expect } = Code; experiment('loading', () => { test('Serpentity should be exported', () => { expect(typeof Serpentity).to.not.be.undefined(); }); test('Serpentity should include the Entity class', () => { expect(typeof Entity).to.not.be.undefined(); }); test('Serpentity should include the Component class', () => { expect(typeof Component).to.not.be.undefined(); }); test('Serpentity should include the System class', () => { expect(typeof System).to.not.be.undefined(); }); test('Serpentity should include the Node class', () => { expect(typeof Node).to.not.be.undefined(); }); test('Serpentity should include the NodeCollection class', () => { expect(typeof NodeCollection).to.not.be.undefined(); }); }); experiment('Engine Tests', () => { 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); }); test('Engine should call added callback on added systems', () => { // Ensure the added callback is being called expect(internals.context.regularSystem.addedCalled).to.be.true(); expect(internals.context.highPrioritySystem.addedCalled).to.be.true(); expect(internals.context.lowPrioritySystem.addedCalled).to.be.true(); }); test('Engine should send the engine instance in added callback', () => { // Ensure the added callback is sending the engine expect(internals.context.regularSystem.addedEngine instanceof Serpentity).to.be.true(); expect(internals.context.highPrioritySystem.addedEngine instanceof Serpentity).to.be.true(); expect(internals.context.lowPrioritySystem.addedEngine instanceof Serpentity).to.be.true(); }); test('Engine 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 expect(added).to.be.false(); expect(originalSystemsLength).to.be.equal(newSystemsLength); }); test('Engine should call update callback on added systems', () => { internals.context.engine.update(internals.delta); // Ensure update function called expect(!!internals.context.regularSystem.updateCalled).to.be.true(); expect(!!internals.context.highPrioritySystem.updateCalled).to.be.true(); expect(!!internals.context.lowPrioritySystem.updateCalled).to.be.true(); }); test('Engine should call update callback in the order of priorities', () => { internals.context.engine.update(internals.delta); // Ensure order of priorities expect(internals.context.regularSystem.updateCalled).to.be.lessThan(internals.context.lowPrioritySystem.updateCalled); expect(internals.context.regularSystem.updateCalled).to.be.greaterThan(internals.context.highPrioritySystem.updateCalled); }); test('Engine should send the delta in the update callback', () => { internals.context.engine.update(internals.delta); // Ensure delta is being sent expect(internals.context.regularSystem.updateDt).to.be.equal(internals.delta); expect(internals.context.highPrioritySystem.updateDt).to.be.equal(internals.delta); expect(internals.context.lowPrioritySystem.updateDt).to.be.equal(internals.delta); }); test('System remove callback', () => { 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 expect(originalRemoved).to.be.true(); expect(finalRemoved).to.be.false(); // Confirm that only removed if found by checking length of systems // array expect(originalSystemLength).to.be.above(intermediateSystemLength); expect(finalSystemLength).to.be.equal(intermediateSystemLength); // Ensure callback is sent expect(!!internals.context.regularSystem.removedCalled).to.be.false(); expect(!!internals.context.highPrioritySystem.removedCalled).to.be.false(); expect(!!internals.context.lowPrioritySystem.removedCalled).to.be.true(); // Ensure update is no longer sent expect(!!internals.context.regularSystem.updateCalled).to.be.true(); expect(!!internals.context.highPrioritySystem.updateCalled).to.be.true(); expect(!!internals.context.lowPrioritySystem.updateCalled).to.be.false(); }); test('Entity node selection', () => { internals.context.engine.update(internals.delta); // Ensure component is called for each entity expect(!!internals.context.firstEntity._components[0].called).to.be.true(); expect(!!internals.context.secondEntity._components[0].called).to.be.true(); // Ensure entity not in node collection not called expect(!!internals.context.firstEntity.called).to.be.true(); expect(!!internals.context.secondEntity.called).to.be.true(); expect(!!internals.context.emptyEntity.called).to.be.false(); }); test('Entity node removal', () => { internals.context.engine.removeEntity(internals.context.secondEntity); internals.context.engine.update(internals.delta); expect(!!internals.context.firstEntity._components[0].called).to.be.true(); expect(!!internals.context.secondEntity._components[0].called).to.be.false(); expect(!!internals.context.firstEntity.called).to.be.true(); expect(!!internals.context.secondEntity.called).to.be.false(); expect(!!internals.context.emptyEntity.called).to.be.false(); }); test('Entity should not add duplicate components', () => { const originalComponentsLength = internals.context.secondEntity._components.length; const result = internals.context.secondEntity.addComponent(new internals.component()); const newComponentsLength = internals.context.secondEntity._components.length; expect(result).to.be.false(); expect(originalComponentsLength).to.be.equal(newComponentsLength); }); test('Entity should allow access to components by class', () => { const firstComponent = internals.context.firstEntity.getComponent(internals.component); const emptyComponent = internals.context.emptyEntity.getComponent(internals.component); expect(firstComponent instanceof internals.component).to.be.true(); expect(emptyComponent).to.be.equal(undefined); }); test('Engine 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; expect(added).to.be.false(); expect(originalEntitiesLength).to.be.equal(finalEntitiesLength); }); test('Engine 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 expect(originalRemoved).to.be.true(); expect(finalRemoved).to.be.false(); // Confirm that only removed if found by checking length of systems // array expect(originalEntityLength).to.be.above(intermediateEntityLength); expect(finalEntityLength).to.be.equal(intermediateEntityLength); // Ensure callback is sent expect(!!internals.context.firstEntity.called).to.be.false(); expect(!!internals.context.secondEntity.called).to.be.true(); }); }); Lab.report(lab).then((result) => process.exit(result.code));