/** * 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'); });