Files
Hare db4c8ba97f
Reference site / build (push) Canceled after 0s
feat: add highlighted protocol views and inline operation forms
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.
2026-10-26 10:12:22 +09:00

116 lines
8.1 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
import { tokenize } from '@vortexlabs/kirari/core';
import { json } from '@vortexlabs/kirari/languages/json';
test.use({ javaScriptEnabled: true, reducedMotion: 'reduce' });
const result = (page: Page) => page.getByLabel('Operation result', { exact: true });
async function readDocument(page: Page, base: string) {
await page.goto(new URL('?mode=protocol', base).href);
await page.getByRole('button', { name: '? /::document', exact: true }).click();
await page.getByRole('button', { name: 'Run read', exact: true }).click();
await page.getByRole('button', { name: 'Execute read', exact: true }).click();
await expect(result(page)).toBeVisible();
}
for (const scheme of ['light', 'dark'] as const) {
test(`Kirari JSON tokens preserve response text and use readable ${scheme} colors`, async ({ page }, info) => {
await page.emulateMedia({ colorScheme: scheme });
await page.route('**/wip-wasm/wip_browser.js?instance=protocol', async route => {
const response = await route.fetch();
// Preserve the real call and attach a display-only fixture for JSON kinds
// not emitted by this site's current Operations (booleans and null).
await route.fulfill({ response, body: `${await response.text()}\nconst original = BrowserSession.prototype.call_operation; BrowserSession.prototype.call_operation = function(...args) { const value = JSON.parse(original.apply(this,args)); value.preview = { enabled: true, disabled: false, empty: null, fraction: -1.25e-8, nested: [1, { 'escaped"key': '日本語 🐈 \\" <img src=x onerror="window.__jsonExecuted=1"> & \\n\\t' }] }; window.__jsonResponse = value; return JSON.stringify(value); };` });
});
await readDocument(page, info.project.use.baseURL!);
const source = await page.evaluate(() => JSON.stringify((window as unknown as { __jsonResponse: unknown }).__jsonResponse, null, 2));
const view = result(page);
expect(await view.textContent()).toBe(source);
expect(await view.innerText()).toBe(source);
const expected = tokenize(source, json).filter(token => token.type !== 'text').map(token => ({ className: `kirari-${token.type}`, text: token.value }));
expect(await view.locator('span').evaluateAll(elements => elements.map(element => ({ className: element.className, text: element.textContent })))).toEqual(expected);
for (const type of ['property', 'string', 'number', 'boolean', 'null', 'punctuation']) await expect(view.locator(`.kirari-${type}`).first()).toBeVisible();
await expect(view.locator('img, script, a, style, iframe')).toHaveCount(0);
expect(await page.evaluate(() => '__jsonExecuted' in window)).toBe(false);
const selected = await view.evaluate(element => {
const range = document.createRange(); range.selectNodeContents(element);
const selection = window.getSelection()!; selection.removeAllRanges(); selection.addRange(range);
const text = selection.toString(); selection.removeAllRanges(); return text;
});
expect(selected).toBe(source);
const palette = await view.evaluate(element => {
const probe = document.createElement('span'); probe.style.backgroundColor = 'var(--paper)'; element.append(probe);
const background = getComputedStyle(probe).backgroundColor; probe.remove();
return { background, colors: [...element.querySelectorAll('span')].map(span => getComputedStyle(span).color) };
});
const luminance = (color: string) => {
const rgb = color.match(/[\d.]+/g)!.slice(0, 3).map(Number).map(value => value / 255).map(value => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4);
return .2126 * rgb[0] + .7152 * rgb[1] + .0722 * rgb[2];
};
expect(new Set(palette.colors).size).toBe(6);
for (const color of palette.colors) {
const a = luminance(color), b = luminance(palette.background);
expect((Math.max(a, b) + .05) / (Math.min(a, b) + .05)).toBeGreaterThanOrEqual(4.5);
}
const key = view.locator('.kirari-property').first();
const before = await key.evaluate(element => getComputedStyle(element).color);
await page.emulateMedia({ colorScheme: scheme === 'light' ? 'dark' : 'light' });
await expect(key).not.toHaveCSS('color', before);
expect(await view.textContent()).toBe(source);
await page.emulateMedia({ colorScheme: scheme });
await page.screenshot({ path: info.outputPath(`json-${scheme}.png`), fullPage: true });
await expect(page.locator('[data-protocol-panel]')).toHaveAttribute('data-calls', '1');
await page.getByRole('button', { name: 'Run read', exact: true }).click();
await expect(view).toBeHidden(); await expect(view.locator('span')).toHaveCount(0);
});
}
test('real response markup stays literal and each new result replaces the old highlighted JSON', async ({ page }, info) => {
await page.route('**/worldspace.json', async route => {
const response = await route.fetch(); const data = await response.json();
data.nodes[0].html = '<script>window.__jsonExecuted=1</script><img src=x onerror="window.__jsonExecuted=1">日本語 🐈 & " \\';
await route.fulfill({ response, json: data });
});
await readDocument(page, info.project.use.baseURL!);
const view = result(page);
await expect(view.locator('.kirari-string').filter({ hasText: '<script>' })).toHaveCount(1);
await expect(view.locator('script, img')).toHaveCount(0);
expect(await page.evaluate(() => '__jsonExecuted' in window)).toBe(false);
await page.locator('.demo-node[data-path="/playground"]').click();
await page.locator('.demo-node[data-path="/playground/apple"]').click();
await expect(view).toBeHidden(); await expect(view.locator('span')).toHaveCount(0);
await page.getByRole('button', { name: '? /::counter', exact: true }).click();
await page.getByRole('button', { name: 'Run increment', exact: true }).click();
await page.getByRole('button', { name: 'Execute increment', exact: true }).click();
await expect(view.locator('.kirari-string').filter({ hasText: '<script>' })).toHaveCount(0);
const called = JSON.parse((await view.textContent())!);
expect(called.result).toEqual({ kind: 'integer', value: '4' });
expect(called.stats.calls).toBe(2);
await expect(view.locator('.kirari-number')).toHaveCount(3); // calls, interfaces, observes
await expect(page.locator('.demo-node[data-path="/playground/apple"]')).toHaveText('apple');
await expect(page.locator('.node-value')).toHaveCount(0);
await page.getByLabel('increment.amount', { exact: true }).fill('2');
await expect(view).toBeHidden(); await expect(view.locator('span')).toHaveCount(0);
});
test('a highlighting failure keeps the successful result as text without replaying the call', async ({ page }, info) => {
await page.goto(new URL('playground/?mode=protocol&object=%2Fplayground%2Fapple', info.project.use.baseURL!).href);
await page.getByRole('button', { name: '? /::counter', exact: true }).click();
await page.getByRole('button', { name: 'Run increment', exact: true }).click();
await page.evaluate(() => {
const original = document.createDocumentFragment.bind(document);
document.createDocumentFragment = () => { document.createDocumentFragment = original; throw new Error('presentation unavailable'); };
});
await page.getByRole('button', { name: 'Execute increment', exact: true }).click();
const view = result(page);
await expect(view).toBeVisible(); await expect(view.locator('span')).toHaveCount(0);
expect(JSON.parse((await view.textContent())!).result).toEqual({ kind: 'integer', value: '4' });
await expect(page.locator('[data-protocol-panel]')).toHaveAttribute('data-calls', '1');
await expect(page.locator('.demo-node[data-path="/playground/apple"]')).toHaveText('apple');
await expect(page.locator('.node-value')).toHaveCount(0);
await expect(page.locator('[data-protocol-status]')).toBeHidden();
await page.getByRole('button', { name: 'Execute increment', exact: true }).click();
await expect(view.locator('.kirari-property').first()).toBeVisible();
expect(JSON.parse((await view.textContent())!).result).toEqual({ kind: 'integer', value: '5' });
await expect(page.locator('[data-protocol-panel]')).toHaveAttribute('data-calls', '2');
});