fix: rebuild landing with SVG motion and a monospace tree
Reference site / build (push) Canceled after 0s

This commit is contained in:
2026-10-10 02:45:33 +09:00
parent 01c9498e6d
commit 5c6ccc3677
9 changed files with 803 additions and 14 deletions
+169
View File
@@ -0,0 +1,169 @@
import { expect, test } from '@playwright/test';
test.use({ javaScriptEnabled: true });
const stage = '[data-landing-stage]';
async function armed(page: import('@playwright/test').Page) {
await page.goto('./');
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'armed');
}
async function complete(page: import('@playwright/test').Page) {
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'complete', { timeout: 10000 });
await expect(page.locator('html')).not.toHaveAttribute('data-landing');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
await expect(page.getByRole('link', { name: 'Book', exact: true })).toBeVisible();
}
for (const trigger of ['click', 'wheel', 'key']) {
test(`one ${trigger} plays through to the interactive world without more input`, async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', (error) => errors.push(error.message));
await armed(page);
if (trigger === 'click') await page.getByRole('treeitem', { name: 'Worldspaceを開く' }).click();
if (trigger === 'wheel') await page.mouse.wheel(0, 240);
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 expect(page.getByRole('status')).toContainText('read_body が完了');
expect(errors).toEqual([]);
});
}
test('storyboard phases retain one root and ignore repeated triggers', async ({ page }, testInfo) => {
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);
const root = page.locator('.root-mark');
await expect(root.locator('svg .root-shape')).toHaveCount(1);
await expect(root).toHaveText(''); // Geometry, never a font glyph.
await expect(page.locator('.call-panel')).toHaveAttribute('inert', '');
const initial = await root.boundingBox();
expect(initial).not.toBeNull();
expect(Math.abs(initial!.x + initial!.width / 2 - page.viewportSize()!.width / 2)).toBeLessThan(2);
expect(Math.abs(initial!.y + initial!.height / 2 - page.viewportSize()!.height / 2)).toBeLessThan(2);
await page.screenshot({ path: testInfo.outputPath('01-root.png') });
await root.click();
await page.clock.runFor(320);
await page.screenshot({ path: testInfo.outputPath('02-tap.png') });
await page.clock.runFor(1450);
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'orbit');
const arcLength = await page.locator('#welcome-orbit').evaluate((path) => (path as SVGPathElement).getTotalLength());
await page.screenshot({ path: testInfo.outputPath('03-orbit.png') });
await page.mouse.wheel(0, 180);
await page.keyboard.press('ArrowDown');
await page.mouse.click(20, 20);
await page.clock.runFor(1430);
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'unfold');
const unfoldingLength = await page.locator('#welcome-orbit').evaluate((path) => (path as SVGPathElement).getTotalLength());
expect(Math.abs(unfoldingLength - arcLength)).toBeLessThan(2);
await page.screenshot({ path: testInfo.outputPath('04-unfold.png') });
await page.clock.runFor(1550);
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'tree');
await page.screenshot({ path: testInfo.outputPath('05-tree.png') });
await page.clock.runFor(1800);
await complete(page);
await expect(root).toHaveCount(1);
await expect(page.locator('.call-panel')).not.toHaveAttribute('inert');
await expect(page.locator('.branch-lines')).toHaveCount(0);
await expect(page.locator('.branch-prefix')).toHaveText([
'├── ', '│ ├── ', '│ └── ', '│ └── ',
'├── ', '│ └── ', '└── ', ' └── ',
]);
const grid = await page.evaluate(() => {
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());
return {
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),
font: getComputedStyle(prefix).fontFamily,
};
});
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);
expect(grid.font).toContain('Mono');
await page.screenshot({ path: testInfo.outputPath('06-world.png'), fullPage: true, animations: 'disabled' });
await page.keyboard.press('z');
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'complete');
});
test('tree selection, collapse and call results are keyboard accessible', 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 page.keyboard.press('ArrowUp');
const issues = page.getByRole('treeitem', { name: 'issues', exact: true });
await expect(issues).toBeFocused();
await page.keyboard.press('ArrowLeft');
await expect(issues).toHaveAttribute('aria-expanded', 'false');
await expect(selected).toBeHidden();
await page.keyboard.press('ArrowRight');
await expect(selected).toBeVisible();
await page.keyboard.press('ArrowRight');
await expect(selected).toBeFocused();
await page.keyboard.press('Home');
await expect(page.getByRole('treeitem', { name: 'ルート /' })).toBeFocused();
await page.keyboard.press('ArrowLeft');
await expect(page.getByRole('treeitem')).toHaveCount(1);
await page.keyboard.press('ArrowRight');
await page.keyboard.press('End');
await expect(page.getByRole('treeitem', { name: 'protocol', exact: true })).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByRole('button', { name: 'Call', exact: false })).toBeFocused();
await page.keyboard.press('Enter');
await expect(page.locator('[data-result]')).toContainText('fetch_interface');
});
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.');
});
test('changing motion preference and resizing cannot strand the intro', async ({ page }) => {
await armed(page);
await page.setViewportSize({ width: 360, height: 740 });
await page.keyboard.press('Space');
await expect(page.locator('html')).toHaveAttribute('data-landing', 'playing');
await page.emulateMedia({ reducedMotion: 'reduce' });
await complete(page);
await page.mouse.wheel(0, 650);
await expect.poll(() => page.evaluate(() => scrollY)).toBeGreaterThan(0);
});
test('blocked animation module fails open to readable content', async ({ page }) => {
await page.route('**/*.js', (route) => route.abort());
await page.goto('./');
await expect(page.locator('html')).not.toHaveAttribute('data-landing', { timeout: 6000 });
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
await page.getByRole('link', { name: 'Book', exact: true }).click();
await expect(page.getByRole('heading', { level: 1 })).toContainText('通信の約束まで');
});
test('touch swipe starts the uninterrupted timeline', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'mobile', 'Touch viewport regression');
await armed(page);
const cdp = await page.context().newCDPSession(page);
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: 195, y: 550 }] });
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: 195, y: 400 }] });
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
await expect(page.locator('html')).toHaveAttribute('data-landing', 'playing');
await complete(page);
await cdp.detach();
});