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:pass@127.0.0.1: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('redirected control document supplies the final same-origin base; foreign navigation is refused', t => { const { connection, frames, connections, errors } = setup(t); connection.load('/public/entry.html'); frames[0].contentWindow = { location: { href: base + 'public/gallery/control.html' } }; frames[0].handlers.load(); assert.equal(connection.url, base + 'public/gallery/control.html'); assert.equal(connections.length, 1); frames[0].contentWindow.location.href = 'https://other.example/'; frames[0].handlers.load(); assert.equal(connection.frame, null); assert.equal(errors[0].code, 'INVALID_LOCATION'); assert.equal(connections.length, 1); }); 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' }); } });