Files

140 lines
9.7 KiB
TypeScript

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 <script>window.__textViewExecuted=true</script>';
const session = new module.BrowserSession(JSON.stringify({ nodes: [{ path: '/', name: '', displayPath: '/', title, href: '/', html: '<p>body only on call</p>' }] }));
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 <script>window.__textViewExecuted=true</script>";\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 });
});