Eleven dialogs passed a `label` that never reached the accessibility
tree: Web Awesome renders it into an <h2 id="title"> in the same shadow
root as the native <dialog> and never points aria-labelledby at it, so
getByRole('dialog', {name}) matched nothing and a screen reader
announced an unnamed dialog. a11y.md lists all of them under "what is
already correct".
utils/name-dialog.ts sets the IDREF, with aria-label as the fallback for
without-header (first-run-wizard), called from each host's updated().
aria-labelledby rather than aria-label because three call sites compute
their label at render time, and the heading re-renders anyway. It waits
for the dialog's own first update: wa-dialog populates its shadow root
in its own update, so a query at the host's firstUpdated names nothing.
Reaching into another library's open shadow root is deliberate and the
failure is bounded — if the structure moves, the query misses and the
dialog is as unnamed as it was.
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
import { test, expect } from '../support/fixtures.js';
|
|
import type { Page } from '@playwright/test';
|
|
|
|
/**
|
|
* Plan 007 phase 5: every `wa-dialog` in this app was an unnamed dialog.
|
|
*
|
|
* `a11y.md` lists all of them under "what is already correct" and notes
|
|
* that every call site passes a `label`. Both true, and the label never
|
|
* reached the accessibility tree: Web Awesome renders it into an
|
|
* `<h2 id="title">` in the same shadow root as the native `<dialog>` and
|
|
* never points `aria-labelledby` at it.
|
|
*
|
|
* This spec is the reason the fix is believable, and it lives here
|
|
* rather than in the component tier for one reason: **only Playwright
|
|
* computes an accessible name.** The Vitest tier queries shadow roots
|
|
* directly, so it can assert the IDREF is wired and resolves to a
|
|
* heading carrying the label — it cannot assert that anything would
|
|
* announce it. `getByRole('dialog', { name })` can, and before the fix
|
|
* it matched nothing anywhere in this app.
|
|
*
|
|
* Nor can the a11y *snapshot*: `playwright-cli snapshot` renders this
|
|
* dialog as a bare `- dialog [ref=…]` whether it is named by
|
|
* `aria-labelledby`, named by `aria-label`, or not named at all —
|
|
* checked all three ways against the running app. Verifying the fix by
|
|
* reading a snapshot would have reported failure on a working build,
|
|
* which is this plan's most-repeated trap wearing an accessibility hat:
|
|
* a probe that cannot move is not evidence.
|
|
*/
|
|
test.describe('a dialog says what it is', () => {
|
|
test('the shortcuts overlay is announced by its title', async ({ app }) => {
|
|
await app.keyboard.press('?');
|
|
|
|
await expect(
|
|
app.locator('shortcuts-overlay').getByRole('dialog', {
|
|
name: 'Keyboard Shortcuts',
|
|
}),
|
|
).toBeVisible();
|
|
|
|
await app.keyboard.press('Escape');
|
|
await expect(
|
|
app.locator('shortcuts-overlay').getByRole('dialog'),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test('a dialog with a computed label is announced by it', async ({ app }) => {
|
|
// `track-details` builds its label at render time ("Track Details"
|
|
// or "Batch Edit"), which is why it is the one checked here: the
|
|
// fix is an IDREF to the heading Web Awesome re-renders, so a label
|
|
// that changes stays announced without anything resyncing it.
|
|
await app.getByTestId('nav-tracks').click();
|
|
await expect(app.getByTestId('main-content')).toHaveAttribute(
|
|
'data-active-view',
|
|
'tracks',
|
|
);
|
|
|
|
await openRowMenu(app);
|
|
await app.getByRole('menuitem', { name: 'Track Details' }).click();
|
|
|
|
await expect(
|
|
app.getByRole('dialog', { name: 'Track Details' }),
|
|
).toBeVisible();
|
|
|
|
// Leave the app as this suite found it — the specs share one
|
|
// backend process in file order.
|
|
await app.keyboard.press('Escape');
|
|
await expect(
|
|
app.getByRole('dialog', { name: 'Track Details' }),
|
|
).toHaveCount(0);
|
|
});
|
|
});
|
|
|
|
/** Right-click the third track row, which is where the menu is anchored. */
|
|
async function openRowMenu(app: Page): Promise<void> {
|
|
await app.evaluate(() => {
|
|
const row = document
|
|
.querySelector('track-list')
|
|
?.shadowRoot?.querySelectorAll('[role="row"]')[2];
|
|
|
|
row?.dispatchEvent(
|
|
new MouseEvent('contextmenu', {
|
|
bubbles: true,
|
|
composed: true,
|
|
clientX: 200,
|
|
clientY: 300,
|
|
}),
|
|
);
|
|
});
|
|
|
|
await expect(app.getByRole('menu', { name: 'Track actions' })).toBeVisible();
|
|
}
|