generated from Labyricorn/labyricorn-project-template
Step 5 Complete
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { ExhibitConnection, exhibitURL } from '../src/connection.js';
|
||||
import { validateCatalog } from '../src/validation.js';
|
||||
|
||||
const base = 'http://127.0.0.1:4173/';
|
||||
test('connection accepts served paths and same-origin URLs, rejects unsupported locations', () => {
|
||||
assert.equal(exhibitURL('/public/example/index.html', base), base + 'public/example/index.html');
|
||||
assert.equal(exhibitURL(' public/example/ ', base), base + 'public/example/');
|
||||
for (const value of ['', ' ', 'file:///C:/exhibit.html', 'https://other.example/', 'http://127.0.0.1:4174/', 'javascript:alert(1)', 'http://user:[email protected]:4173/']) {
|
||||
assert.throws(() => exhibitURL(value, base));
|
||||
}
|
||||
});
|
||||
function setup(t, loadTimeoutMs = 1000) {
|
||||
const frames = [], connections = [], errors = [];
|
||||
const host = { disconnect() {}, async connect(frame) { connections.push(frame); }, report(e) { errors.push(e); } };
|
||||
const connection = new ExhibitConnection({ host, base, loadTimeoutMs, transport: frame => frame,
|
||||
createFrame() {
|
||||
const frame = { handlers: {}, addEventListener(type, fn) { this.handlers[type] = fn; }, remove() { this.removed = true; } };
|
||||
frames.push(frame); return frame;
|
||||
}
|
||||
});
|
||||
t.after(() => connection.disconnect());
|
||||
return { connection, frames, connections, errors };
|
||||
}
|
||||
test('switch and disconnect cancel old frame loads; invalid URL preserves active frame', async t => {
|
||||
const { connection, frames, connections } = setup(t);
|
||||
connection.load('/public/one/'); connection.load('/public/two/');
|
||||
frames[0].handlers.load(); assert.equal(connections.length, 0);
|
||||
frames[1].handlers.load(); assert.equal(connections.length, 1);
|
||||
assert.throws(() => connection.load('https://other.example/'));
|
||||
assert.equal(connection.frame, frames[1]);
|
||||
connection.disconnect(); frames[1].handlers.load();
|
||||
assert.equal(connections.length, 1); assert.equal(connection.frame, null);
|
||||
assert.ok(frames.every(f => f.removed));
|
||||
});
|
||||
test('reconnect negotiates existing frame and reloads remembered URL after disconnect', async t => {
|
||||
const { connection, frames, connections } = setup(t);
|
||||
connection.load('/public/one/'); frames[0].handlers.load();
|
||||
await connection.reconnect(); assert.equal(connections.length, 2); assert.equal(frames.length, 1);
|
||||
connection.disconnect(); connection.reconnect();
|
||||
assert.equal(frames.length, 2); assert.equal(frames[1].src, base + 'public/one/');
|
||||
frames[1].handlers.load(); assert.equal(connections.length, 3);
|
||||
});
|
||||
test('stalled document load ends with recoverable error', async t => {
|
||||
const { connection, frames, errors } = setup(t, 5);
|
||||
connection.load('/public/stalled/');
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
assert.equal(connection.loading, false); assert.equal(connection.frame, null);
|
||||
assert.equal(errors[0].code, 'LOAD_TIMEOUT');
|
||||
connection.reconnect(); assert.equal(frames.length, 2);
|
||||
});
|
||||
test('optional descriptor metadata is permissive when omitted and typed when supplied', () => {
|
||||
const message = { exhibit: {}, contract: { major: 5 }, registryRevision: 0, stateRevision: 0, capabilities: [],
|
||||
targets: [{ id: 'sample.enabled', kind: 'state', readable: true, writable: true, valueType: 'boolean', requires: [] }] };
|
||||
validateCatalog(message);
|
||||
for (const metadata of [{ restorable: false, category: '' }, { restorable: true, category: 'Custom category' }]) {
|
||||
validateCatalog({ ...message, targets: [{ ...message.targets[0], ...metadata }] });
|
||||
}
|
||||
for (const metadata of [{ restorable: 'false' }, { restorable: null }, { restorable: 1 }, { category: null }, { category: [] }, { category: false }]) {
|
||||
assert.throws(() => validateCatalog({ ...message, targets: [{ ...message.targets[0], ...metadata }] }), { code: 'INVALID_MESSAGE' });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
import { validateReportedValue } from '../src/validation.js';
|
||||
|
||||
function setup() {
|
||||
const context = vm.createContext({ window: {} });
|
||||
for (const file of ['shared/contract-core.js', 'haunted-house/exhibit.js', 'haunted-house/contract-adapter.js']) {
|
||||
vm.runInContext(readFileSync(new URL(`../test-fixtures/reference-exhibits/${file}`, import.meta.url), 'utf8'), context);
|
||||
}
|
||||
const exhibit = new context.window.HauntedHouseExhibit({
|
||||
canvas: { getContext: () => ({ setTransform() {} }), getBoundingClientRect: () => ({ width: 100, height: 100 }) },
|
||||
announcer: { flash() {} }
|
||||
});
|
||||
const core = context.window.HauntedHouseContract.create(exhibit, { sync() {} });
|
||||
const descriptor = core.catalog.get('room.haunt-level');
|
||||
assert.deepEqual([descriptor.min, descriptor.max, descriptor.step], [0, 1, 0.01]);
|
||||
return { exhibit, core, descriptor };
|
||||
}
|
||||
|
||||
test('Haunted House reports bounded hundredths while preserving continuous drift', () => {
|
||||
const { exhibit, core, descriptor } = setup();
|
||||
for (const [raw, expected] of [[-0.1, 0], [0, 0], [0.0049, 0], [0.0051, 0.01], [0.303, 0.3], [0.999, 1], [1, 1], [1.1, 1]]) {
|
||||
exhibit.house.hauntLevel = raw;
|
||||
assert.equal(core.readValue(descriptor.id), expected);
|
||||
assert.equal(exhibit.house.hauntLevel, raw, 'reading must not quantize the simulation');
|
||||
}
|
||||
for (const room of ['foyer', 'library', 'cellar', 'attic', 'nursery']) {
|
||||
exhibit.setRoom(room);
|
||||
for (const spirit of ['restless', 'vengeful', 'mourning', 'mischievous']) {
|
||||
exhibit.setSpirit(spirit);
|
||||
exhibit.house.hauntLevel = 0.303;
|
||||
for (let i = 0; i < 600; i++) {
|
||||
exhibit.house.step(1 / 60);
|
||||
validateReportedValue(descriptor, core.readValue(descriptor.id));
|
||||
}
|
||||
assert.notEqual(exhibit.house.hauntLevel, 0.303, 'simulation must keep drifting');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Haunted House snapshots and mutation events stay valid through Manifest, drift and Calm', () => {
|
||||
const { exhibit, core, descriptor } = setup();
|
||||
core.handleHello({ supportedContractMajors: [5] });
|
||||
const checkSnapshot = () => {
|
||||
const snapshot = core.stateSnapshot();
|
||||
for (const target of core.catalog.descriptors().filter(t => t.readable)) validateReportedValue(target, snapshot.values[target.id]);
|
||||
return snapshot.values[descriptor.id];
|
||||
};
|
||||
assert.equal(core.invokeAction('action.manifest', {}, 'host').ok, true);
|
||||
assert.equal(exhibit.house.hauntLevel, 1);
|
||||
assert.equal(checkSnapshot(), 1);
|
||||
for (let i = 0; i < 600; i++) { exhibit.house.step(1 / 60); checkSnapshot(); }
|
||||
for (const value of [0, 0.01, 0.29, 0.5, 0.99, 1]) {
|
||||
assert.equal(core.applyMutation(descriptor.id, value, 'ui').ok, true);
|
||||
assert.equal(checkSnapshot(), value);
|
||||
}
|
||||
for (const event of core.eventLog().filter(e => e.type === 'state.changed')) validateReportedValue(core.catalog.get(event.target), event.value);
|
||||
assert.ok(core.eventLog().some(e => e.type === 'action.executed' && e.target === 'action.manifest'));
|
||||
assert.equal(core.invokeAction('action.calm', {}, 'host').ok, true);
|
||||
assert.equal(checkSnapshot(), 0);
|
||||
});
|
||||
Reference in New Issue
Block a user