fix: grow landing trunk downward into the tree
Reference site / build (push) Canceled after 0s

This commit is contained in:
2026-10-10 17:39:40 +09:00
parent 340df47e65
commit 651cc573be
6 changed files with 90 additions and 33 deletions
+50 -7
View File
@@ -68,7 +68,7 @@ test('storyboard phases retain one root and ignore repeated triggers', async ({
await page.mouse.wheel(0, 180);
await page.keyboard.press('ArrowDown');
await page.mouse.click(20, 20);
await page.clock.runFor(1080); // Scene 4: early trunk growth, before docking.
await page.clock.runFor(1330); // Scene 4: early trunk growth, before docking.
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'extend');
expect(await page.locator('#welcome-orbit').getAttribute('d')).toMatch(new RegExp(`^${circlePath} H`));
const tangent = await page.locator('#welcome-orbit').evaluate((element, length) => {
@@ -80,28 +80,60 @@ test('storyboard phases retain one root and ignore repeated triggers', async ({
expect(tangent.dx).toBeCloseTo(40, 1);
expect(tangent.dy).toBeCloseTo(0, 1);
expect(tangent.y).toBeCloseTo(-180, 1);
// The trunk must not cut through letters still on the bottom of the circle.
expect(Number(await page.locator('.orbit textPath').getAttribute('startOffset'))).toBeGreaterThan(arcLength / 2 + 100);
const earlyLength = await page.locator('.trunk-line').evaluate((path) => (path as SVGPathElement).getTotalLength());
expect(earlyLength).toBeGreaterThan(10);
await page.screenshot({ path: testInfo.outputPath('04-unfold.png') });
await page.clock.runFor(950); // Let the horizontal line finish extending.
await page.clock.runFor(700); // Let the downward trunk finish extending.
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'extend');
const extendedLength = await page.locator('.trunk-line').evaluate((path) => (path as SVGPathElement).getTotalLength());
expect(extendedLength).toBeGreaterThan(earlyLength * 1.4);
expect(extendedLength).toBeGreaterThan(page.viewportSize()!.width * .18);
expect(extendedLength).toBeGreaterThan(page.viewportSize()!.height * .18);
await expect(page.locator('.trunk-line')).toHaveCSS('stroke-width', '10px');
const extendedRoot = await root.boundingBox();
expect(extendedRoot!.height).toBeCloseTo(initial!.height, 0);
expect(extendedRoot!.y).toBeCloseTo(initial!.y, 0);
await expect(page.locator('.demo-node:not(.root-mark)').first()).toHaveCSS('opacity', '0');
const trunkGeometry = () => page.evaluate(() => {
const path = document.querySelector<SVGPathElement>('.trunk-line')!;
const start = path.getPointAtLength(0).matrixTransform(path.getScreenCTM()!);
const tip = path.getPointAtLength(path.getTotalLength()).matrixTransform(path.getScreenCTM()!);
const root = document.querySelector('.root-mark')!.getBoundingClientRect();
return { dx: tip.x - start.x, dy: tip.y - start.y, axis: start.x - (root.left + root.width / 2), gap: start.y - root.bottom, bottom: tip.y };
});
const extended = await trunkGeometry();
expect(Math.abs(extended.dx)).toBeLessThan(.1);
expect(Math.abs(extended.axis)).toBeLessThan(.1);
expect(extended.dy).toBeGreaterThan(0);
expect(extended.gap).toBeCloseTo(8, 0);
expect(extended.bottom).toBeLessThan(page.viewportSize()!.height * .91);
await page.screenshot({ path: testInfo.outputPath('04-trunk.png') });
await page.clock.runFor(1700); // SVG/box-drawing handoff, with neither removed.
await page.clock.runFor(850);
const docking = await trunkGeometry();
expect(Math.abs(docking.dx)).toBeLessThan(.1);
expect(Math.abs(docking.axis)).toBeLessThan(.1);
expect(docking.gap).toBeGreaterThanOrEqual(0);
await page.screenshot({ path: testInfo.outputPath('05-docking.png') });
await page.clock.runFor(850); // SVG/box-drawing handoff, with neither removed.
const handoff = await page.evaluate(() => {
const path = document.querySelector<SVGPathElement>('.trunk-line')!;
const start = path.getPointAtLength(0).matrixTransform(path.getScreenCTM()!);
const tip = path.getPointAtLength(path.getTotalLength()).matrixTransform(path.getScreenCTM()!);
const target = document.querySelector('.root-wire')!.getBoundingClientRect();
return Math.hypot(tip.x - (target.right - target.width / 6), tip.y - (target.top + target.height / 2));
const first = document.querySelector('.branch-spine')!.getBoundingClientRect();
const last = [...document.querySelectorAll('.demo-node[aria-level="2"]')].at(-1)!.parentElement!.querySelector('.branch-spine')!.getBoundingClientRect();
return {
start: Math.hypot(start.x - (first.left + first.width / 2), start.y - first.top),
end: Math.hypot(tip.x - (last.left + last.width / 2), tip.y - (last.top + last.height / 2)),
spineOpacity: Number(getComputedStyle(document.querySelector('.branch-spine')!).opacity),
trunkOpacity: Number(getComputedStyle(path).opacity),
};
});
expect(handoff).toBeLessThan(1);
expect(handoff.start).toBeLessThan(1);
expect(handoff.end).toBeLessThan(1);
expect(handoff.spineOpacity).toBeGreaterThan(0);
expect(handoff.trunkOpacity).toBeGreaterThan(0);
expect(handoff.spineOpacity + handoff.trunkOpacity).toBeCloseTo(1, 4);
await page.clock.runFor(550);
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'tree');
await page.screenshot({ path: testInfo.outputPath('05-tree.png') });
@@ -127,13 +159,19 @@ test('storyboard phases retain one root and ignore repeated triggers', async ({
'├── ', '│ ├── ', '│ └── ', '│ └── ',
'├── ', '│ └── ', '└── ', ' └── ',
]);
await expect(page.locator('.root-wire')).toHaveCount(0);
await expect(page.locator('.branch-spine')).toHaveText(['├', '│', '│', '│', '├', '│', '└', ' ']);
const grid = await page.evaluate(() => {
const root = document.querySelector('.root-mark')!.getBoundingClientRect();
const spine = document.querySelector('.branch-spine')!.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());
return {
rootColumnError: Math.abs(root.left + root.width / 2 - spine.left - spine.width / 2),
rootGap: Math.abs(root.bottom - spine.top),
frameCells: frame.width / cell,
rowHeights: rows.map((row) => row.height),
nodeColumns: nodes.map((node) => (node.left - frame.left) / cell),
@@ -141,6 +179,8 @@ test('storyboard phases retain one root and ignore repeated triggers', async ({
font: getComputedStyle(prefix).fontFamily,
};
});
expect(grid.rootColumnError).toBeLessThan(1);
expect(grid.rootGap).toBeLessThan(1);
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);
@@ -192,7 +232,10 @@ test('tree selection, collapse and call results are keyboard accessible', async
await expect(page.getByRole('treeitem', { name: 'ルート /' })).toBeFocused();
await page.keyboard.press('ArrowLeft');
await expect(page.getByRole('treeitem')).toHaveCount(1);
await expect(page.locator('.branch-spine').first()).toBeHidden();
await page.keyboard.press('ArrowRight');
await expect(page.locator('.branch-spine').first()).toBeVisible();
await expect(page.locator('.branch-prefix').first()).toHaveText('├── ');
await page.keyboard.press('End');
await expect(page.getByRole('treeitem', { name: 'protocol', exact: true })).toBeFocused();
await page.keyboard.press('Tab');
+7
View File
@@ -6,6 +6,13 @@ test('home and book are usable without client JavaScript', async ({ page }, test
await page.goto('./');
await expect(page.getByRole('heading', { level: 1 })).toContainText('探索できる世界へ');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
await expect(page.locator('.branch-prefix').first()).toHaveText('├── ');
const root = await page.locator('.root-mark').boundingBox();
const spine = await page.locator('.branch-spine').first().boundingBox();
expect(root).not.toBeNull();
expect(spine).not.toBeNull();
expect(Math.abs(root!.x + root!.width / 2 - spine!.x - spine!.width / 2)).toBeLessThan(1);
expect(Math.abs(root!.y + root!.height - spine!.y)).toBeLessThan(1);
await page.screenshot({ path: testInfo.outputPath('home.png'), fullPage: true });
await page.getByRole('link', { name: 'Book', exact: true }).click();
await expect(page.getByRole('heading', { level: 1 })).toContainText('通信の約束まで');