Reference site / build (push) Canceled after 0s
Expose Rust Text View tokens and safely highlight signatures and JSON results. Add animated Interface and inline Run disclosures with a shared 10ms line cadence, typed argument inputs, and explicit execution. Update documentation and browser regressions for the new controls and explicit counter-value responses.
236 lines
16 KiB
TypeScript
236 lines
16 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
|
|
|
test.use({ javaScriptEnabled: true, reducedMotion: 'reduce' });
|
|
const panel = (page: Page) => page.locator('[data-protocol-panel]');
|
|
const node = (page: Page, name: string) => page.locator(`.demo-node[data-path="/playground${name ? `/${name}` : ''}"]`);
|
|
const url = (base: string, child?: string) => {
|
|
const result = new URL('playground/?mode=protocol', base);
|
|
if (child) result.searchParams.set('object', `/playground/${child}`);
|
|
return result.href;
|
|
};
|
|
async function ready(page: Page, path = '/playground') {
|
|
await expect(panel(page)).toHaveAttribute('data-state', 'ready');
|
|
await expect(panel(page)).toHaveAttribute('data-path', path);
|
|
await expect(page.getByLabel('Object Text View')).toBeVisible();
|
|
}
|
|
async function openOperation(page: Page, operation: string) {
|
|
const form = page.getByRole('form', { name: `${operation} arguments`, exact: true });
|
|
if (await form.count() === 0) await page.getByRole('button', { name: `Run ${operation}`, exact: true }).click();
|
|
await expect(form).toBeVisible();
|
|
await expect(form).toHaveAttribute('data-operation-form', operation);
|
|
return form;
|
|
}
|
|
async function execute(page: Page, iface: string, operation: string, args?: Record<string, { kind: string; value: string }>) {
|
|
const toggle = page.getByRole('button', { name: new RegExp(`^[?>v] /::${iface}$`) });
|
|
if (await toggle.getAttribute('aria-expanded') !== 'true') await toggle.click();
|
|
const calls = Number(await panel(page).getAttribute('data-calls'));
|
|
const form = await openOperation(page, operation);
|
|
await expect(panel(page)).toHaveAttribute('data-calls', String(calls));
|
|
for (const [parameter, argument] of Object.entries(args ?? {})) {
|
|
await form.getByLabel(`${operation}.${parameter}`, { exact: true }).fill(argument.value);
|
|
}
|
|
await form.getByRole('button', { name: `Execute ${operation}`, exact: true }).click();
|
|
await expect(page.getByLabel('Operation result')).toBeVisible();
|
|
await expect(panel(page)).toHaveAttribute('data-calls', String(calls + 1));
|
|
await expect(page.locator('[data-protocol-status]')).toBeHidden();
|
|
return JSON.parse(await page.getByLabel('Operation result').innerText());
|
|
}
|
|
const nameArg = (name: string) => ({ name: { kind: 'string', value: name } });
|
|
const amountArg = (amount: string) => ({ amount: { kind: 'integer', value: amount } });
|
|
|
|
test('Observe updates tree membership while counter values come only from Operations', async ({ page }, info) => {
|
|
await page.route('**/wip-wasm/wip_browser.js?instance=protocol', async route => {
|
|
const response = await route.fetch();
|
|
await route.fulfill({ response, body: `${await response.text()}\nconst observe = BrowserSession.prototype.observe; BrowserSession.prototype.observe = function(path, depth) { (window.__observations ||= []).push({path,depth}); return observe.call(this,path,depth); };` });
|
|
});
|
|
await page.goto(url(info.project.use.baseURL!)); await ready(page);
|
|
await expect(node(page, 'apple')).toHaveText('apple');
|
|
await expect(node(page, 'orange')).toHaveText('orange');
|
|
await expect(page.locator('.node-value')).toHaveCount(0);
|
|
await expect(page.getByRole('button', { name: '? /::collection', exact: true })).toBeVisible();
|
|
await expect(panel(page)).toHaveAttribute('data-calls', '0');
|
|
await execute(page, 'collection', 'create', nameArg('banana'));
|
|
await expect(node(page, 'banana')).toHaveText('banana');
|
|
await node(page, 'banana').click(); await ready(page, '/playground/banana');
|
|
await expect(node(page, 'banana')).toHaveAttribute('aria-selected', 'true');
|
|
await expect(page.getByRole('button', { name: '? /::counter', exact: true })).toBeVisible();
|
|
expect((await execute(page, 'counter', 'get')).result).toEqual({ kind: 'integer', value: '0' });
|
|
const metadata = await page.getByLabel('Object Text View').textContent();
|
|
const before = Number(await panel(page).getAttribute('data-observes'));
|
|
const increment = await execute(page, 'counter', 'increment', amountArg('2'));
|
|
expect(increment.result).toEqual({ kind: 'integer', value: '2' });
|
|
await expect(node(page, 'banana')).toHaveText('banana');
|
|
await expect(node(page, 'banana')).toHaveAttribute('aria-label', 'banana');
|
|
expect(await page.getByLabel('Object Text View').textContent()).toBe(metadata);
|
|
await expect(page.getByLabel('Object Text View')).toContainText('description: "An in-memory counter."');
|
|
expect(Number(await panel(page).getAttribute('data-observes'))).toBeGreaterThan(before);
|
|
expect(await page.evaluate(() => (window as Window & { __observations?: unknown[] }).__observations?.slice(-2)))
|
|
.toEqual([{ path: '/playground', depth: 1 }, { path: '/playground/banana', depth: 1 }]);
|
|
await execute(page, 'counter', 'increment', amountArg('-1'));
|
|
expect((await execute(page, 'counter', 'get')).result).toEqual({ kind: 'integer', value: '1' });
|
|
expect((await execute(page, 'counter', 'reset')).result).toEqual({ kind: 'integer', value: '0' });
|
|
expect((await execute(page, 'counter', 'get')).result).toEqual({ kind: 'integer', value: '0' });
|
|
await expect(node(page, 'banana')).toHaveText('banana');
|
|
await page.screenshot({ path: info.outputPath('playground-counter.png'), fullPage: true });
|
|
await node(page, '').click(); await ready(page);
|
|
await execute(page, 'collection', 'remove', nameArg('banana'));
|
|
await expect(node(page, 'banana')).toHaveCount(0);
|
|
await execute(page, 'collection', 'create', nameArg('banana'));
|
|
await expect(node(page, 'banana')).toHaveText('banana');
|
|
await node(page, 'banana').click(); await ready(page, '/playground/banana');
|
|
expect((await execute(page, 'counter', 'get')).result).toEqual({ kind: 'integer', value: '0' });
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
|
});
|
|
|
|
test('state survives selection, history and Read mode, but reload resets the session', async ({ page }, info) => {
|
|
const base = info.project.use.baseURL!;
|
|
await page.goto(url(base, 'apple')); await ready(page, '/playground/apple');
|
|
expect((await execute(page, 'counter', 'increment', amountArg('4'))).result).toEqual({ kind: 'integer', value: '7' });
|
|
await node(page, 'orange').click(); await ready(page, '/playground/orange');
|
|
await page.goBack(); await ready(page, '/playground/apple');
|
|
await expect(panel(page)).toHaveAttribute('data-calls', '1');
|
|
await expect(page.getByLabel('Operation result')).toBeHidden();
|
|
expect((await execute(page, 'counter', 'get')).result).toEqual({ kind: 'integer', value: '7' });
|
|
await page.goForward(); await ready(page, '/playground/orange');
|
|
await page.getByRole('button', { name: 'Read', exact: true }).click();
|
|
await expect(page.locator('[data-selected-content] h1')).toHaveText('Playground');
|
|
await expect(page.locator('[data-landing-stage]')).toHaveAttribute('data-wip-state', 'ready');
|
|
await expect(page.locator('[data-playground-row]')).toHaveCount(0);
|
|
await page.getByRole('button', { name: 'Protocol', exact: true }).click(); await ready(page, '/playground/orange');
|
|
await expect(node(page, 'apple')).toHaveText('apple');
|
|
await expect(panel(page)).toHaveAttribute('data-calls', '2');
|
|
await page.locator('.demo-node[data-path="/book"]').click();
|
|
await ready(page, '/book');
|
|
await expect(node(page, 'apple')).toHaveText('apple');
|
|
await node(page, 'apple').click(); await ready(page, '/playground/apple');
|
|
await expect(panel(page)).toHaveAttribute('data-calls', '2');
|
|
expect((await execute(page, 'counter', 'get')).result).toEqual({ kind: 'integer', value: '7' });
|
|
await expect(page.locator('.node-value')).toHaveCount(0);
|
|
await page.reload(); await ready(page, '/playground/apple');
|
|
await expect(panel(page)).toHaveAttribute('data-calls', '0');
|
|
expect((await execute(page, 'counter', 'get')).result).toEqual({ kind: 'integer', value: '3' });
|
|
});
|
|
|
|
test('invalid operations do not alter the tree and deleted history does not resurrect a counter', async ({ page }, info) => {
|
|
await page.goto(url(info.project.use.baseURL!)); await ready(page);
|
|
await execute(page, 'collection', 'create', nameArg('temporary'));
|
|
await node(page, 'temporary').click(); await ready(page, '/playground/temporary');
|
|
await node(page, '').click(); await ready(page);
|
|
await execute(page, 'collection', 'remove', nameArg('temporary'));
|
|
await page.goBack();
|
|
await expect(panel(page)).toHaveAttribute('data-path', '/playground/temporary');
|
|
await expect(page.locator('[data-protocol-status]')).toBeVisible();
|
|
await expect(page.getByLabel('Object Text View')).toBeHidden();
|
|
await expect(node(page, 'temporary')).toHaveCount(0);
|
|
await node(page, '').click(); await ready(page);
|
|
await page.getByRole('button', { name: '> /::collection', exact: true }).click();
|
|
for (const invalid of ['apple', '../book', '<img src=x onerror=alert(1)>']) {
|
|
const form = await openOperation(page, 'create');
|
|
await form.getByLabel('create.name', { exact: true }).fill(invalid);
|
|
await form.getByRole('button', { name: 'Execute create', exact: true }).click();
|
|
await expect(page.locator('[data-protocol-status]')).toBeVisible();
|
|
await expect(page.getByLabel('Operation result')).toBeHidden();
|
|
await expect(page.locator('[data-playground-row]')).toHaveCount(2);
|
|
await expect(node(page, 'apple')).toHaveText('apple');
|
|
}
|
|
await expect(panel(page).locator('img')).toHaveCount(0);
|
|
await node(page, 'apple').click(); await ready(page, '/playground/apple');
|
|
await page.getByRole('button', { name: '? /::counter', exact: true }).click();
|
|
const incrementForm = await openOperation(page, 'increment');
|
|
await incrementForm.getByLabel('increment.amount', { exact: true }).fill('9223372036854775807');
|
|
await incrementForm.getByRole('button', { name: 'Execute increment', exact: true }).click();
|
|
await expect(page.locator('[data-protocol-status]')).toBeVisible();
|
|
await expect(node(page, 'apple')).toHaveText('apple');
|
|
expect((await execute(page, 'counter', 'get')).result).toEqual({ kind: 'integer', value: '3' });
|
|
});
|
|
|
|
for (const variant of ['missing', 'null', 'prose', 'numeric'] as const) {
|
|
test(`tree membership does not depend on ${variant} descriptions or fetch counter values`, async ({ page }, info) => {
|
|
await page.route('**/wip-wasm/wip_browser.js?instance=protocol', async route => {
|
|
const response = await route.fetch();
|
|
await route.fulfill({ response, body: `${await response.text()}\nconst original = BrowserSession.prototype.observe; BrowserSession.prototype.observe = function(...args) { const value = JSON.parse(original.apply(this,args)); for (const child of value.observation.children || []) { const variant = ${JSON.stringify(variant)}; if (variant === 'missing') delete child.object.description; else child.object.description = variant === 'null' ? null : variant === 'numeric' ? '999999' : 'カウンターの説明 <img src=x onerror="window.__descriptionExecuted=1">'; } return JSON.stringify(value); };` });
|
|
});
|
|
await page.goto(url(info.project.use.baseURL!)); await ready(page);
|
|
for (const name of ['apple', 'orange']) {
|
|
await expect(node(page, name)).toHaveText(name);
|
|
await expect(node(page, name)).toHaveAttribute('aria-label', name);
|
|
}
|
|
await expect(page.locator('.node-value, [data-playground-row] img')).toHaveCount(0);
|
|
await expect(panel(page)).toHaveAttribute('data-calls', '0');
|
|
await expect(panel(page)).toHaveAttribute('data-interfaces', '0');
|
|
await node(page, 'apple').click(); await ready(page, '/playground/apple');
|
|
await expect(panel(page)).toHaveAttribute('data-calls', '0');
|
|
await expect(page.getByLabel('Operation result')).toBeHidden();
|
|
expect((await execute(page, 'counter', 'get')).result).toEqual({ kind: 'integer', value: '3' });
|
|
expect(await page.evaluate(() => '__descriptionExecuted' in window)).toBe(false);
|
|
});
|
|
}
|
|
|
|
test('dynamic children participate in tree keyboard navigation and folding', async ({ page }, info) => {
|
|
await page.goto(url(info.project.use.baseURL!)); await ready(page);
|
|
await page.locator('.demo-node[data-path="/book"]').click(); await ready(page, '/book');
|
|
await node(page, 'apple').focus(); await page.keyboard.press('Enter');
|
|
await ready(page, '/playground/apple');
|
|
await expect(node(page, 'apple')).toBeFocused();
|
|
await page.keyboard.press('ArrowDown');
|
|
await expect(node(page, 'orange')).toBeFocused();
|
|
await node(page, '').focus();
|
|
await page.keyboard.press('ArrowRight');
|
|
await expect(node(page, 'apple')).toBeFocused();
|
|
await page.keyboard.press('Enter'); await ready(page, '/playground/apple');
|
|
await expect(node(page, 'apple')).toBeFocused();
|
|
await page.keyboard.press('ArrowDown');
|
|
await expect(node(page, 'orange')).toBeFocused();
|
|
await page.keyboard.press('ArrowLeft');
|
|
await expect(node(page, '')).toBeFocused();
|
|
await page.keyboard.press('ArrowLeft');
|
|
await expect(node(page, 'apple')).toBeHidden();
|
|
await expect(node(page, '')).toHaveAttribute('aria-expanded', 'false');
|
|
await page.keyboard.press('ArrowRight');
|
|
await expect(node(page, 'apple')).toBeVisible();
|
|
});
|
|
|
|
for (const failurePath of ['/playground', '/playground/apple']) test(`failed refresh at ${failurePath} preserves success without replaying the mutation`, async ({ page }, info) => {
|
|
await page.emulateMedia({ colorScheme: 'dark' });
|
|
await page.route('**/wip-wasm/wip_browser.js?instance=protocol', async route => {
|
|
const response = await route.fetch();
|
|
await route.fulfill({ response, body: `${await response.text()}\nconst call = BrowserSession.prototype.call_operation; const observe = BrowserSession.prototype.observe; let failRefresh = false; BrowserSession.prototype.call_operation = function(...args) { const result = call.apply(this,args); if (args[3] === 'increment') failRefresh = true; return result; }; BrowserSession.prototype.observe = function(...args) { if(failRefresh && args[0] === ${JSON.stringify(failurePath)}) { failRefresh = false; throw new Error('refresh unavailable'); } return observe.apply(this,args); };` });
|
|
});
|
|
await page.goto(url(info.project.use.baseURL!, 'apple')); await ready(page, '/playground/apple');
|
|
await page.getByRole('button', { name: '? /::counter', exact: true }).click();
|
|
const form = await openOperation(page, 'increment');
|
|
await expect(form.getByLabel('increment.amount', { exact: true })).toHaveValue('1');
|
|
await form.getByRole('button', { name: 'Execute increment', exact: true }).click();
|
|
await expect(page.locator('[data-protocol-status]')).toContainText('Operationは成功しましたが再Observeに失敗');
|
|
expect(JSON.parse(await page.getByLabel('Operation result').innerText()).result).toEqual({ kind: 'integer', value: '4' });
|
|
await expect(panel(page)).toHaveAttribute('data-calls', '1');
|
|
// Stale observations and target readiness are retired, not inferred from the Call result.
|
|
if (failurePath === '/playground') await expect(node(page, 'apple')).toHaveCount(0);
|
|
else await expect(node(page, 'apple')).toHaveText('apple');
|
|
await expect(form.getByRole('button', { name: 'Execute increment', exact: true })).toBeDisabled();
|
|
await expect(form.getByLabel('increment.amount', { exact: true })).toBeDisabled();
|
|
await expect(page.getByRole('button', { name: 'Run increment', exact: true })).toBeDisabled();
|
|
await expect(page.getByLabel('Object Text View')).toBeHidden();
|
|
await page.locator('.demo-node[data-path="/book"]').click(); await ready(page, '/book');
|
|
if (failurePath === '/playground') await expect(node(page, 'apple')).toHaveCount(0);
|
|
else await expect(node(page, 'apple')).toHaveText('apple');
|
|
await node(page, '').click(); await ready(page);
|
|
await node(page, 'apple').click(); await ready(page, '/playground/apple');
|
|
await expect(node(page, 'apple')).toHaveText('apple');
|
|
await expect(panel(page)).toHaveAttribute('data-calls', '1');
|
|
await expect(page.locator('[data-protocol-status]')).toBeHidden();
|
|
expect((await execute(page, 'counter', 'get')).result).toEqual({ kind: 'integer', value: '4' });
|
|
await expect(page.locator('.node-value')).toHaveCount(0);
|
|
});
|
|
|
|
test.describe('static explanation', () => {
|
|
test.use({ javaScriptEnabled: false });
|
|
test('Playground is readable without claiming live counter state', async ({ page }, info) => {
|
|
await page.goto(url(info.project.use.baseURL!, 'apple'));
|
|
await expect(page.locator('[data-selected-content] h1')).toHaveText('Playground');
|
|
await expect(page.locator('[data-selected-content]')).toContainText('再読み込みで初期状態');
|
|
await expect(panel(page)).toBeHidden();
|
|
await expect(page.locator('[data-playground-row]')).toHaveCount(0);
|
|
});
|
|
});
|