Files
iptvnator/apps/web-e2e/src/player-settings-panel.e2e.ts
T
4grayandClaude Opus 5.5 adb4889b0f fix(player): keyboard focus, contrast and ARIA for controls and settings (#1769)
* fix(player): keyboard focus, contrast and ARIA for controls and settings

Dock and settings-panel icon buttons draw a 2px --pc-text ring on
:focus-visible, and Material's theme-coloured focus layer is off, so
keyboard focus shows on video in the light theme too. A focused selected
subtitle swatch now differs from one that is only selected.

Settings headings read --pc-text-secondary on denser glass
(--pc-glass-bg-dense, 0.86): 4.5:1 or more over mid-grey and white
frames. They wrap (overflow-wrap: anywhere, hyphens: auto), so long
German and Russian headings stay inside the sheet's heading column.

The settings panel is now radio groups only (SettingsRadioGroupDirective
over a CDK FocusKeyManager): one Tab stop per group on the checked option,
arrows, Home and End move focus without applying, and Space/Enter checks.
The dialog and its groups are named by real h2/h3/h4 headings, the
load-file action sits outside the subtitle radio group, the subtitle and
speed chips carry their value in their name ("Subtitles: English"), and
tune has aria-haspopup="dialog".

Adds a web E2E for the keyboard path in both themes with an axe check on
the open panel, and de/ru sheet heading wrapping; axe-core is a new dev
dependency for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(player): arrows check settings radios; subtitle chip reads On

Review follow-up:
- Arrow keys, Home and End now check the settings radio they reach, as a
  native radio group does (the directive clicks it, so the template's
  handler applies the choice); an option the engine already reports as
  checked is not applied again.
- With subtitles on but no track marked selected yet (the engine can
  report the switch before the track list), the subtitle chip reads and
  announces "On" (new SUBTITLES_ON key, 19 locales) instead of "Off".
- The swatch row has 4px padding on every side, so the outer focus ring
  is not clipped at the scroll edge of the panel body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(player): re-apply a settings radio while a switch is pending

The arrow-key check skipped any option the engine still reported as
checked. Arrowing from audio track A to B and back to A before the engine
confirmed B therefore sent no command for A, and playback ended on B with
focus on A. An arrow move always lands on an option other than the last
one applied, so it now applies unconditionally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 06:37:02 +02:00

129 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test } from './fixtures';
import {
importPlaylist,
selectPlayer,
serveClip,
startClip,
} from './player-settings-panel.fixture';
/**
* The shared controls' settings panel: on a wide player the speed chip opens
* the panel beside the video and a choice applies in place; on a compact
* player the tune button opens a bottom sheet that a tap on the video
* dismisses. Runs against the built-in HTML5 player and a local clip so no
* remote media is involved.
*/
test.use({ serviceWorkers: 'block' });
test('@web @playback settings panel opens from the speed chip and applies in place', async ({
page,
}) => {
test.setTimeout(90_000);
// Chips and the side panel need a player of at least 960px.
await page.setViewportSize({ width: 1600, height: 1000 });
await serveClip(page);
await selectPlayer(page);
await importPlaylist(page);
const { video, controls } = await startClip(page);
const speedChip = controls.locator(
'[data-test-id="player-controls-speed-chip"]'
);
await expect(speedChip).toContainText('1×');
await expect(
controls.locator('[data-test-id="player-controls-settings-panel"]')
).toHaveCount(0);
await speedChip.click();
const panel = controls.getByRole('dialog', { name: 'Settings' });
await expect(panel).toBeVisible();
await expect(panel).not.toHaveClass(/player-controls__settings--sheet/);
await expect(controls.locator('.player-controls__bar')).toHaveClass(
/player-controls__bar--panel-open/
);
// Chips fold away while the panel is open; the tune button lights up.
await expect(speedChip).toHaveCount(0);
await expect(
controls.locator('[data-test-id="player-controls-settings-button"]')
).toHaveAttribute('aria-expanded', 'true');
await panel.getByRole('radio', { name: '1.5×', exact: true }).click();
await expect
.poll(() => video.evaluate((el: HTMLVideoElement) => el.playbackRate))
.toBe(1.5);
// The choice keeps the panel open for comparison…
await expect(panel).toBeVisible();
// …and Escape closes it; the chip now reads the modified value.
await page.keyboard.press('Escape');
await expect(panel).toHaveCount(0);
await expect(controls.locator('.player-controls__bar')).not.toHaveClass(
/player-controls__bar--panel-open/
);
await expect(
controls.locator('[data-test-id="player-controls-speed-chip"]')
).toContainText('1.5×');
await expect(
controls.locator('[data-test-id="player-controls-speed-chip"]')
).toHaveClass(/player-controls__chip--modified/);
});
test('@web @playback compact player folds the chips into a tune button with a bottom sheet', async ({
page,
}) => {
test.setTimeout(90_000);
await page.setViewportSize({ width: 900, height: 700 });
await serveClip(page);
await selectPlayer(page);
await importPlaylist(page);
const { view, controls } = await startClip(page);
await expect(controls.locator('.player-controls__bar')).toHaveClass(
/player-controls__bar--compact/
);
await expect(
controls.locator('[data-test-id="player-controls-speed-chip"]')
).toHaveCount(0);
const tune = controls.locator(
'[data-test-id="player-controls-settings-button"]'
);
await tune.click();
const sheet = controls.getByRole('dialog', { name: 'Settings' });
await expect(sheet).toBeVisible();
await expect(sheet).toHaveClass(/player-controls__settings--sheet/);
await expect(controls.locator('.player-controls__bar')).toHaveClass(
/player-controls__bar--sheet-open/
);
await sheet.getByRole('radio', { name: '2×', exact: true }).click();
await expect
.poll(() =>
view
.locator('video')
.evaluate((el: HTMLVideoElement) => el.playbackRate)
)
.toBe(2);
// A modified value shows as a violet dot once the sheet is closed.
await sheet.locator('[data-test-id="player-settings-close"]').click();
await expect(sheet).toHaveCount(0);
await expect(
controls
.locator('[data-test-id="player-controls-settings-dots"]')
.locator('.player-controls__tune-dot--violet')
).toHaveCount(1);
// Reopen and dismiss by tapping the video: the tap closes the sheet
// instead of toggling playback.
await tune.click();
await expect(sheet).toBeVisible();
await view
.locator('video')
.click({ position: { x: 40, y: 40 }, force: true });
await expect(sheet).toHaveCount(0);
await expect(
controls.getByRole('button', { name: 'Play', exact: true })
).toBeVisible();
});