/**
* TreeView — end-to-end tests (Playwright, real browser).
*
* These drive the rendered "Default" Storybook story and verify the REQUIRED
* APG keyboard model that only works with real focus / `activeElement`: Up/Down
* navigation over visible nodes, Right/Left expand-collapse-and-move, Home/End,
* type-ahead, and `*` to expand siblings. The ARIA wiring, selection modes and
* JS API are covered by the Vitest unit suite (`TreeView.test.ts`).
*
* Requires `bunx playwright install chromium` first. The Storybook web server is
* started automatically (see `playwright.config.ts`).
*
* The Default story tree (document order, with `src` and `src/components`
* initially expanded):
* README.md
* src [expanded]
* index.ts
* components [expanded]
* Accordion.ts
* TreeView.ts
* docs [collapsed]
* getting-started.md
* accessibility.md
*/
import { test, expect } from '@playwright/test';
const STORY = '/iframe.html?id=components-treeview--default&viewMode=story';
/**
* Locate a treeitem by the exact text of its OWN label. An XPath `./span`
* step matches only the item's direct-child label, so a parent node is never
* matched by one of its descendants' labels (which live in nested `
`s).
*/
function item(page: import('@playwright/test').Page, label: string) {
return page.locator(
`xpath=//li[contains(concat(" ", normalize-space(@class), " "), " treeview__item ")]` +
`[./span[contains(concat(" ", normalize-space(@class), " "), " treeview__label ")` +
` and normalize-space(.)=${JSON.stringify(label)}]]`,
);
}
test.beforeEach(async ({ page }) => {
await page.goto(STORY);
await page.waitForSelector('.treeview__item');
});
test('Down and Up move focus over visible nodes only', async ({ page }) => {
await item(page, 'README.md').focus();
await page.keyboard.press('ArrowDown');
await expect(item(page, 'src')).toBeFocused();
await page.keyboard.press('ArrowDown');
await expect(item(page, 'index.ts')).toBeFocused();
await page.keyboard.press('ArrowUp');
await expect(item(page, 'src')).toBeFocused();
});
test('Right expands a collapsed parent then moves to its first child', async ({ page }) => {
const docs = item(page, 'docs');
await docs.focus();
await expect(docs).toHaveAttribute('aria-expanded', 'false');
await page.keyboard.press('ArrowRight');
await expect(docs).toHaveAttribute('aria-expanded', 'true');
await page.keyboard.press('ArrowRight');
await expect(item(page, 'getting-started.md')).toBeFocused();
});
test('Left collapses an expanded parent then moves to the parent', async ({ page }) => {
const src = item(page, 'src');
await src.focus();
await expect(src).toHaveAttribute('aria-expanded', 'true');
await page.keyboard.press('ArrowLeft');
await expect(src).toHaveAttribute('aria-expanded', 'false');
// index.ts is now hidden; move onto a child then Left returns to the parent.
await page.keyboard.press('ArrowRight'); // re-expand
await page.keyboard.press('ArrowRight'); // focus index.ts
await expect(item(page, 'index.ts')).toBeFocused();
await page.keyboard.press('ArrowLeft'); // leaf → focus parent
await expect(src).toBeFocused();
});
test('Home and End move to the first and last visible node', async ({ page }) => {
await item(page, 'index.ts').focus();
await page.keyboard.press('End');
await expect(item(page, 'TreeView.ts')).toBeFocused();
await page.keyboard.press('Home');
await expect(item(page, 'README.md')).toBeFocused();
});
test('type-ahead focuses the next node whose label matches', async ({ page }) => {
await item(page, 'README.md').focus();
await page.keyboard.press('d'); // "docs"
await expect(item(page, 'docs')).toBeFocused();
});
test('asterisk expands all sibling parents at the level', async ({ page }) => {
// Collapse src first so both top-level parents (src, docs) are collapsed.
await item(page, 'src').focus();
await page.keyboard.press('ArrowLeft');
await expect(item(page, 'src')).toHaveAttribute('aria-expanded', 'false');
await page.keyboard.press('*');
await expect(item(page, 'src')).toHaveAttribute('aria-expanded', 'true');
await expect(item(page, 'docs')).toHaveAttribute('aria-expanded', 'true');
});
test('Enter selects the focused node (aria-selected)', async ({ page }) => {
const readme = item(page, 'README.md');
await readme.focus();
await page.keyboard.press('Enter');
await expect(readme).toHaveAttribute('aria-selected', 'true');
});