fix: replace shallow landing fixture with a practical guide
Reference site / build (push) Canceled after 0s
Reference site / build (push) Canceled after 0s
This commit is contained in:
@@ -28,6 +28,12 @@ test('self-hosted Lyth Mono and EB Garamond load with the configured base', asyn
|
||||
expect(fonts.stack).toContain('Lyth Mono');
|
||||
expect(fonts.prefix).toBe(''); // SSR uses the same glyph paths, not HTML wires.
|
||||
expect(external).toEqual([]);
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.locator('[data-landing-stage]')).toHaveAttribute('data-phase', 'complete');
|
||||
await page.getByRole('button', { name: '本文を読む' }).click();
|
||||
await expect(page.locator('[data-result]')).toContainText('Unicode正規化');
|
||||
expect(await page.locator('[data-result]').evaluate((element) => getComputedStyle(element).fontFamily)).toContain('EB Garamond');
|
||||
await page.locator('.call-panel').screenshot({ path: testInfo.outputPath('eb-garamond-guide.png') });
|
||||
await page.goto('./book/1-concept/');
|
||||
expect(await page.locator('.prose').evaluate((element) => getComputedStyle(element).fontFamily)).toContain('EB Garamond');
|
||||
await page.screenshot({ path: testInfo.outputPath('eb-garamond-book.png') });
|
||||
@@ -35,17 +41,21 @@ test('self-hosted Lyth Mono and EB Garamond load with the configured base', asyn
|
||||
|
||||
test.describe('static font and tree rendering', () => {
|
||||
test.use({ javaScriptEnabled: false, deviceScaleFactor: 4 });
|
||||
test('glyph joins paint continuously across SVG cell boundaries', async ({ page }) => {
|
||||
test('glyph joins paint continuously across SVG cell boundaries', async ({ page }, testInfo) => {
|
||||
await page.goto('./');
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
// Sample all four joins without clipping the final branch in the scrollable tree.
|
||||
await page.setViewportSize({ width: page.viewportSize()!.width, height: 2000 });
|
||||
await page.locator('.tree-viewport').evaluate((el) => { (el as HTMLElement).style.height = 'auto'; });
|
||||
const joins = await page.locator('.demo-node[aria-level="2"]').evaluateAll((nodes) => nodes.map((node) => {
|
||||
const svg = node.parentElement!.querySelector<SVGSVGElement>('.branch-vector')!;
|
||||
const p = new DOMPoint(Number(svg.dataset.cellWidth) / 2, 12).matrixTransform(svg.getScreenCTM()!);
|
||||
return { x: p.x, y: p.y };
|
||||
}));
|
||||
for (const join of joins) {
|
||||
expect(joins).toHaveLength(4);
|
||||
for (const [index, join] of joins.entries()) {
|
||||
const clip = { x: Math.floor(join.x), y: Math.floor(join.y) - 1, width: 14, height: 3 };
|
||||
const png = (await page.screenshot({ clip })).toString('base64');
|
||||
const png = (await page.screenshot({ clip, path: testInfo.outputPath(`glyph-join-${index}.png`) })).toString('base64');
|
||||
const columns = await page.evaluate(async (png) => {
|
||||
const image = new Image(); image.src = 'data:image/png;base64,' + png;
|
||||
await image.decode();
|
||||
@@ -73,7 +83,10 @@ test.describe('static font and tree rendering', () => {
|
||||
await expect(page.locator('.trunk-line')).toBeVisible();
|
||||
await expect(page.locator('.trunk-line')).toHaveCSS('stroke', 'none');
|
||||
await expect(page.locator('.branch-prefix').first()).toHaveAttribute('data-prefix', '├── ');
|
||||
await expect(page.locator('.branch-prefix').first().locator('path')).toHaveCount(2);
|
||||
await expect(page.locator('.branch-prefix').first().locator('path')).toHaveCount(0);
|
||||
await expect(page.locator('.branch-prefix').first().locator('use')).toHaveCount(4);
|
||||
await expect(page.locator('.branch-prefix').first().locator('.branch-spine use')).toHaveAttribute('href', '#tree-glyph-9500');
|
||||
await expect(page.locator('.tree-glyphs defs path')).toHaveCount(5);
|
||||
await expect(page.getByRole('link', { name: 'Book', exact: true })).toBeVisible();
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
await page.screenshot({ path: testInfo.outputPath('lyth-mono-static.png') });
|
||||
@@ -86,6 +99,6 @@ test('failed font downloads do not strand the animation or interactive demo', as
|
||||
await expect(page.locator('[data-landing-stage]')).toHaveAttribute('data-phase', 'armed');
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(page.locator('[data-landing-stage]')).toHaveAttribute('data-phase', 'complete');
|
||||
await page.getByRole('button', { name: 'Call', exact: false }).click();
|
||||
await expect(page.locator('[data-result]')).toContainText('Give every capability a place.');
|
||||
await page.getByRole('button', { name: '本文を読む' }).click();
|
||||
await expect(page.locator('[data-result]')).toContainText('Unicode正規化');
|
||||
});
|
||||
|
||||
@@ -3,6 +3,64 @@ import { expect, test } from '@playwright/test';
|
||||
test.use({ javaScriptEnabled: true });
|
||||
|
||||
const stage = '[data-landing-stage]';
|
||||
const initialPath = '/design/object-tree/paths/names';
|
||||
const nodeAt = (page: import('@playwright/test').Page, path: string) => page.locator(`.demo-node[data-path="${path}"]`);
|
||||
const visiblePrefixes = '.node-row:not([hidden]) .branch-prefix';
|
||||
|
||||
// Ink sampling only: expose the last branch without scroll-container clipping.
|
||||
// Interaction tests retain the real scrollable viewport.
|
||||
async function exposeTree(page: import('@playwright/test').Page) {
|
||||
await page.setViewportSize({ width: page.viewportSize()!.width, height: 2000 });
|
||||
await page.locator('.tree-viewport').evaluate((el) => {
|
||||
(el as HTMLElement).style.height = 'auto';
|
||||
el.scrollTop = el.scrollLeft = 0;
|
||||
});
|
||||
await page.evaluate(() => scrollTo(0, 0));
|
||||
}
|
||||
|
||||
async function checkGlyphRefs(page: import('@playwright/test').Page) {
|
||||
const rendered = await page.locator(visiblePrefixes).evaluateAll((prefixes) => {
|
||||
const paths = [...document.querySelectorAll<HTMLElement>('.node-row:not([hidden]) .demo-node')].map((node) => node.dataset.path!);
|
||||
const parent = (path: string) => path.slice(0, path.lastIndexOf('/')) || '/';
|
||||
const last = (path: string) => paths.filter((other) => other !== '/' && parent(other) === parent(path)).at(-1) === path;
|
||||
return prefixes.map((prefix) => {
|
||||
const path = prefix.parentElement!.querySelector<HTMLElement>('.demo-node')!.dataset.path!;
|
||||
let expected = last(path) ? '└── ' : '├── ';
|
||||
let ancestor = parent(path);
|
||||
while (ancestor !== '/') {
|
||||
expected = (last(ancestor) ? ' ' : '│ ') + expected;
|
||||
ancestor = parent(ancestor);
|
||||
}
|
||||
const refs = [...prefix.querySelectorAll<SVGUseElement>('use[data-cell]')].map((use) => ({
|
||||
cell: Number(use.dataset.cell), href: use.getAttribute('href'),
|
||||
resolves: Boolean(document.querySelector(use.getAttribute('href')!)),
|
||||
}));
|
||||
return { actual: prefix.getAttribute('data-prefix'), expected, refs };
|
||||
});
|
||||
});
|
||||
expect(rendered.length).toBeGreaterThan(0);
|
||||
for (const { actual, expected, refs } of rendered) {
|
||||
expect(actual).toBe(expected);
|
||||
expect(refs.map(({ cell }) => cell)).toEqual([...expected].map((_, index) => index));
|
||||
for (const { cell, href, resolves } of refs) {
|
||||
const char = cell === 0 && !['├', '└'].includes(expected[0]) ? ' ' : expected[cell];
|
||||
expect(href).toBe('#tree-glyph-' + char.codePointAt(0));
|
||||
expect(resolves).toBe(true);
|
||||
}
|
||||
}
|
||||
await expect(page.locator('.branch-prefix path')).toHaveCount(0);
|
||||
await expect(page.locator('.tree-glyphs defs path')).toHaveCount(5);
|
||||
const endpoint = await page.evaluate(() => {
|
||||
const anchor = document.querySelector<SVGGElement>('.trunk-anchor')!;
|
||||
const last = [...document.querySelectorAll<HTMLElement>('.node-row:not([hidden]) .demo-node[aria-level="2"]')].at(-1)!;
|
||||
const svg = last.parentElement!.querySelector<SVGSVGElement>('.branch-vector')!;
|
||||
const join = new DOMPoint(Number(svg.dataset.cellWidth) / 2, Number(anchor.dataset.branchY) + Number(anchor.dataset.branchThickness) / 2).matrixTransform(svg.getScreenCTM()!);
|
||||
return { x: join.x, y: join.y };
|
||||
});
|
||||
const trunk = await trunkGeometry(page);
|
||||
expect(Math.abs(trunk.x - endpoint.x)).toBeLessThan(.1);
|
||||
expect(Math.abs(trunk.bottom - endpoint.y)).toBeLessThan(.1);
|
||||
}
|
||||
// Filled glyph bounds describe the ink; the anchor describes its origin.
|
||||
// Path length/start points describe an outline perimeter, not the trunk axis.
|
||||
async function trunkGeometry(page: import('@playwright/test').Page) {
|
||||
@@ -94,11 +152,13 @@ for (const trigger of ['click', 'wheel', 'key']) {
|
||||
if (trigger === 'key') await page.keyboard.press('a');
|
||||
await expect(page.locator('html')).toHaveAttribute('data-landing', 'playing');
|
||||
await complete(page);
|
||||
await page.getByRole('treeitem', { name: 'README.md', exact: true }).click();
|
||||
await expect(page.locator('[data-object-signature]')).toContainText('object at "/repository/README.md"');
|
||||
await expect(page.locator('[data-result]')).not.toContainText('# Web Interface Protocol');
|
||||
await page.getByRole('button', { name: 'Call', exact: false }).click();
|
||||
await expect(page.locator('[data-result]')).toContainText('# Web Interface Protocol');
|
||||
await nodeAt(page, initialPath).click();
|
||||
await expect(page.locator('[data-object-signature]')).toBeHidden();
|
||||
await page.getByText('Object signature', { exact: true }).click();
|
||||
await expect(page.locator('[data-object-signature]')).toContainText('object at "/design/object-tree/paths/names"');
|
||||
await expect(page.locator('[data-result]')).not.toContainText('Unicode正規化');
|
||||
await page.getByRole('button', { name: '本文を読む' }).click();
|
||||
await expect(page.locator('[data-result]')).toContainText('Unicode正規化');
|
||||
await expect(page.getByRole('status')).toContainText('read_body が完了');
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
@@ -397,7 +457,7 @@ test('continuous motion overlaps growth, travel and reveal without intermediate
|
||||
await expect(root).toHaveCount(1);
|
||||
await expect(page.locator('.call-panel')).not.toHaveAttribute('inert');
|
||||
await expect(page.locator('[data-glitching]')).toHaveCount(0);
|
||||
await expect(page.locator('[data-object-signature]')).toContainText('object at "/issues/123"');
|
||||
await expect(page.locator('[data-object-signature]')).toContainText('object at "/design/object-tree/paths/names"');
|
||||
for (const panel of await page.locator('.ascii-panel').all()) {
|
||||
await expect(panel).toHaveCSS('outline-style', 'none');
|
||||
await expect(panel).toHaveCSS('border-top-width', '0px');
|
||||
@@ -406,22 +466,22 @@ test('continuous motion overlaps growth, travel and reveal without intermediate
|
||||
for (const side of await panel.locator('.box-column').all()) await expect(side).toHaveText(/^[│\s]+$/);
|
||||
}
|
||||
await expect(page.locator('.branch-lines')).toHaveCount(0);
|
||||
expect(await page.locator('.branch-prefix').evaluateAll((prefixes) => prefixes.map((prefix) => prefix.getAttribute('data-prefix')))).toEqual([
|
||||
'├── ', '│ ├── ', '│ └── ', '│ └── ',
|
||||
'├── ', '│ └── ', '└── ', ' └── ',
|
||||
]);
|
||||
await expect(page.locator('.demo-node')).toHaveCount(42);
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(24);
|
||||
await expect(page.locator('[data-tree-count]')).toHaveText('24 / 42 objects');
|
||||
await expect(page.locator('.demo-badge')).toHaveText('4 AREAS / 26 GUIDES / 42 OBJECTS');
|
||||
await checkGlyphRefs(page);
|
||||
await expect(page.locator('.root-wire')).toHaveCount(0);
|
||||
await expect(page.locator('.branch-prefix svg')).toHaveCount(8);
|
||||
await expect(page.locator('.branch-spine path')).toHaveCount(3);
|
||||
await expect(page.locator('.branch-arm path')).toHaveCount(8);
|
||||
await expect(page.locator(visiblePrefixes + ' svg')).toHaveCount(23);
|
||||
await expect(page.locator(visiblePrefixes + ' .branch-spine use')).toHaveCount(23);
|
||||
await expect(page.locator('.tree-scene')).toHaveCSS('transform', 'none');
|
||||
const grid = await page.evaluate(() => {
|
||||
const root = document.querySelector('.root-mark')!.getBoundingClientRect();
|
||||
const prefix = document.querySelector<HTMLElement>('.branch-prefix')!;
|
||||
const cell = prefix.getBoundingClientRect().width / 4;
|
||||
const frame = document.querySelector('.landing-content')!.getBoundingClientRect();
|
||||
const rows = [...document.querySelectorAll<HTMLElement>('.node-row:not(.root-slot)')].map((row) => row.getBoundingClientRect());
|
||||
const nodes = [...document.querySelectorAll<HTMLElement>('.demo-node:not(.root-mark)')].map((node) => node.getBoundingClientRect());
|
||||
const rows = [...document.querySelectorAll<HTMLElement>('.node-row:not(.root-slot):not([hidden])')].map((row) => row.getBoundingClientRect());
|
||||
const nodes = [...document.querySelectorAll<HTMLElement>('.node-row:not([hidden]) .demo-node:not(.root-mark)')].map((node) => node.getBoundingClientRect());
|
||||
const foot = new DOMPoint(12, 60).matrixTransform(document.querySelector<SVGPathElement>('.root-shape')!.getScreenCTM()!);
|
||||
return {
|
||||
rootColumnError: Math.abs(foot.x - prefix.getBoundingClientRect().left - cell / 2),
|
||||
@@ -429,7 +489,7 @@ test('continuous motion overlaps growth, travel and reveal without intermediate
|
||||
frameCells: frame.width / cell,
|
||||
rowHeights: rows.map((row) => row.height),
|
||||
nodeColumns: nodes.map((node) => (node.left - frame.left) / cell),
|
||||
panelRows: [...document.querySelectorAll('.ascii-panel')].map((panel) => (panel.getBoundingClientRect().top - frame.top) / 24),
|
||||
panels: [...document.querySelectorAll('.ascii-panel')].map((panel) => panel.getBoundingClientRect().toJSON()),
|
||||
font: getComputedStyle(prefix).fontFamily,
|
||||
};
|
||||
});
|
||||
@@ -438,7 +498,9 @@ test('continuous motion overlaps growth, travel and reveal without intermediate
|
||||
expect(Math.abs(grid.frameCells / 4 - Math.round(grid.frameCells / 4))).toBeLessThan(.02);
|
||||
expect(grid.rowHeights.every((height) => height === 24)).toBe(true);
|
||||
expect(grid.nodeColumns.every((col) => Math.abs(col - Math.round(col)) < .02)).toBe(true);
|
||||
expect(grid.panelRows.every((row) => Math.abs(row - Math.round(row)) < .02)).toBe(true);
|
||||
// Prose determines panel height; the panels still must not overlap.
|
||||
expect(grid.panels[1].top - grid.panels[0].bottom).toBeCloseTo(24, 4);
|
||||
expect(grid.panels.every((panel) => panel.height > 0 && panel.width > 0)).toBe(true);
|
||||
expect(grid.font).toContain('Mono');
|
||||
await page.screenshot({ path: testInfo.outputPath('06-world.png'), fullPage: true, animations: 'disabled' });
|
||||
await page.keyboard.press('z');
|
||||
@@ -478,45 +540,194 @@ test('skipping an active glitch restores exact text and frames', async ({ page }
|
||||
await page.keyboard.press('Escape');
|
||||
await complete(page);
|
||||
await expect(page.locator('[data-glitching]')).toHaveCount(0);
|
||||
await expect(page.locator('#call-result-title')).toHaveText('03 / Call result');
|
||||
await expect(page.locator('#call-result-title')).toHaveText('03 / Read');
|
||||
await expect(page.locator('.call-panel .box-corner')).toHaveText(['┌', '┐', '└', '┘']);
|
||||
await expect(page.locator('.call-panel .box-fill').first()).toHaveText(/^─+$/);
|
||||
await page.clock.runFor(2000);
|
||||
await expect(page.locator('[data-object-signature]')).toContainText('object at "/issues/123"');
|
||||
await expect(page.locator('[data-object-signature]')).toContainText('object at "/design/object-tree/paths/names"');
|
||||
await page.locator('.root-mark').hover();
|
||||
await expect(page.locator(stage)).not.toHaveAttribute('data-hover');
|
||||
});
|
||||
|
||||
test('tree selection, collapse and call results are keyboard accessible', async ({ page }) => {
|
||||
test('tree selection, collapse and reading are keyboard accessible in the scrollable viewport', async ({ page }) => {
|
||||
await armed(page);
|
||||
await page.keyboard.press('Escape');
|
||||
await complete(page);
|
||||
const selected = page.getByRole('treeitem', { name: '123', exact: true });
|
||||
await expect(selected).toBeFocused();
|
||||
await expect(nodeAt(page, initialPath)).toBeFocused();
|
||||
await page.keyboard.press('ArrowUp');
|
||||
const issues = page.getByRole('treeitem', { name: 'issues', exact: true });
|
||||
await expect(issues).toBeFocused();
|
||||
const paths = nodeAt(page, '/design/object-tree/paths');
|
||||
await expect(paths).toBeFocused();
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(issues).toHaveAttribute('aria-expanded', 'false');
|
||||
await expect(selected).toBeHidden();
|
||||
await expect(paths).toHaveAttribute('aria-expanded', 'false');
|
||||
await expect(nodeAt(page, initialPath)).toBeHidden();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(selected).toBeVisible();
|
||||
await expect(nodeAt(page, initialPath)).toBeVisible();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(selected).toBeFocused();
|
||||
await expect(nodeAt(page, initialPath)).toBeFocused();
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(paths).toBeFocused();
|
||||
await page.keyboard.press('Home');
|
||||
await expect(page.getByRole('treeitem', { name: 'ルート /' })).toBeFocused();
|
||||
const root = nodeAt(page, '/');
|
||||
await expect(root).toBeFocused();
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(1);
|
||||
await expect(page.locator('.branch-spine').first()).toBeHidden();
|
||||
expect((await trunkGeometry(page)).length).toBe(0);
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(page.locator('.branch-spine').first()).toBeVisible();
|
||||
await expect(page.locator('.branch-prefix').first()).toHaveAttribute('data-prefix', '├── ');
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(24);
|
||||
await checkGlyphRefs(page);
|
||||
await page.getByRole('button', { name: 'すべて展開', exact: true }).click();
|
||||
await root.focus();
|
||||
await page.keyboard.press('End');
|
||||
await expect(page.getByRole('treeitem', { name: 'protocol', exact: true })).toBeFocused();
|
||||
const finalLeaf = nodeAt(page, '/review/boundary-checks/failure-tests');
|
||||
await expect(finalLeaf).toBeFocused();
|
||||
await expect(finalLeaf).toHaveAttribute('aria-selected', 'true');
|
||||
const scroll = await page.locator('.tree-viewport').evaluate((viewport) => {
|
||||
const node = document.activeElement!.getBoundingClientRect(), box = viewport.getBoundingClientRect();
|
||||
return { top: viewport.scrollTop, inside: node.top >= box.top && node.bottom <= box.bottom && node.left >= box.left && node.right <= box.right };
|
||||
});
|
||||
expect(scroll.top).toBeGreaterThan(0);
|
||||
expect(scroll.inside).toBe(true);
|
||||
await page.keyboard.press('ArrowUp');
|
||||
await expect(nodeAt(page, '/review/boundary-checks/validation')).toBeFocused();
|
||||
await page.keyboard.press('ArrowDown');
|
||||
await expect(finalLeaf).toBeFocused();
|
||||
await expect(page.locator('.demo-node[tabindex="0"]')).toHaveCount(1);
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.getByRole('button', { name: 'Call', exact: false })).toBeFocused();
|
||||
await expect(page.locator('.text-panel summary')).toBeFocused();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('[data-result]')).toContainText('fetch_interface');
|
||||
await expect(page.locator('[data-object-signature]')).toBeVisible();
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.locator('.call-panel summary')).toBeFocused();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('[data-interface-signature]')).toBeVisible();
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.getByRole('button', { name: '本文を読む' })).toBeFocused();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('[data-call-state]')).toHaveText('RETURNED');
|
||||
await expect(page.locator('[data-result]')).toContainText('結果不明やTransport不達でcallを自動再実行しない');
|
||||
});
|
||||
|
||||
test('expand/collapse all preserves visible selection and rehomes hidden selection, including a closed root', async ({ page }, testInfo) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto('./');
|
||||
await complete(page);
|
||||
const expand = page.getByRole('button', { name: 'すべて展開', exact: true });
|
||||
const collapse = page.getByRole('button', { name: 'たたむ', exact: true });
|
||||
await expect(nodeAt(page, initialPath)).toHaveAttribute('aria-selected', 'true');
|
||||
await expand.click();
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(42);
|
||||
await expect(page.locator('[data-tree-count]')).toHaveText('42 / 42 objects');
|
||||
await expect(page.locator('.demo-node:not([aria-expanded])')).toHaveCount(26);
|
||||
await expect(nodeAt(page, initialPath)).toHaveAttribute('aria-selected', 'true');
|
||||
await checkGlyphRefs(page);
|
||||
await collapse.click();
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(5);
|
||||
await expect(page.locator('[data-tree-count]')).toHaveText('5 / 42 objects');
|
||||
await expect(nodeAt(page, '/')).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.locator('[data-target]')).toHaveText('/');
|
||||
await expect(page.getByRole('button', { name: '本文を読む' })).toBeDisabled();
|
||||
await checkGlyphRefs(page);
|
||||
await nodeAt(page, '/build').click();
|
||||
await expand.click();
|
||||
await expect(nodeAt(page, '/build')).toHaveAttribute('aria-selected', 'true');
|
||||
await collapse.click();
|
||||
await expect(nodeAt(page, '/build')).toHaveAttribute('aria-selected', 'true');
|
||||
await nodeAt(page, '/').focus();
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(1);
|
||||
await collapse.click(); // Regression: collapse-all must reopen a keyboard-collapsed root.
|
||||
await expect(nodeAt(page, '/')).toHaveAttribute('aria-expanded', 'true');
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(5);
|
||||
await checkGlyphRefs(page);
|
||||
await page.locator('.tree-pane').screenshot({ path: testInfo.outputPath('guide-collapse-all-root-reopened.png') });
|
||||
});
|
||||
|
||||
test('body search exposes ancestors, preserves or rehomes selection, and handles no results', async ({ page }, testInfo) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto('./');
|
||||
await complete(page);
|
||||
const search = page.getByRole('searchbox', { name: 'コンテンツを検索' });
|
||||
const expand = page.getByRole('button', { name: 'すべて展開', exact: true });
|
||||
const collapse = page.getByRole('button', { name: 'たたむ', exact: true });
|
||||
await search.fill('Unicode正規化'); // Body only, not a name/title/description.
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(5);
|
||||
await expect(page.locator('[data-tree-count]')).toHaveText('5 / 42 objects');
|
||||
await expect(nodeAt(page, initialPath)).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(expand).toBeDisabled();
|
||||
await expect(collapse).toBeDisabled();
|
||||
await checkGlyphRefs(page);
|
||||
await expect(nodeAt(page, '/design').locator('..').locator('.branch-spine use')).toHaveAttribute('href', '#tree-glyph-9492');
|
||||
const shortTrunk = (await trunkGeometry(page)).length;
|
||||
await page.locator('.tree-pane').screenshot({ path: testInfo.outputPath('guide-search-body-ancestors.png') });
|
||||
await search.fill('疑似ID'); // Initially visible identity, also body-only.
|
||||
await expect(nodeAt(page, '/design/object-tree/paths/identity')).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.locator('[data-target]')).toHaveText('/design/object-tree/paths/identity');
|
||||
await checkGlyphRefs(page);
|
||||
await search.fill('観測cache'); // A leaf behind an initially collapsed parent.
|
||||
await expect(nodeAt(page, '/build/client/cache')).toBeVisible();
|
||||
await expect(nodeAt(page, '/build/client/cache')).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(nodeAt(page, '/build/client')).toHaveAttribute('aria-expanded', 'true');
|
||||
await checkGlyphRefs(page);
|
||||
await search.press('Enter');
|
||||
await expect(nodeAt(page, '/build/client/cache')).toBeFocused();
|
||||
await page.keyboard.press('Home');
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(nodeAt(page, '/build')).toBeFocused();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(nodeAt(page, '/build/client')).toBeFocused();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
await expect(nodeAt(page, '/build/client/cache')).toBeFocused();
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(nodeAt(page, '/build/client')).toBeFocused();
|
||||
await expect(nodeAt(page, '/build/client')).toHaveAttribute('aria-expanded', 'true');
|
||||
await search.fill('no-guide-can-match-this-98371');
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(1);
|
||||
await expect(page.locator('[data-tree-count]')).toHaveText('一致する項目がありません');
|
||||
await expect(nodeAt(page, '/')).toHaveAttribute('aria-selected', 'true');
|
||||
await expect(page.locator('[data-target]')).toHaveText('/');
|
||||
expect((await trunkGeometry(page)).length).toBe(0);
|
||||
await expect(page.getByRole('button', { name: '本文を読む' })).toBeDisabled();
|
||||
await page.locator('.tree-pane').screenshot({ path: testInfo.outputPath('guide-search-no-results.png') });
|
||||
await search.fill('');
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(24);
|
||||
await expect(expand).toBeEnabled();
|
||||
await expect(collapse).toBeEnabled();
|
||||
await expect(nodeAt(page, '/build/client')).toHaveAttribute('aria-expanded', 'false');
|
||||
await expect(nodeAt(page, '/')).toHaveAttribute('aria-selected', 'true');
|
||||
expect((await trunkGeometry(page)).length).toBeGreaterThan(shortTrunk);
|
||||
await checkGlyphRefs(page);
|
||||
});
|
||||
|
||||
test('reading uses explicit operations, disclosure signatures, and navigable SITE_BASE-aware sources', async ({ page }, testInfo) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto('./');
|
||||
await complete(page);
|
||||
await expect(page.locator('[data-target]')).toHaveText(initialPath);
|
||||
await expect(page.locator('[data-object-signature]')).toBeHidden();
|
||||
await expect(page.locator('[data-interface-signature]')).toBeHidden();
|
||||
await expect(page.locator('[data-sources]')).toBeHidden();
|
||||
await expect(page.locator('[data-result]')).not.toContainText('Unicode正規化');
|
||||
await page.getByText('Object signature', { exact: true }).click();
|
||||
await expect(page.locator('[data-object-signature]')).toBeVisible();
|
||||
await expect(page.locator('[data-object-signature]')).toContainText('object at "' + initialPath + '"');
|
||||
await page.getByText('Interface signature', { exact: true }).click();
|
||||
await expect(page.locator('[data-interface-signature]')).toBeVisible();
|
||||
await expect(page.locator('[data-interface-signature]')).toContainText('operation read_body() -> { body: string }');
|
||||
await page.getByRole('button', { name: '本文を読む' }).click();
|
||||
await expect(page.locator('[data-result]')).toContainText('Unicode正規化');
|
||||
await expect(page.locator('[data-call-state]')).toHaveText('RETURNED');
|
||||
await expect(page.getByRole('status')).toContainText('read_body が完了');
|
||||
await expect(page.locator('[data-sources]')).toBeVisible();
|
||||
const links = page.locator('[data-sources] a');
|
||||
await expect(links).toHaveCount(2);
|
||||
const base = testInfo.project.use.baseURL!;
|
||||
const expected = ['book/2.1-object-tree-schema/', 'book/3-protocol/'].map((path) => new URL(path, base).href);
|
||||
expect(await links.evaluateAll((anchors) => anchors.map((a) => (a as HTMLAnchorElement).href))).toEqual(expected);
|
||||
await page.locator('.call-panel').screenshot({ path: testInfo.outputPath('guide-read-signatures-sources.png') });
|
||||
await links.first().click();
|
||||
await expect(page).toHaveURL(expected[0]);
|
||||
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
|
||||
await expect(page.locator('.prose')).toContainText('Object');
|
||||
});
|
||||
|
||||
test.describe('glyph trunk regressions', () => {
|
||||
@@ -556,7 +767,12 @@ test.describe('glyph trunk regressions', () => {
|
||||
await armed(page);
|
||||
await page.keyboard.press('Escape');
|
||||
await complete(page);
|
||||
await exposeTree(page);
|
||||
const checkEnd = async (name: string) => {
|
||||
// Keyboard selection can scroll wide prose summaries horizontally. Reset
|
||||
// the sampling column before measuring, never after computing screen points.
|
||||
await page.locator('.tree-viewport').evaluate((el) => { el.scrollTop = el.scrollLeft = 0; });
|
||||
await page.evaluate(() => scrollTo(0, 0));
|
||||
const trunk = await trunkGeometry(page);
|
||||
const join = await page.evaluate(() => {
|
||||
const anchor = document.querySelector<SVGGElement>('.trunk-anchor')!;
|
||||
@@ -602,7 +818,7 @@ test.describe('glyph trunk regressions', () => {
|
||||
return trunk.length;
|
||||
};
|
||||
let previous = await checkEnd('expanded');
|
||||
for (const name of ['repository', 'issues']) {
|
||||
for (const name of ['design', 'build', 'operate']) {
|
||||
const node = page.getByRole('treeitem', { name, exact: true });
|
||||
await node.focus();
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
@@ -611,17 +827,17 @@ test.describe('glyph trunk regressions', () => {
|
||||
expect(length).toBeLessThan(previous);
|
||||
previous = length;
|
||||
}
|
||||
const knowledge = page.getByRole('treeitem', { name: 'knowledge', exact: true });
|
||||
await knowledge.focus();
|
||||
const review = page.getByRole('treeitem', { name: 'review', exact: true });
|
||||
await review.focus();
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
expect(await checkEnd('knowledge')).toBe(previous); // Last child never extends the first-column spine.
|
||||
expect(await checkEnd('review')).toBe(previous); // Last child never extends the first-column spine.
|
||||
await page.keyboard.press('Home');
|
||||
await page.keyboard.press('ArrowLeft');
|
||||
await expect(page.getByRole('treeitem')).toHaveCount(1);
|
||||
await checkEnd('root');
|
||||
await page.keyboard.press('ArrowRight');
|
||||
expect(await checkEnd('root-reopened')).toBe(previous);
|
||||
for (const name of ['repository', 'issues', 'knowledge']) {
|
||||
for (const name of ['design', 'build', 'operate', 'review']) {
|
||||
await page.getByRole('treeitem', { name, exact: true }).focus();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
}
|
||||
@@ -632,6 +848,28 @@ test.describe('glyph trunk regressions', () => {
|
||||
test.describe('JavaScript-free glyph tree', () => {
|
||||
test.use({ javaScriptEnabled: false });
|
||||
|
||||
test('all 26 authored bodies are accessible without JavaScript', async ({ page }, testInfo) => {
|
||||
await page.goto('./');
|
||||
const articles = page.locator('.static-guides details');
|
||||
await expect(articles).toHaveCount(26);
|
||||
await expect(page.locator('.static-guides .guide-body')).toHaveCount(26);
|
||||
const bodies = await page.locator('.static-guides .guide-body').allTextContents();
|
||||
expect(new Set(bodies).size).toBe(26);
|
||||
for (const article of await articles.all()) {
|
||||
await article.locator('summary').click();
|
||||
await expect(article).toHaveAttribute('open', '');
|
||||
await expect(article.locator('.guide-body')).toBeVisible();
|
||||
expect((await article.locator('.guide-body').textContent())!.length).toBeGreaterThan(200);
|
||||
const sources = await article.locator('a').evaluateAll((links) => links.map((a) => (a as HTMLAnchorElement).href));
|
||||
expect(sources.length).toBeGreaterThan(0);
|
||||
for (const source of sources) expect(source.startsWith(new URL('book/', testInfo.project.use.baseURL!).href)).toBe(true);
|
||||
await article.locator('summary').click();
|
||||
await expect(article.locator('.guide-body')).toBeHidden();
|
||||
}
|
||||
await articles.last().locator('summary').click();
|
||||
await articles.last().screenshot({ path: testInfo.outputPath('no-js-final-article-body.png') });
|
||||
});
|
||||
|
||||
test('SSR paints one filled trunk and SVG branch outlines without animation code', async ({ page }, testInfo) => {
|
||||
await page.goto('./');
|
||||
await expect(page.locator('html')).not.toHaveAttribute('data-landing');
|
||||
@@ -640,7 +878,7 @@ test.describe('JavaScript-free glyph tree', () => {
|
||||
await expect(page.locator('.trunk-line')).toHaveCount(1);
|
||||
await expect(page.locator('.trunk-line')).toHaveCSS('opacity', '1');
|
||||
await expect(page.locator('.trunk-line')).toHaveCSS('stroke', 'none');
|
||||
await expect(page.locator('.branch-prefix svg')).toHaveCount(8);
|
||||
await expect(page.locator(visiblePrefixes + ' svg')).toHaveCount(23);
|
||||
expect(await page.locator('.branch-prefix').evaluateAll((prefixes) => prefixes.every((prefix) => !prefix.textContent!.trim()))).toBe(true);
|
||||
const trunk = await trunkGeometry(page);
|
||||
expect(trunk.width).toBeCloseTo(1.33, 5);
|
||||
@@ -653,13 +891,35 @@ test.describe('JavaScript-free glyph tree', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('selecting another guide cancels a read reveal and resets its state', async ({ page }) => {
|
||||
await page.clock.install({ time: new Date('2026-01-01T00:00:00Z') });
|
||||
await page.clock.pauseAt(new Date('2026-01-01T00:00:01Z'));
|
||||
await armed(page);
|
||||
await page.keyboard.press('Escape');
|
||||
await complete(page);
|
||||
await expect(page.locator('[data-call-state]')).toHaveText('READY');
|
||||
await page.getByRole('button', { name: '本文を読む' }).click();
|
||||
await page.clock.runFor(32);
|
||||
await expect(page.locator('[data-call-state]')).toHaveText('RETURNED');
|
||||
await nodeAt(page, '/design/object-tree/paths/identity').click();
|
||||
await expect(page.locator('[data-call-state]')).toHaveText('READY');
|
||||
await expect(page.locator('[data-result]')).toHaveCSS('opacity', '1');
|
||||
await expect(page.locator('[data-result]')).toHaveCSS('transform', 'none');
|
||||
await expect(page.locator('[data-sources]')).toBeHidden();
|
||||
await expect(page.locator('[data-result]')).not.toContainText('Unicode正規化');
|
||||
await page.clock.runFor(500);
|
||||
await expect(page.locator('[data-call-state]')).toHaveText('READY');
|
||||
await page.getByRole('button', { name: '本文を読む' }).click();
|
||||
await expect(page.locator('[data-result]')).toContainText('疑似ID');
|
||||
});
|
||||
|
||||
test('reduced motion opens the working demo immediately', async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto('./');
|
||||
await complete(page);
|
||||
await expect(page.locator('.intro-skip')).toBeHidden();
|
||||
await page.getByRole('button', { name: 'Call', exact: false }).click();
|
||||
await expect(page.locator('[data-result]')).toContainText('Give every capability a place.');
|
||||
await page.getByRole('button', { name: '本文を読む' }).click();
|
||||
await expect(page.locator('[data-result]')).toContainText('Unicode正規化');
|
||||
});
|
||||
|
||||
test('changing motion preference and resizing cannot strand the intro', async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user