]> git.r.bdr.sh - rbdr/serpentity/blobdiff - test/integration.js
Make node components event emitters
[rbdr/serpentity] / test / integration.js
index 7d88432bb73859c460fb2be51474cd23eee3af09..d87474250f6b1e414615a27653341da9f51d543d 100644 (file)
-'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);
   });
-}
+});