import { expect, test } from '@playwright/test'; test.use({ javaScriptEnabled: true }); test('manual exchanges are explicit, cached, session-local, and return genuine signatures', async ({ page }, testInfo) => { const base = testInfo.project.use.baseURL!; await page.goto(new URL('worldspace.json', base).href); const result = await page.evaluate(async (base) => { const manifest = await (await fetch(new URL('worldspace.json', base))).text(); const module = await import(new URL('wip-wasm/wip_browser.js', base).href); await module.default(); const normal = new module.BrowserSession(manifest); const manual = new module.BrowserSession(manifest); const data = JSON.parse(manifest); const node = data.nodes.find((n: { path: string }) => n.path !== '/'); const originalFetch = window.fetch; window.fetch = () => { throw new Error('Manual protocol must not use a network transport'); }; try { normal.tree(); const initial = JSON.parse(manual.stats()); let missing; try { manual.call_operation(node.path, '/', 'document', 'read', '{}'); } catch (error) { missing = JSON.parse(String(error)); } const observed = JSON.parse(manual.observe(node.path, 0)); const cachedObject = JSON.parse(manual.observe(node.path, 0)); const descriptor = JSON.parse(manual.fetch_interface('/', 'document')); const cachedInterface = JSON.parse(manual.fetch_interface('/', 'document')); const called = JSON.parse(manual.call_operation(node.path, '/', 'document', 'read', '{}')); const second = JSON.parse(manual.call_operation(node.path, '/', 'document', 'read', '{}')); const result = { initial, missing, observed, cachedObject, descriptor, cachedInterface, called, second, node }; normal.free(); manual.free(); return result; } finally { window.fetch = originalFetch; } }, base); expect(result.initial).toEqual({ observes: 0, interfaces: 0, calls: 0 }); expect(result.missing).toMatchObject({ kind: 'client', code: 'MissingObservation' }); expect(result.observed).toMatchObject({ path: result.node.path, cached: false, stats: { observes: 1, interfaces: 0, calls: 0 } }); expect(result.observed.observation.object).toMatchObject({ name: result.node.name, description: result.node.title, interfaces: [{ scope: '/', name: 'document' }] }); expect(result.observed.observation.children).toBeNull(); expect(result.observed.observation.object).not.toHaveProperty('html'); expect(result.observed.textView).toContain('interfaces: ["/::document"];'); expect(result.observed.textView).not.toContain('validator:'); expect(result.observed.textView).not.toContain('ref:'); expect(result.cachedObject).toEqual({ ...result.observed, cached: true }); expect(result.descriptor).toMatchObject({ reference: { scope: '/', name: 'document' }, cached: false, stats: { observes: 1, interfaces: 1, calls: 0 } }); expect(result.descriptor.descriptor.operations).toEqual([expect.objectContaining({ name: 'read', parameters: [] })]); expect(result.descriptor.textView).toMatch(/^interface "\/::document" \{\n/m); expect(result.descriptor.textView).toContain('read('); expect(result.cachedInterface).toEqual({ ...result.descriptor, cached: true }); expect(result.called).toMatchObject({ path: result.node.path, interface: { scope: '/', name: 'document' }, operation: 'read', result: { kind: 'record', value: { path: { kind: 'string', value: result.node.path }, html: { kind: 'string', value: result.node.html } } }, stats: { observes: 1, interfaces: 1, calls: 1 } }); expect(result.called).not.toHaveProperty('textView'); expect(result.second.stats).toEqual({ observes: 1, interfaces: 1, calls: 2 }); }); test('manual errors distinguish input, Client, Protocol and renderer failures without automatic work', async ({ page }, testInfo) => { const base = testInfo.project.use.baseURL!; await page.goto(new URL('worldspace.json', base).href); const result = await page.evaluate(async (base) => { const manifest = await (await fetch(new URL('worldspace.json', base))).text(); const module = await import(new URL('wip-wasm/wip_browser.js', base).href); await module.default(); const session = new module.BrowserSession(manifest); const attempt = (fn: () => unknown) => { try { fn(); return { kind: 'unexpected success' }; } catch (error) { if (error instanceof WebAssembly.RuntimeError) throw error; return JSON.parse(String(error)); } }; const missingObject = attempt(() => session.call_operation('/', '/', 'document', 'read', '{}')); const depth = [-1, 0.1, 33, NaN, Infinity, 2 ** 32].map((d) => attempt(() => session.observe('/', d))); const oversizePath = attempt(() => session.observe('/'.repeat(4097), 0)); const malformedPath = attempt(() => session.observe('relative', 0)); const unknownPath = attempt(() => session.observe('/not-published-protocol-test', 0)); const unknownInterface = attempt(() => session.fetch_interface('/', 'not-published-protocol-test')); const afterMissing = JSON.parse(session.stats()); const retainedPath = attempt(() => session.observe('/not-published-protocol-test', 0)); const retainedInterface = attempt(() => session.fetch_interface('/', 'not-published-protocol-test')); const afterRetained = JSON.parse(session.stats()); session.observe('/', 0); const missingInterface = attempt(() => session.call_operation('/', '/', 'document', 'read', '{}')); session.fetch_interface('/', 'document'); const unknownOperation = attempt(() => session.call_operation('/', '/', 'document', 'unknown', '{}')); const mismatchedInterface = attempt(() => session.call_operation('/', '/', 'other', 'read', '{}')); const invalidArguments = attempt(() => session.call_operation('/', '/', 'document', 'read', '{"unexpected":{"kind":"integer","value":"9007199254740993"}}')); const malformedArguments = ['{', '[]', '{"x":42}', '{"x":{"kind":"integer","value":9007199254740993}}', '{"x":{"kind":"bytes","value":[256]}}', ' '.repeat(65537)].map((args) => attempt(() => session.call_operation('/', '/', 'document', 'read', args))); const beforeCall = JSON.parse(session.stats()); const called = JSON.parse(session.call_operation('/', '/', 'document', 'read', '{}')); session.free(); const data = JSON.parse(manifest); data.nodes.find((n: { path: string }) => n.path === '/').title = 'x'.repeat(1024 * 1024 + 1); const large = new module.BrowserSession(JSON.stringify(data)); const render = attempt(() => large.observe('/', 0)); const renderStats = JSON.parse(large.stats()); large.free(); return { missingObject, missingInterface, depth, oversizePath, malformedPath, unknownPath, unknownInterface, afterMissing, retainedPath, retainedInterface, afterRetained, unknownOperation, mismatchedInterface, invalidArguments, malformedArguments, beforeCall, called, render, renderStats }; }, base); expect(result.missingObject).toMatchObject({ kind: 'client', code: 'MissingObservation' }); expect(result.missingInterface).toMatchObject({ kind: 'client', code: 'MissingObservation' }); for (const error of [...result.depth, result.oversizePath, ...result.malformedArguments]) expect(error.kind).toBe('input'); expect(result.malformedPath).toMatchObject({ kind: 'client', code: 'InvalidRequest' }); expect(result.unknownPath).toMatchObject({ kind: 'protocol', code: 'NotFound' }); expect(result.unknownInterface).toMatchObject({ kind: 'protocol', code: 'InterfaceNotFound' }); expect(result.afterMissing).toEqual({ observes: 1, interfaces: 1, calls: 0 }); expect(result.afterRetained).toEqual(result.afterMissing); expect(result.retainedPath.kind).toBe('client'); expect(result.retainedInterface.kind).toBe('client'); for (const error of [result.unknownOperation, result.mismatchedInterface, result.invalidArguments]) expect(error.kind).toBe('client'); expect(result.beforeCall.calls).toBe(0); expect(result.called.stats.calls).toBe(1); expect(result.render.kind).toBe('render'); expect(result.renderStats).toEqual({ observes: 1, interfaces: 0, calls: 0 }); }); test('Rust Text View preserves Unicode, whitespace and quoted escapes without becoming HTML', async ({ page }, testInfo) => { const base = testInfo.project.use.baseURL!; await page.goto(new URL('worldspace.json', base).href); const result = await page.evaluate(async (base) => { const module = await import(new URL('wip-wasm/wip_browser.js', base).href); await module.default(); const title = ' 引用 "quote" \\ space\nnext\tline '; const session = new module.BrowserSession(JSON.stringify({ nodes: [{ path: '/', name: '', displayPath: '/', title, href: '/', html: '
body only on call
' }] })); const observed = JSON.parse(session.observe('/', 0)); const fetched = JSON.parse(session.fetch_interface('/', 'document')); const pre = document.createElement('pre'); pre.textContent = observed.textView; document.body.append(pre); session.free(); return { observed, fetched, title, displayed: pre.textContent, scripts: pre.querySelectorAll('script').length, executed: '__textViewExecuted' in window }; }, base); // Golden output is also checked against render_object directly in native tests. expect(result.observed.textView).toBe('object {\n name: "";\n description: " 引用 \\"quote\\" \\\\ space\\nnext\\tline ";\n interfaces: ["/::document"];\n}\n'); expect(result.observed.observation.object.description).toBe(result.title); expect(result.displayed).toBe(result.observed.textView); expect(result.scripts).toBe(0); expect(result.executed).toBe(false); expect(result.fetched.stats).toEqual({ observes: 1, interfaces: 1, calls: 0 }); });