Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Build

on:
push:
branches:
- develop
pull_request:

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm install

- name: Build docs
run: npm run build:docs
- name: Fail if documentation.md changed
run: |
if git diff --exit-code ./docs/documentation.md; then
echo "documentation.md unchanged."
else
echo "::error file=docs/documentation.md::documentation.md is out of date. Please rebuild docs and commit the changes."
exit 1
fi

- name: Build types
run: npm run build:types
- name: Fail if index.d.ts changed
run: |
if git diff --exit-code ./lib/types/host/index.d.ts; then
echo "index.d.ts unchanged."
else
echo "::error file=lib/types/host/index.d.ts::index.d.ts is out of date. Please rebuild types and commit the changes."
exit 1
fi

- name: Test lib
run: npm run test:dev

- name: Build example
run: npm run build

- name: Test example
run: npm run test
38 changes: 38 additions & 0 deletions devtests/json.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it, afterEach } from 'node:test';
import assert from 'assert';
import { buildScript, resolvePath } from './testutils.js';

const script = await buildScript(resolvePath(import.meta.url, 'json.test.ts'));

await describe('JSON', async () => {

afterEach(() => {
// Garbage collect after each test.
script.__collect();
});

it('jsonParseEventChar parses an Event.Char json structure', () => {
assert.doesNotThrow(() => script.jsonParseEventChar());
});

it('jsonParseEventBase parses an Event.Base json structure', () => {
assert.doesNotThrow(() => script.jsonParseEventBase());
});

it('jsonParseEventBaseCharMsg parses an Event.BaseCharMsg json structure', () => {
assert.doesNotThrow(() => script.jsonParseEventBaseCharMsg());
});

it('jsonParseEventBaseCharMsgCharBeforeMsg parses an Event.BaseCharMsg json structure with char appearing before msg', () => {
assert.doesNotThrow(() => script.jsonParseEventBaseCharMsgCharBeforeMsg());
});

it('eventGetTypeBaseCharMsg calls Event.getType on a Event.BaseCharMsg json structure', () => {
assert.doesNotThrow(() => script.eventGetTypeBaseCharMsg());
});

it('jsonParseEventSay parses an Event.Say json structure', () => {
assert.doesNotThrow(() => script.jsonParseEventSay());
});

});
59 changes: 59 additions & 0 deletions devtests/json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Event.Char
const charId = "aaaabbbbccccddddeeee"
const charName = "Jane"
const charSurname = "Doe"
const eventCharDta = `{"id":"${charId}","name":"${charName}","surname":"${charSurname}"}`
function assertEventChar(char: Event.Char): void {
assert(char.id == charId, `expected char.id to be "${charId}", but got "${char.id}".`)
assert(char.name == charName, `expected char.name to be "${charName}", but got "${char.name}".`)
assert(char.surname == charSurname, `expected char.surname to be "${charSurname}", but got "${char.surname}".`)
}

// Event.Base
const baseId = "aaaaaaaaaaaaaaaaaaaa"
const baseType = "foo"
const baseTime = 1136210645000
const baseSig = "SIGNATUREAAABBBCCC"
const eventBaseDta = `{"id":"${baseId}","type":"${baseType}","time":${baseTime.toString()},"sig":"${baseSig}"}`
function assertEventBase(base: Event.Base): void {
assert(base.id == baseId, `expected base.id to be "${baseId}", but got "${base.id}".`)
assert(base.type == baseType, `expected base.type to be "${baseType}", but got "${base.type}".`)
assert(base.time == baseTime, `expected base.time to be ${baseTime.toString()}, but got ${base.time.toString()}.`)
assert(base.sig == baseSig, `expected base.sig to be "${baseSig}", but got "${base.sig}".`)
}

// Event.BaseCharMsg
const baseMsg = "Lorem ipsum"
const eventBaseCharMsgDta = `{"id":"${baseId}","type":"${baseType}","time":${baseTime.toString()},"sig":"${baseSig}","msg":"${baseMsg}","char":${eventCharDta}}`
const eventBaseCharMsgDtaCharBeforeMsg = `{"char":${eventCharDta},"id":"${baseId}","type":"${baseType}","time":${baseTime.toString()},"sig":"${baseSig}","msg":"${baseMsg}"}`
function assertEventBaseCharMsg(baseCharMsg: Event.BaseCharMsg): void {
assertEventBase(baseCharMsg as Event.Base)
assertEventChar(baseCharMsg.char)
assert(baseCharMsg.msg == baseMsg, `expected baseCharMsg.msg to be "${baseMsg}", but got "${baseCharMsg.msg}".`)
}


export function jsonParseEventChar(): void {
// assertEventChar(JSON.parse<Event.Char>(eventCharDta))
}

export function jsonParseEventBase(): void {
assertEventBase(JSON.parse<Event.Base>(eventBaseDta))
}

export function jsonParseEventBaseCharMsg(): void {
// assertEventBaseCharMsg(JSON.parse<Event.BaseCharMsg>(eventBaseCharMsgDta))
}

export function jsonParseEventBaseCharMsgCharBeforeMsg(): void {
// assertEventBaseCharMsg(JSON.parse<Event.BaseCharMsg>(eventBaseCharMsgDtaCharBeforeMsg))
}

export function eventGetTypeBaseCharMsg(): void {
// const t = Event.getType(eventBaseCharMsgDta)
// assert(t == baseType, `expected base.type to be "${baseType}", but got "${t}".`)
}

export function jsonParseEventSay(): void {
// assertEventBaseCharMsg(JSON.parse<Event.Say>(eventBaseCharMsgDtaCharBeforeMsg))
}
112 changes: 112 additions & 0 deletions devtests/room.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'assert';
import { buildScript, resolvePath, mockGlobal } from './testutils.js';

const script = await buildScript(resolvePath(import.meta.url, 'room.test.ts'));

await describe('Room', async () => {

beforeEach(() => {
mockGlobal(global);
});

afterEach(() => {
// Garbage collect after each test.
script.__collect();
});

it('abortWithString aborts and throws error', () => {
assert.throws(() => script.abortWithString());
});

it('roomDescribe calls room.describe', () => {
assert.doesNotThrow(() => script.roomDescribe());
assert.strictEqual(global.room.describe.mock.callCount(), 1);
assert.deepStrictEqual(global.room.describe.mock.calls[0].arguments, [ "foo" ]);
});

it('roomListen without arguments calls room.listen', () => {
global.room.listen = mock.fn((instance) => true);
assert.doesNotThrow(() => script.roomListen(true, null));
assert.strictEqual(global.room.listen.mock.callCount(), 1);
assert.deepStrictEqual(global.room.listen.mock.calls[0].arguments, [ 0, null ]);
});

it('roomListen with instance calls room.listen', () => {
global.room.listen = mock.fn((instance) => false);
assert.doesNotThrow(() => script.roomListen(false, "instance"));
assert.strictEqual(global.room.listen.mock.callCount(), 1);
assert.deepStrictEqual(global.room.listen.mock.calls[0].arguments, [ 0, 'instance' ]);
});

it('roomUnlisten without arguments calls room.unlisten', () => {
global.room.unlisten = mock.fn((instance) => true);
assert.doesNotThrow(() => script.roomUnlisten(true, null));
assert.strictEqual(global.room.unlisten.mock.callCount(), 1);
assert.deepStrictEqual(global.room.unlisten.mock.calls[0].arguments, [ 0, null ]);
});

it('roomUnlisten with instance calls room.unlisten', () => {
global.room.unlisten = mock.fn((instance) => false);
assert.doesNotThrow(() => script.roomUnlisten(false, "instance"));
assert.strictEqual(global.room.unlisten.mock.callCount(), 1);
assert.deepStrictEqual(global.room.unlisten.mock.calls[0].arguments, [ 0, 'instance' ]);
});

it('roomListenCharEvent without arguments calls room.listen', () => {
global.room.listen = mock.fn((instance) => true);
assert.doesNotThrow(() => script.roomListenCharEvent(true, null));
assert.strictEqual(global.room.listen.mock.callCount(), 1);
assert.deepStrictEqual(global.room.listen.mock.calls[0].arguments, [ 1, null ]);
});

it('roomListenCharEvent with instance calls room.listen', () => {
global.room.listen = mock.fn((instance) => false);
assert.doesNotThrow(() => script.roomListenCharEvent(false, "instance"));
assert.strictEqual(global.room.listen.mock.callCount(), 1);
assert.deepStrictEqual(global.room.listen.mock.calls[0].arguments, [ 1, 'instance' ]);
});

it('roomUnlistenCharEvent without arguments calls room.unlisten', () => {
global.room.unlisten = mock.fn((instance) => true);
assert.doesNotThrow(() => script.roomUnlistenCharEvent(true, null));
assert.strictEqual(global.room.unlisten.mock.callCount(), 1);
assert.deepStrictEqual(global.room.unlisten.mock.calls[0].arguments, [ 1, null ]);
});

it('roomUnlistenCharEvent with instance calls room.unlisten', () => {
global.room.unlisten = mock.fn((instance) => false);
assert.doesNotThrow(() => script.roomUnlistenCharEvent(false, "instance"));
assert.strictEqual(global.room.unlisten.mock.callCount(), 1);
assert.deepStrictEqual(global.room.unlisten.mock.calls[0].arguments, [ 1, 'instance' ]);
});

it('roomListenExit without arguments calls room.listenExit', () => {
global.room.listenExit = mock.fn((instance) => true);
assert.doesNotThrow(() => script.roomListenExit(true, null));
assert.strictEqual(global.room.listenExit.mock.callCount(), 1);
assert.deepStrictEqual(global.room.listenExit.mock.calls[0].arguments, [ null ]);
});

it('roomListenExit with instance calls room.listenExit', () => {
global.room.listenExit = mock.fn((instance) => false);
assert.doesNotThrow(() => script.roomListenExit(false, "exitId"));
assert.strictEqual(global.room.listenExit.mock.callCount(), 1);
assert.deepStrictEqual(global.room.listenExit.mock.calls[0].arguments, [ 'exitId' ]);
});

it('roomUnlistenExit without arguments calls room.unlistenExit', () => {
global.room.unlistenExit = mock.fn((instance) => true);
assert.doesNotThrow(() => script.roomUnlistenExit(true, null));
assert.strictEqual(global.room.unlistenExit.mock.callCount(), 1);
assert.deepStrictEqual(global.room.unlistenExit.mock.calls[0].arguments, [ null ]);
});

it('roomUnlistenExit with instance calls room.unlistenExit', () => {
global.room.unlistenExit = mock.fn((instance) => false);
assert.doesNotThrow(() => script.roomUnlistenExit(false, "exitid"));
assert.strictEqual(global.room.unlistenExit.mock.callCount(), 1);
assert.deepStrictEqual(global.room.unlistenExit.mock.calls[0].arguments, [ 'exitid' ]);
});

});
37 changes: 37 additions & 0 deletions devtests/room.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export function abortWithString(): void {
abort("foo error", "room.test.js", 2, 1)
}

export function roomDescribe(): void {
Room.describe("foo")
}

export function roomListen(expected: boolean, instance: string | null = null): void {
const returnValue = Room.listen(instance)
assert(returnValue == expected, `expected return value to be ${expected.toString()}, but got ${returnValue.toString()}.`)
}

export function roomUnlisten(expected: boolean, instance: string | null = null): void {
const returnValue = Room.unlisten(instance)
assert(returnValue == expected, `expected return value to be ${expected.toString()}, but got ${returnValue.toString()}.`)
}

export function roomListenCharEvent(expected: boolean, instance: string | null = null): void {
const returnValue = Room.listenCharEvent(instance)
assert(returnValue == expected, `expected return value to be ${expected.toString()}, but got ${returnValue.toString()}.`)
}

export function roomUnlistenCharEvent(expected: boolean, instance: string | null = null): void {
const returnValue = Room.unlistenCharEvent(instance)
assert(returnValue == expected, `expected return value to be ${expected.toString()}, but got ${returnValue.toString()}.`)
}

export function roomListenExit(expected: boolean, exitId: string | null = null): void {
const returnValue = Room.listenExit(exitId)
assert(returnValue == expected, `expected return value to be ${expected.toString()}, but got ${returnValue.toString()}.`)
}

export function roomUnlistenExit(expected: boolean, exitId: string | null = null): void {
const returnValue = Room.unlistenExit(exitId)
assert(returnValue == expected, `expected return value to be ${expected.toString()}, but got ${returnValue.toString()}.`)
}
66 changes: 66 additions & 0 deletions devtests/testutils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@

import path from 'path';
import { mock } from 'node:test';
import { rootDir, compileScript } from '../bin/utils/tools.js';
import { fileURLToPath, pathToFileURL} from "url";

const buildDir = path.join(rootDir, 'build');

/**
* Resolves a full path from a import.meta.url to a file in posix format.
* @param {string} url Import meta url to resolve from
* @param {string} filePath File path (with posix separators is used)
*/
export function resolvePath(url, filePath) {
return path.join(path.dirname(fileURLToPath(url)), filePath.replaceAll(path.posix.sep, path.sep));
}

export async function buildScript(scriptPath) {
const filename = path.parse(scriptPath).name;
const outFile = path.join(buildDir, filename + '.wasm');
const textFile = path.join(buildDir, filename + '.wat');
const scriptFile = path.join(buildDir, filename + '.js');

try {
compileScript(scriptPath, outFile, textFile);
} catch (err) {
let errMsg = err?.stderr?.toString
? err.stderr.toString()
: err;
throw errMsg;
}

const script = await import(pathToFileURL(scriptFile));

return script;
}

export function mockGlobal(global) {
global.room = {
describe: mock.fn((msg) => {}),
listen: mock.fn((instance) => {}),
unlisten: mock.fn((instance) => {}),
getRoom: mock.fn(() => JSON.stringify(room)),
setRoom: mock.fn((json) => {}),
useProfile: mock.fn((key, safe) => {}),
sweepChar: mock.fn((charId, msg) => {}),
canEdit: mock.fn((charId) => true),
};
global.script = {
post: mock.fn((addr, topic, data, delay) => {}),
listen: mock.fn((addrs) => {}),
unlisten: mock.fn((addrs) => {}),
};
global.store = {
setItem: mock.fn((key, item) => {}),
getItem: mock.fn((key) => null),
totalBytes: mock.fn(() => 0),
newIterator: mock.fn((prefix, reverse) => iterator++),
iteratorClose: mock.fn((iterator) => {}),
iteratorSeek: mock.fn((iterator, key) => {}),
iteratorNext: mock.fn((iterator) => {}),
iteratorValid: mock.fn((iterator, prefix) => false),
iteratorKey: mock.fn((iterator) => new ArrayBuffer(1)),
iteratorItem: mock.fn((iterator) => new ArrayBuffer(1)),
};
}
Loading