feat(shortcuts): tell the key story once, and give the arrows back
Decision 1 keeps the unmodified single-key bindings, and Settings was the only place they were written down — three of the four categories of them, because config-page listed the categories by hand, so the autotag keys were written down nowhere at all. `?` now opens an overlay from anywhere the app owns the keyboard, and both surfaces read one table (services/shortcut-meta.ts, moved out of config-page's private static). The other half is the same explanation from the other side. Phase 1 gave the arrow keys to the grid, correctly — but all six of them, and no list in this app moves horizontally: track-list's own handler and utils/roving-rows both take Up/Down/Home/End and ignore Left/Right. So seeking stopped working from a focused row and nothing gained the keys. Reproduced in the running app: two ArrowRights on a focused track row, zero Player.Seek calls, against one per press from the body. A shifted character no longer reports Shift, so the binding is `?` and not `Shift+?` — the character already carries the shift, and a layout where it does not is a layout where "Shift+?" is wrong anyway.
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
test,
|
||||
expect,
|
||||
callBinding,
|
||||
resetEvents,
|
||||
waitForEvent,
|
||||
LONG_TRACK,
|
||||
} from '../support/fixtures.js';
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Plan 007 phase 5: the key story, told once.
|
||||
*
|
||||
* Decision 1 keeps the unmodified single-key bindings, and until now
|
||||
* Settings was the only place they were written down — three of the
|
||||
* four categories of them, so the autotag keys were written down
|
||||
* nowhere. `?` opens the overlay from anywhere the app owns the
|
||||
* keyboard.
|
||||
*
|
||||
* The other half is the same explanation from the other side: Phase 1
|
||||
* gave the arrow keys to the grid, correctly, but all six of them —
|
||||
* and no list in this app moves horizontally, so seeking stopped
|
||||
* working from a focused row and nothing gained the keys. Reproduced
|
||||
* in the running app before the fix: two ArrowRights on a focused
|
||||
* track row produced zero `Player.Seek` calls, against one per press
|
||||
* with focus on the body.
|
||||
*/
|
||||
test.describe('the shortcuts overlay', () => {
|
||||
// Two things about locating a `wa-dialog`, both found here:
|
||||
//
|
||||
// - the host is `display: contents`, so the element carrying the
|
||||
// testid always reports hidden; what is visible is the native
|
||||
// `<dialog>` inside its shadow root, and
|
||||
// - that dialog has **no accessible name**. Web Awesome renders the
|
||||
// `label` into an `<h2 id="title">` in the same shadow root and
|
||||
// never points `aria-labelledby` at it, so every dialog in this
|
||||
// app is an unnamed dialog to a screen reader. Not fixed here:
|
||||
// it is eight call sites and a helper, and it is worth doing on
|
||||
// purpose rather than as a side effect of a spec.
|
||||
const overlay = (app: Page) =>
|
||||
app.locator('shortcuts-overlay').getByRole('dialog');
|
||||
|
||||
test('? opens it, and it names the keys', async ({ app }) => {
|
||||
await app.keyboard.press('?');
|
||||
|
||||
await expect(overlay(app)).toBeVisible();
|
||||
// The rows are slotted, so they are in the overlay's own shadow
|
||||
// root rather than inside the native `<dialog>`.
|
||||
const content = app.locator('shortcuts-overlay');
|
||||
|
||||
await expect(content).toContainText('Play / Pause');
|
||||
// Autotag's bindings are in the table Settings renders three
|
||||
// quarters of.
|
||||
await expect(content).toContainText('Apply Match');
|
||||
});
|
||||
|
||||
test('Escape closes it', async ({ app }) => {
|
||||
await app.keyboard.press('?');
|
||||
await expect(overlay(app)).toBeVisible();
|
||||
|
||||
// Not a toggle: a dialog owns every unmodified key while it is up,
|
||||
// so a second `?` never reaches the shortcut service. Escape is
|
||||
// what closes a dialog here, and `wa-dialog` brings it.
|
||||
await app.keyboard.press('Escape');
|
||||
await expect(overlay(app)).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('seeking from a focused track row', () => {
|
||||
test('Left and Right seek; Up and Down still move the row', async ({
|
||||
app,
|
||||
}) => {
|
||||
await app.getByTestId('nav-tracks').click();
|
||||
await callBinding(app, 'queue.Queue.Clear');
|
||||
await resetEvents(app);
|
||||
|
||||
await app
|
||||
.getByTestId('track-row')
|
||||
.filter({ hasText: LONG_TRACK })
|
||||
.first()
|
||||
.dblclick();
|
||||
await waitForEvent(app, 'TrackChanged');
|
||||
|
||||
// Focus the roving row, which is what took the arrows.
|
||||
await app.evaluate(() => {
|
||||
const list = document.querySelector('track-list');
|
||||
const row = list?.shadowRoot?.querySelector<HTMLElement>(
|
||||
'[role="row"][tabindex="0"]',
|
||||
);
|
||||
|
||||
row?.focus();
|
||||
});
|
||||
|
||||
const position = async (): Promise<number> =>
|
||||
(await callBinding(app, 'player.Player.CurrentPositionSeconds')) as number;
|
||||
|
||||
const before = await position();
|
||||
|
||||
for (let i = 0; i < 3; i++) await app.keyboard.press('ArrowRight');
|
||||
|
||||
// Three 5 s steps, against a clock that advances 1 s per second:
|
||||
// the assertion is a jump the passage of time cannot account for.
|
||||
await expect.poll(position).toBeGreaterThan(before + 8);
|
||||
|
||||
// …and the list still moves vertically, which is what the arrows
|
||||
// were given to the grid for.
|
||||
const focusedBefore = await app.evaluate(
|
||||
() =>
|
||||
(
|
||||
document.querySelector('track-list') as unknown as {
|
||||
focusedIndex: number;
|
||||
}
|
||||
).focusedIndex,
|
||||
);
|
||||
|
||||
await app.keyboard.press('ArrowDown');
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
app.evaluate(
|
||||
() =>
|
||||
(
|
||||
document.querySelector('track-list') as unknown as {
|
||||
focusedIndex: number;
|
||||
}
|
||||
).focusedIndex,
|
||||
),
|
||||
)
|
||||
.toBe(focusedBefore + 1);
|
||||
});
|
||||
});
|
||||
@@ -77,9 +77,12 @@ async function readClocks(
|
||||
* Move focus off the track row.
|
||||
*
|
||||
* Phase 1 made rows a real grid with roving tabindex, and global
|
||||
* single-key bindings yield to a focused control that owns the key —
|
||||
* so with a row focused the arrows navigate the list rather than
|
||||
* seeking, which is correct and is not what this spec is about.
|
||||
* single-key bindings yield to a focused control that owns the key.
|
||||
* A row owns the *vertical* arrows only now (Phase 5), so Left/Right
|
||||
* seek from a focused row too and this is no longer load-bearing — it
|
||||
* stays because this spec is about the clock, not about scope
|
||||
* resolution, and it should keep measuring the same thing if that
|
||||
* rule changes again.
|
||||
*/
|
||||
async function blurDeepActive(app: Page): Promise<void> {
|
||||
await app.evaluate(() => {
|
||||
|
||||
Reference in New Issue
Block a user