diff --git a/e2e/specs/failure-voice.spec.ts b/e2e/specs/failure-voice.spec.ts index 70be358..baafe60 100644 --- a/e2e/specs/failure-voice.spec.ts +++ b/e2e/specs/failure-voice.spec.ts @@ -33,11 +33,16 @@ test.describe('a failed binding says so', () => { const page = app.locator('config-page'); - // Config sections start collapsed. - await page + // Libraries is the one section that starts expanded (H-22), so ask + // the disclosure what state it is in rather than assuming one — a + // blind click used to expand it and now collapses it. + const disclosure = page .locator('config-section[heading="Libraries"]') - .locator('.header') - .click(); + .locator('.header'); + + if ((await disclosure.getAttribute('aria-expanded')) === 'false') { + await disclosure.click(); + } // Renaming the decoy to its own name is a no-op the backend // accepts, so the rename has to happen on the other row. diff --git a/e2e/specs/settings-reach.spec.ts b/e2e/specs/settings-reach.spec.ts new file mode 100644 index 0000000..9e9273b --- /dev/null +++ b/e2e/specs/settings-reach.spec.ts @@ -0,0 +1,76 @@ +import { test, expect } from '../support/fixtures.js'; + +/** + * Plan 007 phase 5: a11y.1 and a11y.2, frozen against the real app. + * + * Every `config-section` header was a bare `
` and every + * section defaults to collapsed, so every setting in the app was behind + * a control that could not be tabbed to. Downloads' two tabs were the + * same bug one page over. + * + * A component test can see the markup; only this tier can see that the + * control is reachable *through the app's own tab order*, past the + * sidebar, the header and whatever else is on screen. + */ +test.describe('Settings is reachable without a mouse', () => { + test('every section disclosure is a button that reports its state', async ({ + app, + }) => { + await app.getByTestId('nav-settings').click(); + + const headers = app.locator('config-page config-section .header'); + + await expect(headers.first()).toBeVisible(); + + const count = await headers.count(); + + expect(count).toBeGreaterThan(4); + + for (let i = 0; i < count; i++) { + const header = headers.nth(i); + + expect(await header.evaluate((el) => el.tagName)).toBe('BUTTON'); + expect(['true', 'false']).toContain( + await header.getAttribute('aria-expanded'), + ); + } + }); + + test('a collapsed section can be opened from the keyboard', async ({ + app, + }) => { + await app.getByTestId('nav-settings').click(); + + const theme = app + .locator('config-page config-section[heading="Theme"] .header') + .first(); + + await expect(theme).toHaveAttribute('aria-expanded', 'false'); + + await theme.focus(); + await app.locator('body').press('Enter'); + + await expect(theme).toHaveAttribute('aria-expanded', 'true'); + }); +}); + +test.describe("Downloads' tabs are tabs", () => { + test('arrow keys move the selection and swap the panel', async ({ app }) => { + await app.getByTestId('nav-downloads').click(); + + const view = app.locator('downloads-view'); + const requests = view.getByRole('tab', { name: 'Requests' }); + const downloads = view.getByRole('tab', { name: 'Downloads' }); + + await expect(requests).toHaveAttribute('aria-selected', 'true'); + + await requests.focus(); + await app.locator('body').press('ArrowRight'); + + await expect(downloads).toHaveAttribute('aria-selected', 'true'); + await expect(view.locator('[role="tabpanel"]')).toHaveAttribute( + 'aria-labelledby', + 'tab-downloads', + ); + }); +});