feat: add dedicated home page and automatic dark mode
Reference site / build (push) Canceled after 0s

This commit is contained in:
2026-10-16 00:52:01 +09:00
parent 839eb2a6a4
commit b7cef01afd
15 changed files with 866 additions and 70 deletions
+199 -17
View File
@@ -55,6 +55,104 @@ async function expectMetadata(page: Page, expectedURL: string) {
}
}
test('home response contains independent root content and the unchanged public tree', async ({ request }, testInfo) => {
const base = testInfo.project.use.baseURL!;
const response = await request.get(base);
expect(response.ok()).toBe(true);
const $ = load(await response.text());
const chapterResponse = await request.get(bookURL('/1-concept.md', base));
expect(chapterResponse.ok()).toBe(true);
const chapter = load(await chapterResponse.text());
expect($(stage).attr('data-current-path')).toBe('/');
expect($(article).length).toBe(1);
expect($('article.prose').length).toBe(1);
const home = $(article).find('.root-page[data-root-page]');
expect(home.length).toBe(1);
expect($(article).find('h1').length).toBe(1);
expect(home.find('h1').attr('aria-label')).toBe('WIP');
const heading = home.find('h1').clone();
heading.find('[aria-hidden="true"]').remove();
expect(heading.text().trim()).toBe('WIP');
expect(home.find('p').text()).toContain('Worldspace Interface Protocol');
const copy = home.find('p').text();
expect(copy).toMatch(/人(?:間)?/);
expect(copy).toMatch(/AI/);
expect(copy).toMatch(/システム/);
expect(copy).toMatch(/共通/);
expect(copy).toMatch(/インタフェース/);
const nav = home.find('nav[aria-label="ルートページのナビゲーション"]');
expect(nav.length).toBe(1);
expect(nav.find('a').length).toBe(2);
expect(nav.find('a').filter((_, el) => $(el).text().includes('Book')).attr('href')).toBe(new URL('book/', base).pathname);
const source = $('.site-header a').filter((_, el) => $(el).text().includes('Source')).attr('href');
expect(source).toMatch(/^https:\/\//);
expect(nav.find('a').filter((_, el) => $(el).text().includes('Source')).attr('href')).toBe(source);
expect($(article).html()).not.toBe(chapter(article).html());
expect($(article).text()).not.toContain('1. コンセプト');
const chapterCopy = chapter(article).find('p').filter((_, el) => Boolean(chapter(el).text().trim())).first().text().trim();
expect(chapterCopy).toBeTruthy();
expect($(article).text()).not.toContain(chapterCopy);
expect($('.root-mark').attr('aria-current')).toBe('page');
expect($('.root-mark').attr('aria-selected')).toBe('true');
expect($('.demo-node[aria-current="page"]').length).toBe(1);
const treeShape = (document: ReturnType<typeof load>) => document('.demo-node').map((_, el) => {
const node = document(el);
return { tag: node.prop('tagName'), path: node.attr('data-path'), href: node.attr('href'), role: node.attr('role'), level: node.attr('aria-level'), parent: node.parent().prop('tagName') };
}).get();
expect(treeShape($)).toEqual(treeShape(chapter));
expect($('.demo-node').map((_, el) => $(el).attr('data-path')).get()).toEqual(treePaths);
const serialized = $('#landing-documents').text();
expect(serialized).not.toContain('<');
const nodes = JSON.parse(serialized) as Record<string, unknown>[];
expect(nodes.map((node) => node.path)).toEqual(treePaths);
for (const node of nodes) {
expect(node).not.toHaveProperty('html');
expect(node).not.toHaveProperty('body');
}
expect($('meta[property="og:title"]').attr('content')).toBe($('title').text());
expect($('meta[name="description"]').attr('content')).toBeTruthy();
expect($('meta[property="og:description"]').attr('content')).toBe($('meta[name="description"]').attr('content'));
if (process.env.SITE_URL) {
expect($('link[rel="canonical"]').attr('href')).toBe(new URL(new URL(base).pathname, process.env.SITE_URL).href);
} else {
expect($('link[rel="canonical"]').length).toBe(0);
}
});
test('root content navigation works without changing the tree or overflowing narrow mobile viewports', async ({ page }, testInfo) => {
const base = testInfo.project.use.baseURL!;
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto('./');
await settled(page, '/');
const home = page.locator(article + ' .root-page[data-root-page]');
await expect(home).toBeVisible();
await expect(home.getByRole('heading', { level: 1, name: 'WIP', exact: true })).toBeVisible();
await expect(page.locator(article + ' h1')).toHaveCount(1);
await expect(home.locator('p').filter({ hasText: 'Worldspace Interface Protocol' })).toBeVisible();
await expect(page.locator(article)).not.toContainText('1. コンセプト');
const nav = home.getByRole('navigation', { name: 'ルートページのナビゲーション', exact: true });
await expect(nav.getByRole('link')).toHaveCount(2);
await expect(nav.getByRole('link', { name: /^Book(?:\s|$)/ })).toHaveAttribute('href', new URL('book/', base).pathname);
const source = await page.locator('.site-header').getByRole('link', { name: 'Source', exact: true }).getAttribute('href');
expect(source).toMatch(/^https:\/\//);
await expect(nav.getByRole('link', { name: /^Source(?:\s|$)/ })).toHaveAttribute('href', source!);
if (testInfo.project.name === 'mobile') {
for (const width of [320, 360, 390]) {
await page.setViewportSize({ width, height: 740 });
await expect(home).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), `${width}px home has no horizontal overflow`).toBe(true);
const bounds = await home.boundingBox();
expect(bounds!.x).toBeGreaterThanOrEqual(0);
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width);
}
}
await nav.getByRole('link', { name: /^Book(?:\s|$)/ }).click();
await expect(page).toHaveURL(new URL('book/', base).href);
await settled(page, '/book');
await expect(page.locator('[data-root-page]')).toHaveCount(0);
await expect(page.locator(article).locator('a[href$="/book/1-concept/"]')).toBeVisible();
});
test('all 14 document responses contain the current article and metadata-only public tree', async ({ request }, testInfo) => {
expect(documentPaths).toHaveLength(14);
expect(treeNodeCount).toBe(16);
@@ -293,8 +391,8 @@ test('directory anchors navigate independently from the sibling plus/minus toggl
await nodeAt(page, '/').focus();
await page.keyboard.press('Enter');
await expect(page).toHaveURL(base);
await settled(page, '/1-concept.md');
await expect(page.locator(article + ' h1')).toHaveText('1. コンセプト');
await settled(page, '/');
await expect(page.locator(article).getByRole('heading', { level: 1, name: 'WIP', exact: true })).toBeVisible();
await expect(page.locator('.sidebar, .mobile-chapters, .mobile-toc, .page-toc, .pagination')).toHaveCount(0);
});
@@ -327,7 +425,7 @@ test('persisted tree retains scroll and collapse, expanding only an ancestor nee
await expect(page.getByRole('treeitem')).toHaveCount(1);
expect(page.url()).toBe(bookURL(second, base));
expect(await page.locator(article).innerHTML()).toBe(secondHTML);
await page.getByRole('link', { name: 'Book', exact: true }).click();
await page.locator('.site-header').getByRole('link', { name: 'Book', exact: true }).click();
await settled(page, '/book');
await expect(nodeAt(page, '/')).toHaveAttribute('aria-expanded', 'true');
await expect(nodeAt(page, '/4-client.md')).toHaveAttribute('aria-expanded', 'false');
@@ -346,6 +444,49 @@ test('persisted tree retains scroll and collapse, expanding only an ancestor nee
expect(await pane!.evaluate((el) => el === document.querySelector('.tree-pane'))).toBe(true);
});
test('home preserves a collapsed root until selecting a child requires expansion', async ({ page }, testInfo) => {
const base = testInfo.project.use.baseURL!;
const path = '/1-concept.md';
await page.addInitScript(recordIntroStates);
await page.goto(bookURL(path, base));
await settled(page, path);
const pane = await page.locator('.tree-pane').elementHandle();
await page.locator('.node-toggle[data-toggle-path="/4-client.md"]').click();
await toggle(page).click();
await expect(nodeAt(page, '/book')).toHaveAttribute('aria-expanded', 'false');
await nodeAt(page, '/').focus();
await page.keyboard.press('ArrowLeft');
await expect(nodeAt(page, '/')).toHaveAttribute('aria-expanded', 'false');
await page.keyboard.press('Enter');
await expect(page).toHaveURL(base);
await settled(page, '/');
await expect(nodeAt(page, '/')).toHaveAttribute('aria-expanded', 'false');
await expect(nodeAt(page, '/book')).toBeHidden();
await expect(page.getByRole('treeitem')).toHaveCount(1);
await expect(page.locator(article).getByRole('heading', { level: 1, name: 'WIP', exact: true })).toBeVisible();
// The content Book link selects a root child, not its descendants.
await page.getByRole('navigation', { name: 'ルートページのナビゲーション', exact: true }).getByRole('link', { name: /^Book(?:\s|$)/ }).click();
await settled(page, '/book');
await expect(nodeAt(page, '/')).toHaveAttribute('aria-expanded', 'true');
await expect(nodeAt(page, '/book')).toHaveAttribute('aria-expanded', 'false');
await expect(nodeAt(page, '/4-client.md')).toHaveAttribute('aria-expanded', 'false');
await nodeAt(page, '/').click();
await settled(page, '/');
await expect(nodeAt(page, '/book')).toHaveAttribute('aria-expanded', 'false');
await nodeAt(page, '/').focus();
await page.keyboard.press('ArrowLeft');
await expect(nodeAt(page, '/')).toHaveAttribute('aria-expanded', 'false');
// Native hidden file activation must reveal only the ancestors needed by it.
await nodeAt(page, path).evaluate((el) => (el as HTMLAnchorElement).click());
await settled(page, path);
await expect(nodeAt(page, '/')).toHaveAttribute('aria-expanded', 'true');
await expect(nodeAt(page, '/book')).toHaveAttribute('aria-expanded', 'true');
await expect(nodeAt(page, path)).toBeVisible();
await expect(nodeAt(page, '/4-client.md')).toHaveAttribute('aria-expanded', 'false');
expect(await pane!.evaluate((el) => el === document.querySelector('.tree-pane'))).toBe(true);
await expectNoIntroStates(page);
});
test('internal Markdown cross-page and fragment links use the same router and current tree', async ({ page }, testInfo) => {
const base = testInfo.project.use.baseURL!;
await page.goto(bookURL('/3-protocol.md', base));
@@ -454,34 +595,72 @@ test('client return home skips intro, while a hard reload home may arm it again'
const base = testInfo.project.use.baseURL!;
await page.goto('./');
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'armed');
await expect(page.locator(stage)).toHaveAttribute('data-current-path', '/');
await page.keyboard.press('Escape');
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'complete');
await settled(page, '/');
const homeHTML = await page.locator(article).innerHTML();
const homeTitle = await page.title();
await expectMetadata(page, base);
await page.evaluate(recordIntroStates);
const pane = await page.locator('.tree-pane').elementHandle();
const tree = await page.locator('.demo-tree').elementHandle();
const nodes = await page.locator('.demo-node').elementHandles();
const expectHome = async () => {
await expect(page).toHaveURL(base);
await settled(page, '/');
await expect(page.locator('.intro-skip')).toBeHidden();
await expect(page.locator(article).getByRole('heading', { level: 1, name: 'WIP', exact: true })).toBeVisible();
expect(await page.locator(article).innerHTML()).toBe(homeHTML);
await expect(page).toHaveTitle(homeTitle);
await expectMetadata(page, base);
};
await nodeAt(page, '/2-core-model.md').click();
await settled(page, '/2-core-model.md');
expect(await page.locator(article).innerHTML()).not.toBe(homeHTML);
await page.locator('.brand').click();
await expect(page).toHaveURL(base);
await settled(page, '/1-concept.md');
await expect(page.locator('.intro-skip')).toBeHidden();
await expect(page.locator(article + ' h1')).toHaveText('1. コンセプト');
await expectHome();
expect(await pane!.evaluate((el) => el === document.querySelector('.tree-pane'))).toBe(true);
// The book directory has its own route; the resting slash returns to HOME.
await nodeAt(page, '/book').click();
await page.getByRole('navigation', { name: 'ルートページのナビゲーション', exact: true }).getByRole('link', { name: /^Book(?:\s|$)/ }).click();
await expect(page).toHaveURL(new URL('book/', base).href);
await settled(page, '/book');
const bookHTML = await page.locator(article).innerHTML();
const bookTitle = await page.title();
expect(bookHTML).not.toBe(homeHTML);
await expectMetadata(page, new URL('book/', base).href);
await nodeAt(page, '/').click();
await expect(page).toHaveURL(base);
await settled(page, '/1-concept.md');
await expectHome();
await page.goBack();
await expect(page).toHaveURL(new URL('book/', base).href);
await settled(page, '/book');
expect(await page.locator(article).innerHTML()).toBe(bookHTML);
await expect(page).toHaveTitle(bookTitle);
await expectMetadata(page, new URL('book/', base).href);
await page.goBack();
await expect(page).toHaveURL(base);
await settled(page, '/1-concept.md');
await expectHome();
await page.goBack();
await expect(page).toHaveURL(bookURL('/2-core-model.md', base));
await settled(page, '/2-core-model.md');
await expectMetadata(page, bookURL('/2-core-model.md', base));
await page.goBack();
await expectHome();
for (const path of ['/2-core-model.md', '/', '/book', '/']) {
await page.goForward();
await expect(page).toHaveURL(urlFor(path, base));
await settled(page, path);
await expectMetadata(page, urlFor(path, base));
if (path === '/') await expectHome();
}
expect(await tree!.evaluate((el) => el === document.querySelector('.demo-tree'))).toBe(true);
for (const node of nodes) {
expect(await node.evaluate((el) => el === document.querySelector(`.demo-node[data-path="${(el as HTMLElement).dataset.path}"]`))).toBe(true);
}
await expect(page.locator('.demo-node')).toHaveCount(treeNodeCount);
expect(await page.locator('.demo-node').evaluateAll((nodes) => nodes.map((node) => (node as HTMLElement).dataset.path))).toEqual(treePaths);
await expectNoIntroStates(page);
await page.reload();
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'armed');
await expect(page.locator(stage)).toHaveAttribute('data-current-path', '/');
});
for (const action of ['skip-click', 'escape', 'root-click', 'key'] as const) {
@@ -541,16 +720,16 @@ for (const action of ['skip-click', 'escape', 'root-click', 'key'] as const) {
released = true;
releaseFont();
if (skipping) {
await settled(page, '/1-concept.md');
await settled(page, '/');
await page.clock.runFor(4000);
await settled(page, '/1-concept.md'); // Font completion must not replay an already-skipped intro.
await settled(page, '/'); // Font completion must not replay an already-skipped intro.
} else {
// No second click/key: the input received during boot must start the timeline.
await expect(page.locator('html')).toHaveAttribute('data-landing', 'playing');
await expect(page.locator(stage)).toHaveAttribute('data-phase', 'flow');
expect(page.url()).toBe(testInfo.project.use.baseURL!);
await page.clock.runFor(4000);
await settled(page, '/1-concept.md');
await settled(page, '/');
}
expect(page.url()).toBe(testInfo.project.use.baseURL!);
} finally {
@@ -589,7 +768,10 @@ test.describe('JavaScript-free directory and error routes', () => {
await expect(page.locator(stage)).toHaveAttribute('data-current-path', '/book');
await nodeAt(page, '/').click();
await expect(page).toHaveURL(base);
await expect(page.locator(stage)).toHaveAttribute('data-current-path', '/1-concept.md');
await expect(page.locator(stage)).toHaveAttribute('data-current-path', '/');
await expect(nodeAt(page, '/')).toHaveAttribute('aria-current', 'page');
await expect(nodeAt(page, '/')).toHaveAttribute('aria-selected', 'true');
await expect(page.locator(article).getByRole('heading', { level: 1, name: 'WIP', exact: true })).toBeVisible();
});
});