feat: add protocol explorer with catalogs and live playground

This commit is contained in:
2026-10-19 18:21:30 +09:00
parent b82c1d0a70
commit 75a36f2298
38 changed files with 1836 additions and 83 deletions
+21 -17
View File
@@ -1,5 +1,5 @@
import { expect, test } from '@playwright/test';
import { documentPaths, treePaths, treeNodeCount, bookURL, chapterSections, treeParents } from './documents';
import { documentPaths, treePaths, treeNodeCount, bookURL, routeURL, chapterSections, treeParents } from './documents';
test.use({ javaScriptEnabled: true });
@@ -11,7 +11,7 @@ const visiblePrefixes = '.node-row:not([hidden]) .branch-prefix';
async function expectNodeGlitch(page: import('@playwright/test').Page) {
expect(await page.locator('.demo-node:not(.root-mark)').evaluateAll((nodes) => nodes.some((node) =>
node.querySelector('.node-label')!.textContent !== ((node as HTMLElement).dataset.path === '/book' ? 'book/' : (node as HTMLElement).dataset.path!.split('/').at(-1))
node.querySelector('.node-label')!.textContent !== ((node as HTMLElement).dataset.path === '/book' ? 'book' : (node as HTMLElement).dataset.path!.split('/').at(-1))
))).toBe(true);
}
@@ -511,7 +511,7 @@ for (const time of [1500, 2600]) {
for (const row of await page.locator('.node-row').all()) await expect(row).toHaveCSS('transform', 'none');
await expect(page.locator('.root-mark')).toHaveCSS('transform', 'none');
await expect(page.locator('.site-header')).toHaveCSS('opacity', '1');
await expect(page.locator('.branch-prefix').first()).toHaveAttribute('data-prefix', '└─ ');
await expect(page.locator('.branch-prefix').first()).toHaveAttribute('data-prefix', '├─ ');
await expect(page.locator('.intro-art')).toBeHidden();
await page.clock.runFor(4000);
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'complete');
@@ -533,7 +533,7 @@ test('skipping an active node glitch restores exact filenames without altering d
await page.keyboard.press('Escape');
await complete(page);
await expect(page.locator('[data-glitching]')).toHaveCount(0);
for (const path of treePaths.slice(1)) await expect(nodeAt(page, path).locator('.node-label')).toHaveText(path === '/book' ? 'book/' : path.split('/').at(-1)!);
for (const path of treePaths.slice(1)) await expect(nodeAt(page, path).locator('.node-label')).toHaveText(path === '/book' ? 'book' : path.split('/').at(-1)!);
await page.clock.runFor(2000);
expect(await page.locator(selectedContent).innerHTML()).toBe(html);
await page.locator('.root-mark').hover();
@@ -578,7 +578,7 @@ test('landing contains only the real filename tree and selected canonical conten
await expect(folder).toHaveAttribute('aria-expanded', 'true');
await expect(folder).toHaveAttribute('aria-level', path === '/' ? '1' : '2');
if (path === '/book') {
await expect(folder.locator('.node-label')).toHaveText('book/');
await expect(folder.locator('.node-label')).toHaveText('book');
const toggle = page.locator('.node-toggle[data-toggle-path="/book"]');
await expect(toggle).toHaveText('−');
expect(await toggle.evaluate((button) => button.tagName)).toBe('BUTTON');
@@ -617,7 +617,8 @@ test('landing contains only the real filename tree and selected canonical conten
await expect(page.locator(selectedContent).getByRole('heading', { level: 1, name: 'WIP', exact: true })).toBeVisible();
await expect(page.locator(selectedContent + ' .root-page[data-root-page]')).toHaveCount(1);
await expect(page.locator(selectedContent)).not.toContainText('1. コンセプト');
expect(await page.locator('.landing h1, .landing h2, .landing h3, .landing h4, .landing h5, .landing h6').evaluateAll((headings) => headings.every((heading) => heading.closest('[data-selected-content]')))).toBe(true);
await expect(page.locator('[data-protocol-panel]')).toBeHidden();
expect(await page.locator('.landing :is(h1, h2, h3, h4, h5, h6):visible').evaluateAll((headings) => headings.every((heading) => heading.closest('[data-selected-content]')))).toBe(true);
await checkGlyphRefs(page);
});
@@ -630,9 +631,10 @@ test('numbered chapters nest visually, fold independently, and retain section UR
await expect(chapter).toHaveAttribute('aria-level', '3');
await expect(child).toHaveAttribute('aria-level', '4');
const prefix = child.locator('..').locator('.branch-prefix');
await expect(prefix).toHaveAttribute('data-prefix', ' │ ├─ ');
await expect(nodeAt(page, '/1.4-concepts-and-scope.md').locator('..').locator('.branch-prefix')).toHaveAttribute('data-prefix', ' │ └─ ');
await expect(prefix).toHaveAttribute('data-prefix', '│ │ ├─ ');
await expect(nodeAt(page, '/1.4-concepts-and-scope.md').locator('..').locator('.branch-prefix')).toHaveAttribute('data-prefix', '│ │ └─ ');
const before = await page.locator(selectedContent).innerHTML();
await nodeAt(page, '/1-concept.md').locator('..').hover();
await page.locator('.node-toggle[data-toggle-path="/1-concept.md"]').click();
for (const path of chapterSections[initialPath]) await expect(nodeAt(page, path)).toBeHidden();
await expect(nodeAt(page, '/2-core-model.md')).toBeVisible();
@@ -717,7 +719,7 @@ test('keyboard arrows only move focus or fold; Enter and Space activate route li
await page.keyboard.press('ArrowLeft');
await expect(book).toHaveAttribute('aria-expanded', 'false');
for (const path of documentPaths) await expect(nodeAt(page, path)).toBeHidden();
await expect(page.getByRole('treeitem')).toHaveCount(2);
await expect(page.getByRole('treeitem')).toHaveCount(5);
await unchanged();
await checkGlyphRefs(page);
await page.keyboard.press('ArrowRight');
@@ -743,7 +745,7 @@ test('keyboard arrows only move focus or fold; Enter and Space activate route li
await expect(page.getByRole('treeitem')).toHaveCount(treeNodeCount);
await checkGlyphRefs(page);
await page.keyboard.press('End');
const finalPath = documentPaths.at(-1)!;
const finalPath = '/playground';
const finalLeaf = nodeAt(page, finalPath);
await expect(finalLeaf).toBeFocused();
await expect(finalLeaf).toHaveAttribute('aria-selected', 'false');
@@ -754,19 +756,19 @@ test('keyboard arrows only move focus or fold; Enter and Space activate route li
if (scroll.needsScroll) expect(scroll.top).toBeGreaterThan(0);
expect(scroll.inside).toBe(true);
await page.keyboard.press('ArrowUp');
const previousPath = '/5-transports.md';
const previousPath = '/packages';
await expect(nodeAt(page, previousPath)).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(finalLeaf).toBeFocused();
await unchanged();
await expect(page.locator('.demo-node[tabindex="0"]')).toHaveCount(1);
await page.keyboard.press('Enter');
await expect(page).toHaveURL(bookURL(finalPath, testInfo.project.use.baseURL!));
await expect(page).toHaveURL(routeURL(finalPath, testInfo.project.use.baseURL!));
await expect(finalLeaf).toHaveAttribute('aria-current', 'page');
await expect(page.locator(selectedContent)).toBeVisible();
await nodeAt(page, previousPath).focus();
await page.keyboard.press('Space');
await expect(page).toHaveURL(bookURL(previousPath, testInfo.project.use.baseURL!));
await expect(page).toHaveURL(routeURL(previousPath, testInfo.project.use.baseURL!));
await expect(nodeAt(page, previousPath)).toHaveAttribute('aria-current', 'page');
await expect(page.locator(selectedContent)).toBeVisible();
});
@@ -863,14 +865,15 @@ test.describe('glyph trunk regressions', () => {
await book.focus();
await page.keyboard.press('ArrowLeft');
await expect(book).toHaveAttribute('aria-expanded', 'false');
// Book is the sole root child: its descendants never extend the first-column spine.
expect(await checkEnd('book')).toBe(expanded);
// Collapsing Book moves the following catalog leaves up and shortens the root spine.
const collapsed = await checkEnd('book');
expect(collapsed).toBeLessThan(expanded);
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(expanded);
expect(await checkEnd('root-reopened')).toBe(collapsed);
await book.focus();
await page.keyboard.press('ArrowRight');
expect(await checkEnd('all-reopened')).toBe(expanded);
@@ -892,7 +895,8 @@ test.describe('JavaScript-free glyph tree', () => {
await expect(home).toContainText('Worldspace Interface Protocol');
const nav = home.getByRole('navigation', { name: 'ルートページのナビゲーション', exact: true });
await expect(nav.getByRole('link', { name: /^Book(?:\s|$)/ })).toHaveAttribute('href', new URL('book/', testInfo.project.use.baseURL!).pathname);
await expect(nav.getByRole('link', { name: /^Source(?:\s|$)/ })).toHaveAttribute('href', (await page.locator('.site-header').getByRole('link', { name: 'Source', exact: true }).getAttribute('href'))!);
await expect(nav.getByRole('link', { name: /^Sources(?:\s|$)/ })).toHaveAttribute('href', new URL('sources/', testInfo.project.use.baseURL!).pathname);
await expect(nav.getByRole('link', { name: /^Packages(?:\s|$)/ })).toHaveAttribute('href', new URL('packages/', testInfo.project.use.baseURL!).pathname);
const initialHTML = await page.locator(selectedContent).innerHTML();
await expect(page.locator('.static-guides, .guide-body, .ascii-panel')).toHaveCount(0);
await expect(page.getByRole('treeitem')).toHaveCount(treeNodeCount);