140 lines
9.7 KiB
TypeScript
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 });
|
|
});
|