fix: rebuild landing with SVG motion and a monospace tree
Reference site / build (push) Canceled after 0s
Reference site / build (push) Canceled after 0s
This commit is contained in:
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user