Two of the three things that made it work are invisible to a component test against hand-built markup: the real wa-dropdown-items have not set their role when the host finishes updating, and the real wa-popup has not positioned itself, so focus() on an item is a silent no-op. Both produced a menu that opened and refused to take focus.
126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
import { test, expect } from '../support/fixtures.js';
|
|
|
|
/**
|
|
* The context menu is reachable, navigable and escapable without a
|
|
* mouse.
|
|
*
|
|
* `a11y.3`: the menu is the only route to Play, Add to Queue, Play Next,
|
|
* Add to Playlist, Favourite and Track Details, and it opened on
|
|
* `contextmenu` alone. The panel had no `role="menu"`, so the six
|
|
* `role="menuitem"`s Web Awesome sets were orphaned; nothing moved focus
|
|
* into it, nothing handled Arrow or Escape, and nothing restored focus.
|
|
*
|
|
* Frozen here because two of the three things that made it work are
|
|
* invisible to a component test running against hand-built markup: the
|
|
* real `wa-dropdown-item`s do not have their `role` yet when the host
|
|
* finishes updating, and the real `wa-popup` has not positioned itself,
|
|
* so `focus()` on an item is a silent no-op. Both produced a menu that
|
|
* opened and refused to take focus, which is exactly the bug this spec
|
|
* exists to catch.
|
|
*/
|
|
test.describe('the context menu without a mouse', () => {
|
|
/** The deep-focused element, described the way an AT would see it. */
|
|
const focused = (page: import('@playwright/test').Page) =>
|
|
page.evaluate(() => {
|
|
let el = document.activeElement;
|
|
|
|
while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement;
|
|
|
|
return {
|
|
role: el?.getAttribute('role') ?? '',
|
|
text: el?.textContent?.trim().slice(0, 40) ?? '',
|
|
};
|
|
});
|
|
|
|
/** The menu panel's own semantics, or null when it is not rendered. */
|
|
const panel = (page: import('@playwright/test').Page) =>
|
|
page.evaluate(() => {
|
|
const el = document
|
|
.querySelector('track-list')
|
|
?.shadowRoot?.querySelector('.context-menu-panel');
|
|
|
|
if (!el) return null;
|
|
|
|
return {
|
|
role: el.getAttribute('role'),
|
|
label: el.getAttribute('aria-label'),
|
|
items: el.querySelectorAll('[role="menuitem"]').length,
|
|
};
|
|
});
|
|
|
|
/** Tab to the track list's single roving stop. */
|
|
async function focusARow(app: import('@playwright/test').Page) {
|
|
for (let i = 0; i < 25; i += 1) {
|
|
await app.keyboard.press('Tab');
|
|
|
|
if ((await focused(app)).role === 'row') return;
|
|
}
|
|
|
|
throw new Error('never reached a track row');
|
|
}
|
|
|
|
test.beforeEach(async ({ app }) => {
|
|
await app.getByTestId('nav-tracks').click();
|
|
await expect(app.getByTestId('main-content')).toHaveAttribute(
|
|
'data-active-view',
|
|
'tracks',
|
|
);
|
|
});
|
|
|
|
test('Shift+F10 on a row opens a menu and focuses its first item', async ({
|
|
app,
|
|
}) => {
|
|
await focusARow(app);
|
|
await app.keyboard.press('Shift+F10');
|
|
|
|
await expect.poll(() => panel(app)).toMatchObject({
|
|
role: 'menu',
|
|
label: 'Track actions',
|
|
});
|
|
|
|
// The row is where the key came from; the menu is where focus went.
|
|
await expect.poll(async () => (await focused(app)).role).toBe('menuitem');
|
|
});
|
|
|
|
test('the arrows move through the items and wrap', async ({ app }) => {
|
|
await focusARow(app);
|
|
await app.keyboard.press('Shift+F10');
|
|
await expect.poll(async () => (await focused(app)).role).toBe('menuitem');
|
|
|
|
const first = (await focused(app)).text;
|
|
|
|
await app.keyboard.press('ArrowDown');
|
|
|
|
const second = (await focused(app)).text;
|
|
|
|
expect(second).not.toBe(first);
|
|
|
|
await app.keyboard.press('ArrowUp');
|
|
expect((await focused(app)).text).toBe(first);
|
|
|
|
// Up from the first item wraps to the last, which is what a menu
|
|
// does and what a list does not.
|
|
await app.keyboard.press('ArrowUp');
|
|
expect((await focused(app)).text).not.toBe(first);
|
|
|
|
await app.keyboard.press('Escape');
|
|
});
|
|
|
|
test('Escape closes the menu and gives the row its focus back', async ({
|
|
app,
|
|
}) => {
|
|
await focusARow(app);
|
|
|
|
const row = (await focused(app)).text;
|
|
|
|
await app.keyboard.press('Shift+F10');
|
|
await expect.poll(async () => (await focused(app)).role).toBe('menuitem');
|
|
|
|
await app.keyboard.press('Escape');
|
|
|
|
await expect.poll(() => panel(app)).toBeNull();
|
|
await expect.poll(async () => (await focused(app)).role).toBe('row');
|
|
expect((await focused(app)).text).toBe(row);
|
|
});
|
|
});
|