feat: add dedicated home page and automatic dark mode
Reference site / build (push) Canceled after 0s
Reference site / build (push) Canceled after 0s
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user