These organizations and open-source projects, and many others, use QUnit to keep their code in check.
Hogan.js,
diff --git a/redirects.json b/redirects.json
index 1c763a348..09d5aca6d 100644
--- a/redirects.json
+++ b/redirects.json
@@ -1 +1 @@
-{"/config/QUnit.assert/":"https://qunitjs.com/api/extension/QUnit.assert/","/extension/QUnit.assert/":"https://qunitjs.com/api/extension/QUnit.assert/","/callbacks/QUnit.begin/":"https://qunitjs.com/api/callbacks/QUnit.begin/","/QUnit.begin/":"https://qunitjs.com/api/callbacks/QUnit.begin/","/callbacks/QUnit.done/":"https://qunitjs.com/api/callbacks/QUnit.done/","/QUnit.done/":"https://qunitjs.com/api/callbacks/QUnit.done/","/QUnit.dump.parse/":"https://qunitjs.com/api/extension/QUnit.dump.parse/","/QUnit.jsDump.parse/":"https://qunitjs.com/api/extension/QUnit.dump.parse/","/config/QUnit.dump.parse/":"https://qunitjs.com/api/extension/QUnit.dump.parse/","/extension/QUnit.dump.parse/":"https://qunitjs.com/api/extension/QUnit.dump.parse/","/config/QUnit.extend/":"https://qunitjs.com/api/extension/QUnit.extend/","/extension/QUnit.extend/":"https://qunitjs.com/api/extension/QUnit.extend/","/callbacks/QUnit.log/":"https://qunitjs.com/api/callbacks/QUnit.log/","/QUnit.log/":"https://qunitjs.com/api/callbacks/QUnit.log/","/callbacks/QUnit.moduleDone/":"https://qunitjs.com/api/callbacks/QUnit.moduleDone/","/QUnit.moduleDone/":"https://qunitjs.com/api/callbacks/QUnit.moduleDone/","/callbacks/QUnit.moduleStart/":"https://qunitjs.com/api/callbacks/QUnit.moduleStart/","/QUnit.moduleStart/":"https://qunitjs.com/api/callbacks/QUnit.moduleStart/","/callbacks/QUnit.on/":"https://qunitjs.com/api/callbacks/QUnit.on/","/config/QUnit.onUncaughtException/":"https://qunitjs.com/api/extension/QUnit.onUncaughtException/","/extension/QUnit.onUncaughtException/":"https://qunitjs.com/api/extension/QUnit.onUncaughtException/","/config/QUnit.stack/":"https://qunitjs.com/api/extension/QUnit.stack/","/extension/QUnit.stack/":"https://qunitjs.com/api/extension/QUnit.stack/","/callbacks/QUnit.testDone/":"https://qunitjs.com/api/callbacks/QUnit.testDone/","/QUnit.testDone/":"https://qunitjs.com/api/callbacks/QUnit.testDone/","/callbacks/QUnit.testStart/":"https://qunitjs.com/api/callbacks/QUnit.testStart/","/QUnit.testStart/":"https://qunitjs.com/api/callbacks/QUnit.testStart/","/config/altertitle/":"https://qunitjs.com/api/config/altertitle/","/assert/async/":"https://qunitjs.com/api/assert/async/","/QUnit.stop/":"https://qunitjs.com/api/assert/async/","/QUnit/stop/":"https://qunitjs.com/api/assert/async/","/stop/":"https://qunitjs.com/api/assert/async/","/async/":"https://qunitjs.com/api/async/","/config/autostart/":"https://qunitjs.com/api/config/autostart/","/node/":"https://qunitjs.com/cli/","/assert/closeTo/":"https://qunitjs.com/api/assert/closeTo/","/config/collapse/":"https://qunitjs.com/api/config/collapse/","/config/current/":"https://qunitjs.com/api/config/current/","/assert/deepEqual/":"https://qunitjs.com/api/assert/deepEqual/","/deepEqual/":"https://qunitjs.com/api/assert/deepEqual/","/deprecated/":"https://qunitjs.com/api/deprecated/","/equal/":"https://qunitjs.com/api/assert/equal/","/equals/":"https://qunitjs.com/api/assert/equal/","/assert/equal/":"https://qunitjs.com/api/assert/equal/","/assert/equals/":"https://qunitjs.com/api/assert/equal/","/assert/expect/":"https://qunitjs.com/api/assert/expect/","/expect/":"https://qunitjs.com/api/assert/expect/","/config/failOnZeroTests/":"https://qunitjs.com/api/config/failOnZeroTests/","/assert/false/":"https://qunitjs.com/api/assert/false/","/config/filter/":"https://qunitjs.com/api/config/filter/","/config/fixture/":"https://qunitjs.com/api/config/fixture/","/config/hidepassed/":"https://qunitjs.com/api/config/hidepassed/","/QUnit/hooks/":"https://qunitjs.com/api/QUnit/hooks/","/extension/":"https://qunitjs.com/api/extension/","/QUnit/":"https://qunitjs.com/api/QUnit/","/QUnit.config/":"https://qunitjs.com/api/config/","/config/":"https://qunitjs.com/api/config/","/config/QUnit.config/":"https://qunitjs.com/api/config/","/assert/":"https://qunitjs.com/api/assert/","/QUnit.assert/":"https://qunitjs.com/api/assert/","/category/assert/":"https://qunitjs.com/api/assert/","/callbacks/":"https://qunitjs.com/api/callbacks/","/category/callbacks/":"https://qunitjs.com/api/callbacks/","/category/all/":"https://qunitjs.com/api/","/guides/":"https://qunitjs.com/intro/","/cookbook/":"https://qunitjs.com/intro/","/QUnit/load/":"https://qunitjs.com/api/QUnit/load/","/config/maxDepth/":"https://qunitjs.com/api/config/maxDepth/","/QUnit.module/":"https://qunitjs.com/api/QUnit/module/","/QUnit/module/":"https://qunitjs.com/api/QUnit/module/","/module/":"https://qunitjs.com/api/QUnit/module/","/config/module/":"https://qunitjs.com/api/config/module/","/config/moduleId/":"https://qunitjs.com/api/config/moduleId/","/config/modules/":"https://qunitjs.com/api/config/modules/","/config/noglobals/":"https://qunitjs.com/api/config/noglobals/","/assert/notDeepEqual/":"https://qunitjs.com/api/assert/notDeepEqual/","/notDeepEqual/":"https://qunitjs.com/api/assert/notDeepEqual/","/assert/notEqual/":"https://qunitjs.com/api/assert/notEqual/","/notEqual/":"https://qunitjs.com/api/assert/notEqual/","/assert/notOk/":"https://qunitjs.com/api/assert/notOk/","/notOk/":"https://qunitjs.com/api/assert/notOk/","/assert/notPropContains/":"https://qunitjs.com/api/assert/notPropContains/","/assert/notPropEqual/":"https://qunitjs.com/api/assert/notPropEqual/","/notPropEqual/":"https://qunitjs.com/api/assert/notPropEqual/","/assert/notStrictEqual/":"https://qunitjs.com/api/assert/notStrictEqual/","/notStrictEqual/":"https://qunitjs.com/api/assert/notStrictEqual/","/config/notrycatch/":"https://qunitjs.com/api/config/notrycatch/","/assert/ok/":"https://qunitjs.com/api/assert/ok/","/ok/":"https://qunitjs.com/api/assert/ok/","/addons/":"https://qunitjs.com/plugins/","/assert/propContains/":"https://qunitjs.com/api/assert/propContains/","/assert/propEqual/":"https://qunitjs.com/api/assert/propEqual/","/propEqual/":"https://qunitjs.com/api/assert/propEqual/","/config/QUnit.push/":"https://qunitjs.com/api/assert/push/","/extension/QUnit.push/":"https://qunitjs.com/api/assert/push/","/api/extension/QUnit.push/":"https://qunitjs.com/api/assert/push/","/pushResult/":"https://qunitjs.com/api/assert/pushResult/","/assert/pushResult/":"https://qunitjs.com/api/assert/pushResult/","/assert/rejects/":"https://qunitjs.com/api/assert/rejects/","/removed/":"https://qunitjs.com/api/removed/","/config/reorder/":"https://qunitjs.com/api/config/reorder/","/config/requireExpects/":"https://qunitjs.com/api/config/requireExpects/","/config/scrolltop/":"https://qunitjs.com/api/config/scrolltop/","/config/seed/":"https://qunitjs.com/api/config/seed/","/QUnit/start/":"https://qunitjs.com/api/QUnit/start/","/start/":"https://qunitjs.com/api/QUnit/start/","/assert/step/":"https://qunitjs.com/api/assert/step/","/config/storage/":"https://qunitjs.com/api/config/storage/","/same/":"https://qunitjs.com/api/assert/strictEqual/","/strictEqual/":"https://qunitjs.com/api/assert/strictEqual/","/assert/same/":"https://qunitjs.com/api/assert/strictEqual/","/assert/strictEqual/":"https://qunitjs.com/api/assert/strictEqual/","/QUnit/test.each/":"https://qunitjs.com/api/QUnit/test.each/","/QUnit.asyncTest/":"https://qunitjs.com/api/QUnit/test/","/QUnit.test/":"https://qunitjs.com/api/QUnit/test/","/QUnit/test/":"https://qunitjs.com/api/QUnit/test/","/asyncTest/":"https://qunitjs.com/api/QUnit/test/","/test/":"https://qunitjs.com/api/QUnit/test/","/QUnit.only/":"https://qunitjs.com/api/QUnit/test.only/","/QUnit/only/":"https://qunitjs.com/api/QUnit/test.only/","/QUnit/test.only/":"https://qunitjs.com/api/QUnit/test.only/","/QUnit.skip/":"https://qunitjs.com/api/QUnit/test.skip/","/QUnit/skip/":"https://qunitjs.com/api/QUnit/test.skip/","/QUnit/test.skip/":"https://qunitjs.com/api/QUnit/test.skip/","/QUnit.todo/":"https://qunitjs.com/api/QUnit/test.todo/","/QUnit/todo/":"https://qunitjs.com/api/QUnit/test.todo/","/QUnit/test.todo/":"https://qunitjs.com/api/QUnit/test.todo/","/config/testId/":"https://qunitjs.com/api/config/testId/","/config/testTimeout/":"https://qunitjs.com/api/config/testTimeout/","/assert/raises/":"https://qunitjs.com/api/assert/throws/","/assert/throws/":"https://qunitjs.com/api/assert/throws/","/throws/":"https://qunitjs.com/api/assert/throws/","/assert/timeout/":"https://qunitjs.com/api/assert/timeout/","/assert/true/":"https://qunitjs.com/api/assert/true/","/config/urlConfig/":"https://qunitjs.com/api/config/urlConfig/","/assert/verifySteps/":"https://qunitjs.com/api/assert/verifySteps/"}
\ No newline at end of file
+{"/config/QUnit.assert/":"https://qunitjs.com/api/extension/QUnit.assert/","/extension/QUnit.assert/":"https://qunitjs.com/api/extension/QUnit.assert/","/callbacks/QUnit.begin/":"https://qunitjs.com/api/callbacks/QUnit.begin/","/QUnit.begin/":"https://qunitjs.com/api/callbacks/QUnit.begin/","/callbacks/QUnit.done/":"https://qunitjs.com/api/callbacks/QUnit.done/","/QUnit.done/":"https://qunitjs.com/api/callbacks/QUnit.done/","/QUnit.dump.parse/":"https://qunitjs.com/api/extension/QUnit.dump.parse/","/QUnit.jsDump.parse/":"https://qunitjs.com/api/extension/QUnit.dump.parse/","/config/QUnit.dump.parse/":"https://qunitjs.com/api/extension/QUnit.dump.parse/","/extension/QUnit.dump.parse/":"https://qunitjs.com/api/extension/QUnit.dump.parse/","/config/QUnit.extend/":"https://qunitjs.com/api/extension/QUnit.extend/","/extension/QUnit.extend/":"https://qunitjs.com/api/extension/QUnit.extend/","/callbacks/QUnit.log/":"https://qunitjs.com/api/callbacks/QUnit.log/","/QUnit.log/":"https://qunitjs.com/api/callbacks/QUnit.log/","/callbacks/QUnit.moduleDone/":"https://qunitjs.com/api/callbacks/QUnit.moduleDone/","/QUnit.moduleDone/":"https://qunitjs.com/api/callbacks/QUnit.moduleDone/","/callbacks/QUnit.moduleStart/":"https://qunitjs.com/api/callbacks/QUnit.moduleStart/","/QUnit.moduleStart/":"https://qunitjs.com/api/callbacks/QUnit.moduleStart/","/callbacks/QUnit.on/":"https://qunitjs.com/api/callbacks/QUnit.on/","/config/QUnit.onUncaughtException/":"https://qunitjs.com/api/extension/QUnit.onUncaughtException/","/extension/QUnit.onUncaughtException/":"https://qunitjs.com/api/extension/QUnit.onUncaughtException/","/config/QUnit.stack/":"https://qunitjs.com/api/extension/QUnit.stack/","/extension/QUnit.stack/":"https://qunitjs.com/api/extension/QUnit.stack/","/callbacks/QUnit.testDone/":"https://qunitjs.com/api/callbacks/QUnit.testDone/","/QUnit.testDone/":"https://qunitjs.com/api/callbacks/QUnit.testDone/","/callbacks/QUnit.testStart/":"https://qunitjs.com/api/callbacks/QUnit.testStart/","/QUnit.testStart/":"https://qunitjs.com/api/callbacks/QUnit.testStart/","/config/altertitle/":"https://qunitjs.com/api/config/altertitle/","/assert/async/":"https://qunitjs.com/api/assert/async/","/QUnit.stop/":"https://qunitjs.com/api/assert/async/","/QUnit/stop/":"https://qunitjs.com/api/assert/async/","/stop/":"https://qunitjs.com/api/assert/async/","/async/":"https://qunitjs.com/api/async/","/config/autostart/":"https://qunitjs.com/api/config/autostart/","/cookbook/":"https://qunitjs.com/browser/","/node/":"https://qunitjs.com/cli/","/assert/closeTo/":"https://qunitjs.com/api/assert/closeTo/","/config/collapse/":"https://qunitjs.com/api/config/collapse/","/config/current/":"https://qunitjs.com/api/config/current/","/assert/deepEqual/":"https://qunitjs.com/api/assert/deepEqual/","/deepEqual/":"https://qunitjs.com/api/assert/deepEqual/","/deprecated/":"https://qunitjs.com/api/deprecated/","/equal/":"https://qunitjs.com/api/assert/equal/","/equals/":"https://qunitjs.com/api/assert/equal/","/assert/equal/":"https://qunitjs.com/api/assert/equal/","/assert/equals/":"https://qunitjs.com/api/assert/equal/","/assert/expect/":"https://qunitjs.com/api/assert/expect/","/expect/":"https://qunitjs.com/api/assert/expect/","/config/failOnZeroTests/":"https://qunitjs.com/api/config/failOnZeroTests/","/assert/false/":"https://qunitjs.com/api/assert/false/","/config/filter/":"https://qunitjs.com/api/config/filter/","/config/fixture/":"https://qunitjs.com/api/config/fixture/","/config/hidepassed/":"https://qunitjs.com/api/config/hidepassed/","/QUnit/hooks/":"https://qunitjs.com/api/QUnit/hooks/","/callbacks/":"https://qunitjs.com/api/callbacks/","/category/callbacks/":"https://qunitjs.com/api/callbacks/","/QUnit/":"https://qunitjs.com/api/QUnit/","/extension/":"https://qunitjs.com/api/extension/","/assert/":"https://qunitjs.com/api/assert/","/QUnit.assert/":"https://qunitjs.com/api/assert/","/category/assert/":"https://qunitjs.com/api/assert/","/QUnit.config/":"https://qunitjs.com/api/config/","/config/":"https://qunitjs.com/api/config/","/config/QUnit.config/":"https://qunitjs.com/api/config/","/category/all/":"https://qunitjs.com/api/","/guides/":"https://qunitjs.com/intro/","/QUnit/load/":"https://qunitjs.com/api/QUnit/load/","/config/maxDepth/":"https://qunitjs.com/api/config/maxDepth/","/QUnit.module/":"https://qunitjs.com/api/QUnit/module/","/QUnit/module/":"https://qunitjs.com/api/QUnit/module/","/module/":"https://qunitjs.com/api/QUnit/module/","/config/module/":"https://qunitjs.com/api/config/module/","/config/moduleId/":"https://qunitjs.com/api/config/moduleId/","/config/modules/":"https://qunitjs.com/api/config/modules/","/config/noglobals/":"https://qunitjs.com/api/config/noglobals/","/assert/notDeepEqual/":"https://qunitjs.com/api/assert/notDeepEqual/","/notDeepEqual/":"https://qunitjs.com/api/assert/notDeepEqual/","/assert/notEqual/":"https://qunitjs.com/api/assert/notEqual/","/notEqual/":"https://qunitjs.com/api/assert/notEqual/","/assert/notOk/":"https://qunitjs.com/api/assert/notOk/","/notOk/":"https://qunitjs.com/api/assert/notOk/","/assert/notPropContains/":"https://qunitjs.com/api/assert/notPropContains/","/assert/notPropEqual/":"https://qunitjs.com/api/assert/notPropEqual/","/notPropEqual/":"https://qunitjs.com/api/assert/notPropEqual/","/assert/notStrictEqual/":"https://qunitjs.com/api/assert/notStrictEqual/","/notStrictEqual/":"https://qunitjs.com/api/assert/notStrictEqual/","/config/notrycatch/":"https://qunitjs.com/api/config/notrycatch/","/assert/ok/":"https://qunitjs.com/api/assert/ok/","/ok/":"https://qunitjs.com/api/assert/ok/","/addons/":"https://qunitjs.com/plugins/","/assert/propContains/":"https://qunitjs.com/api/assert/propContains/","/assert/propEqual/":"https://qunitjs.com/api/assert/propEqual/","/propEqual/":"https://qunitjs.com/api/assert/propEqual/","/config/QUnit.push/":"https://qunitjs.com/api/assert/push/","/extension/QUnit.push/":"https://qunitjs.com/api/assert/push/","/api/extension/QUnit.push/":"https://qunitjs.com/api/assert/push/","/pushResult/":"https://qunitjs.com/api/assert/pushResult/","/assert/pushResult/":"https://qunitjs.com/api/assert/pushResult/","/assert/rejects/":"https://qunitjs.com/api/assert/rejects/","/removed/":"https://qunitjs.com/api/removed/","/config/reorder/":"https://qunitjs.com/api/config/reorder/","/config/requireExpects/":"https://qunitjs.com/api/config/requireExpects/","/config/scrolltop/":"https://qunitjs.com/api/config/scrolltop/","/config/seed/":"https://qunitjs.com/api/config/seed/","/QUnit/start/":"https://qunitjs.com/api/QUnit/start/","/start/":"https://qunitjs.com/api/QUnit/start/","/assert/step/":"https://qunitjs.com/api/assert/step/","/config/storage/":"https://qunitjs.com/api/config/storage/","/same/":"https://qunitjs.com/api/assert/strictEqual/","/strictEqual/":"https://qunitjs.com/api/assert/strictEqual/","/assert/same/":"https://qunitjs.com/api/assert/strictEqual/","/assert/strictEqual/":"https://qunitjs.com/api/assert/strictEqual/","/QUnit/test.each/":"https://qunitjs.com/api/QUnit/test.each/","/QUnit.asyncTest/":"https://qunitjs.com/api/QUnit/test/","/QUnit.test/":"https://qunitjs.com/api/QUnit/test/","/QUnit/test/":"https://qunitjs.com/api/QUnit/test/","/asyncTest/":"https://qunitjs.com/api/QUnit/test/","/test/":"https://qunitjs.com/api/QUnit/test/","/QUnit.only/":"https://qunitjs.com/api/QUnit/test.only/","/QUnit/only/":"https://qunitjs.com/api/QUnit/test.only/","/QUnit/test.only/":"https://qunitjs.com/api/QUnit/test.only/","/QUnit.skip/":"https://qunitjs.com/api/QUnit/test.skip/","/QUnit/skip/":"https://qunitjs.com/api/QUnit/test.skip/","/QUnit/test.skip/":"https://qunitjs.com/api/QUnit/test.skip/","/QUnit.todo/":"https://qunitjs.com/api/QUnit/test.todo/","/QUnit/todo/":"https://qunitjs.com/api/QUnit/test.todo/","/QUnit/test.todo/":"https://qunitjs.com/api/QUnit/test.todo/","/config/testId/":"https://qunitjs.com/api/config/testId/","/config/testTimeout/":"https://qunitjs.com/api/config/testTimeout/","/assert/raises/":"https://qunitjs.com/api/assert/throws/","/assert/throws/":"https://qunitjs.com/api/assert/throws/","/throws/":"https://qunitjs.com/api/assert/throws/","/assert/timeout/":"https://qunitjs.com/api/assert/timeout/","/assert/true/":"https://qunitjs.com/api/assert/true/","/config/urlConfig/":"https://qunitjs.com/api/config/urlConfig/","/assert/verifySteps/":"https://qunitjs.com/api/assert/verifySteps/"}
\ No newline at end of file
diff --git a/resources/calc.js b/resources/calc.js
new file mode 100644
index 000000000..da6da2bee
--- /dev/null
+++ b/resources/calc.js
@@ -0,0 +1,42 @@
+/**
+ * calc.js - An example project to demonstrate QUnit.
+ *
+ * @author Timo Tijhof, 2022
+ * @license 0BSD
+ * @license Public domain
+ */
+
+/**
+ * @param {number} a
+ * @param {number} b
+ * @return {number}
+ */
+export function add (a, b) {
+ return a + b;
+}
+
+/**
+ * @param {number} a
+ * @param {number} b
+ * @return {number}
+ */
+export function substract (a, b) {
+ return a - b;
+}
+
+/**
+ * @param {number} a
+ * @param {number} b
+ * @return {number}
+ */
+export function multiply (a, b) {
+ return a * b;
+}
+
+/**
+ * @param {number} x
+ * @return {number}
+ */
+export function square (x) {
+ return x * x;
+}
diff --git a/resources/calc.test.js b/resources/calc.test.js
new file mode 100644
index 000000000..12b2014a2
--- /dev/null
+++ b/resources/calc.test.js
@@ -0,0 +1,25 @@
+import * as calc from './calc.js';
+
+QUnit.module('calc', () => {
+ QUnit.test('add', (assert) => {
+ assert.equal(calc.add(1, 2), 3);
+ assert.equal(calc.add(2, 3), 5);
+ assert.equal(calc.add(5, -1), 4, 'negative');
+ });
+
+ QUnit.test('substract', (assert) => {
+ assert.equal(calc.substract(3, 2), 1);
+ assert.equal(calc.substract(5, 3), 2);
+ assert.equal(calc.substract(5, -1), 6, 'negative');
+ });
+
+ QUnit.test('multiply', (assert) => {
+ assert.equal(calc.multiply(7, 2), 14);
+ });
+
+ QUnit.test('square', (assert) => {
+ assert.equal(calc.square(5), 25);
+ assert.equal(calc.square(7), 49);
+ assert.equal(calc.square(-8), 64, 'negative');
+ });
+});
diff --git a/resources/example-add.html b/resources/example-add.html
index b7490faed..c828dd1ad 100644
--- a/resources/example-add.html
+++ b/resources/example-add.html
@@ -1,4 +1,5 @@
+
QUnit
@@ -7,14 +8,16 @@
+
diff --git a/resources/example-index.html b/resources/example-index.html
index afb5ddbae..71afc7832 100644
--- a/resources/example-index.html
+++ b/resources/example-index.html
@@ -1,20 +1,14 @@
+
QUnit
+
-
+
+
-
+
diff --git a/resources/game.js b/resources/game.js
new file mode 100644
index 000000000..bbf86e42e
--- /dev/null
+++ b/resources/game.js
@@ -0,0 +1,83 @@
+/**
+ * game.js - An example project to demonstrate QUnit.
+ *
+ * @author Timo Tijhof, 2022
+ * @license 0BSD
+ * @license Public domain
+ */
+
+export class Robot {}
+
+export class BoardObjectError extends Error {}
+export class BoardMoveError extends Error {}
+
+export class Coord {
+ constructor (x = 0, y = 0) {
+ this.x = x;
+ this.y = y;
+ }
+
+ clone () {
+ return new Coord(this.x, this.y);
+ }
+}
+
+export class Board {
+ constructor (width, height) {
+ this.xMax = width - 1;
+ this.yMax = height - 1;
+ this.objects = new Map();
+ }
+
+ add (object) {
+ if (this.objects.has(object)) {
+ throw new BoardObjectError('Object already on board');
+ }
+ this.objects.set(object, new Coord());
+ }
+
+ find (object) {
+ const pos = this.objects.get(object);
+ return pos ? pos.clone() : false;
+ }
+
+ moveUp (object) {
+ const pos = this.objects.get(object);
+ if (!pos) {
+ throw new BoardObjectError('Object not on board');
+ }
+ const newPos = pos.clone();
+ newPos.y = (newPos.y === 0) ? this.yMax : (newPos.y - 1);
+ this.objects.set(object, this.normalizeCoord(newPos));
+ }
+
+ moveDown (object) {
+ const pos = this.objects.get(object);
+ if (!pos) {
+ throw new BoardObjectError('Object not on board');
+ }
+ const newPos = pos.clone();
+ newPos.y = (newPos.y === this.yMax) ? 0 : (newPos.y + 1);
+ this.objects.set(object, newPos);
+ }
+
+ moveLeft (object) {
+ const pos = this.objects.get(object);
+ if (!pos) {
+ throw new BoardObjectError('Object not on board');
+ }
+ const newPos = pos.clone();
+ newPos.x = (newPos.x === 0) ? this.xMax : (newPos.x - 1);
+ this.objects.set(object, newPos);
+ }
+
+ moveRight (object) {
+ const pos = this.objects.get(object);
+ if (!pos) {
+ throw new BoardObjectError('Object not on board');
+ }
+ const newPos = pos.clone();
+ newPos.x = (newPos.x === this.xMax) ? 0 : (newPos.x + 1);
+ this.objects.set(object, newPos);
+ }
+}
diff --git a/resources/game.test.js b/resources/game.test.js
new file mode 100644
index 000000000..27d7849c4
--- /dev/null
+++ b/resources/game.test.js
@@ -0,0 +1,60 @@
+import * as game from './game.js';
+
+QUnit.module('game', (hooks) => {
+ let board;
+
+ hooks.beforeEach(() => {
+ board = new game.Board(3, 3);
+ });
+
+ QUnit.module('Coord', () => {
+ QUnit.test('constructor', (assert) => {
+ const pos1 = new game.Coord();
+ assert.propContains(pos1, { x: 0, y: 0 }, 'defaults');
+
+ const pos2 = new game.Coord(5, 7);
+ assert.propContains(pos2, { x: 5, y: 7 }, 'parameters');
+ });
+ });
+
+ QUnit.module('Board', () => {
+ QUnit.test('add', (assert) => {
+ const alice = new game.Robot();
+ board.add(alice);
+ assert.propContains(board.find(alice), { x: 0, y: 0 }, 'add alice');
+
+ const bob = new game.Robot();
+ board.add(bob);
+ assert.propContains(board.find(bob), { x: 0, y: 0 }, 'add bob');
+
+ assert.throws(() => {
+ board.add(bob);
+ }, game.BoardObjectError, 'add second bob');
+ });
+
+ QUnit.test('find [absent]', (assert) => {
+ const bob = new game.Robot();
+ assert.false(board.find(bob));
+ });
+
+ QUnit.test('move', (assert) => {
+ const alice = new game.Robot();
+ board.add(alice);
+
+ board.moveRight(alice);
+ assert.propContains(board.find(alice), { x: 1, y: 0 }, 'move');
+
+ board.moveRight(alice);
+ assert.propContains(board.find(alice), { x: 2, y: 0 }, 'move again');
+
+ board.moveRight(alice);
+ assert.propContains(board.find(alice), { x: 0, y: 0 }, 'teleport back');
+
+ const bob = new game.Robot();
+
+ assert.throws(() => {
+ board.moveRight(bob);
+ }, board.BoardObjectError, 'not on board');
+ });
+ });
+});
diff --git a/upgrade-guide-2.x/index.html b/upgrade-guide-2.x/index.html
index f2ef2f94a..6fc8f68ce 100644
--- a/upgrade-guide-2.x/index.html
+++ b/upgrade-guide-2.x/index.html
@@ -17,8 +17,8 @@
+ DocumentationAbout
+