diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..26f0f3b --- /dev/null +++ b/.github/workflows/build.yml @@ -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 diff --git a/devtests/json.test.js b/devtests/json.test.js new file mode 100644 index 0000000..76ff137 --- /dev/null +++ b/devtests/json.test.js @@ -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()); + }); + +}); diff --git a/devtests/json.test.ts b/devtests/json.test.ts new file mode 100644 index 0000000..4583936 --- /dev/null +++ b/devtests/json.test.ts @@ -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(eventCharDta)) +} + +export function jsonParseEventBase(): void { + assertEventBase(JSON.parse(eventBaseDta)) +} + +export function jsonParseEventBaseCharMsg(): void { + // assertEventBaseCharMsg(JSON.parse(eventBaseCharMsgDta)) +} + +export function jsonParseEventBaseCharMsgCharBeforeMsg(): void { + // assertEventBaseCharMsg(JSON.parse(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(eventBaseCharMsgDtaCharBeforeMsg)) +} diff --git a/devtests/room.test.js b/devtests/room.test.js new file mode 100644 index 0000000..e96b07a --- /dev/null +++ b/devtests/room.test.js @@ -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' ]); + }); + +}); diff --git a/devtests/room.test.ts b/devtests/room.test.ts new file mode 100644 index 0000000..5602b82 --- /dev/null +++ b/devtests/room.test.ts @@ -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()}.`) +} diff --git a/devtests/testutils.js b/devtests/testutils.js new file mode 100644 index 0000000..9c21dce --- /dev/null +++ b/devtests/testutils.js @@ -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)), + }; +} diff --git a/docs/documentation.md b/docs/documentation.md index de2795e..7367f2f 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -329,6 +329,7 @@ export function onCommand(         [method from](#method-json-value-from)         [method set](#method-json-value-set)         [method get](#method-json-value-get) +        [method as](#method-json-value-as)         [method toString](#method-json-value-tostring)

Room namespace

@@ -2326,6 +2327,22 @@ Gets the value of the JSON.Value instance. * (T): The encapsulated value. +--- + +

method JSON.Value.as

+ +```ts +as(): T +``` + +Gets the value of the JSON.Value instance. +Alias for .get() + +

Returns

+ +* (T): The encapsulated value. + + ---

method JSON.Value.toString

diff --git a/package-lock.json b/package-lock.json index b485b9f..335ba88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "assemblyscript": "^0.28.2", "isomorphic-ws": "^5.0.0", - "json-as": "1.1.9", + "json-as": "github:anisus/json-as", "resclient": "^2.5.0", "tinyargs": "^0.1.4", "visitor-as": "^0.11.4" @@ -502,9 +502,9 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -1085,9 +1085,8 @@ } }, "node_modules/json-as": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/json-as/-/json-as-1.1.9.tgz", - "integrity": "sha512-ts7mro8xtgZz7eiuxSLrqVqwXBrWm2J0vHgqVFD1RR2QNfNGQKjiXHXvaGzp1uLY4Vf+76yHfq8mEyxx+02ZKw==" + "version": "1.1.16", + "resolved": "git+ssh://git@github.com/anisus/json-as.git#f9e640b23b1e3c311dbb7ca2d48219ed9e779717" }, "node_modules/json-buffer": { "version": "3.0.1", @@ -1581,9 +1580,9 @@ } }, "node_modules/typedoc/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "dependencies": { "balanced-match": "^1.0.0" diff --git a/package.json b/package.json index baf16f6..71da145 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.66.1", "scripts": { "test": "node tests", + "test:dev": "node --test", "build": "asc scripts/index.ts --runtime minimal --exportRuntime --lib ./lib/host.ts --lib ./lib/env.ts", "build:types": "node devbin/buildtypes.js", "build:docs": "node devbin/builddocs.js" @@ -36,7 +37,7 @@ "dependencies": { "assemblyscript": "^0.28.2", "isomorphic-ws": "^5.0.0", - "json-as": "1.1.9", + "json-as": "github:anisus/json-as", "resclient": "^2.5.0", "tinyargs": "^0.1.4", "visitor-as": "^0.11.4"