mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
* 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>
129 lines
4.6 KiB
TypeScript
129 lines
4.6 KiB
TypeScript
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();
|
||
});
|