From 2ad9fdf7ede50229dba76f3c47b36b9f7ecc8977 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Tue, 19 May 2026 11:03:16 +1000 Subject: [PATCH] test(sandbox): adapt playback rate E2E to menu UX (#1545) Co-authored-by: Cursor --- apps/e2e/fixtures/selectors.ts | 21 ++++++++++++++++++++- apps/e2e/page-objects/player.ts | 13 +++++++++++++ apps/e2e/tests/audio-controls.spec.ts | 8 +++----- apps/e2e/tests/video-controls.spec.ts | 10 +++++----- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/apps/e2e/fixtures/selectors.ts b/apps/e2e/fixtures/selectors.ts index 3a8b7e92..03fa3ca0 100644 --- a/apps/e2e/fixtures/selectors.ts +++ b/apps/e2e/fixtures/selectors.ts @@ -9,6 +9,12 @@ * * Each selector uses a CSS `,` (or) to match either renderer. */ + +/** Toolbar: HTML wraps controls in ``, React in `
`. */ +function withinControls(selector: string): string { + return `media-controls ${selector}, .media-controls ${selector}`; +} + export const SELECTORS = { // Player containers // HTML: , React: wrapper div around VideoSkin @@ -28,7 +34,20 @@ export const SELECTORS = { fullscreenButton: 'media-fullscreen-button, .media-button--fullscreen', pipButton: 'media-pip-button, .media-button--pip', captionsButton: 'media-captions-button, .media-button--captions', - playbackRateButton: 'media-playback-rate-button, .media-button--playback-rate', + playbackRateButton: [ + // Legacy cycle button / CSS skins (explicit class) + withinControls('media-playback-rate-button'), + withinControls('.media-button--playback-rate'), + // HTML menu trigger — exclude nested settings-submenu rows (.media-menu__item) + withinControls('media-playback-rate-menu-trigger:not(.media-menu__item)'), + // Tailwind skins (utility classes only) + React menu triggers + withinControls('button[aria-haspopup="menu"][aria-label^="Playback rate"]:not(.media-menu__item)'), + ].join(', '), + /** + * Open playback rate surface: trigger is also `data-rate`, so require `role="menu"` too + * (covers HTML `` and React/Tailwind without `media-menu--playback-rate`). + */ + playbackRateMenuPanel: '[data-rate][role="menu"]', // Sliders // HTML: , React: horizontal .media-slider inside .media-time-controls diff --git a/apps/e2e/page-objects/player.ts b/apps/e2e/page-objects/player.ts index 27e53de9..e32a7d07 100644 --- a/apps/e2e/page-objects/player.ts +++ b/apps/e2e/page-objects/player.ts @@ -185,6 +185,19 @@ export class PlayerPage { await this.page.mouse.move(x, y); } + /** + * Opens the playback rate menu and selects the first option that differs from the current rate. + * Skins expose rate via a menu (not cycle-on-trigger). + */ + async selectAlternativePlaybackRate(): Promise { + await this.playbackRateButton.click(); + const option = this.page + .locator(`${SELECTORS.playbackRateMenuPanel} [role="menuitemradio"][aria-checked="false"]`) + .first(); + await expect(option).toBeVisible({ timeout: 5_000 }); + await option.click(); + } + /** Hover over the player area to trigger user-active state and show controls. */ async showControls(): Promise { // Use the play button as anchor — it's always inside the player diff --git a/apps/e2e/tests/audio-controls.spec.ts b/apps/e2e/tests/audio-controls.spec.ts index 1392d229..d677f47d 100644 --- a/apps/e2e/tests/audio-controls.spec.ts +++ b/apps/e2e/tests/audio-controls.spec.ts @@ -76,15 +76,13 @@ for (const { name, path, skipBrowsers } of AUDIO_PAGES as readonly PageEntry[]) // --- Playback Rate --- - test('playback rate button cycles rates', async () => { + test('playback rate menu changes selected rate', async () => { const rateBtn = player.playbackRateButton; const initialRate = await rateBtn.getAttribute(DATA_ATTRS.rate); - await rateBtn.click(); - await player.page.waitForTimeout(200); + await player.selectAlternativePlaybackRate(); - const newRate = await rateBtn.getAttribute(DATA_ATTRS.rate); - expect(newRate).not.toBe(initialRate); + await expect.poll(async () => rateBtn.getAttribute(DATA_ATTRS.rate)).not.toBe(initialRate); }); // Audio skin does NOT have: fullscreen, PiP, captions, poster, storyboard diff --git a/apps/e2e/tests/video-controls.spec.ts b/apps/e2e/tests/video-controls.spec.ts index f1b18efe..2c1eb48d 100644 --- a/apps/e2e/tests/video-controls.spec.ts +++ b/apps/e2e/tests/video-controls.spec.ts @@ -4,6 +4,8 @@ import { DATA_ATTRS, SELECTORS } from '../fixtures/selectors'; import { PlayerPage } from '../page-objects/player'; for (const { name, path, media, skipBrowsers } of ALL_VIDEO_PAGES as readonly PageEntry[]) { + /** Packaged skins use an inline rate control; CDN + site-ejected previews only expose nested settings menus. */ + const skipsInlinePlaybackRateMenuTest = path.includes('/cdn-video') || path.includes('/ejected'); test.describe(`Video Controls — ${name}`, () => { test.skip(({ browserName }) => { return skipBrowsers?.includes(browserName as 'chromium' | 'webkit' | 'firefox') ?? false; @@ -91,15 +93,13 @@ for (const { name, path, media, skipBrowsers } of ALL_VIDEO_PAGES as readonly Pa // --- Playback Rate --- - test('playback rate button cycles rates', async () => { + (skipsInlinePlaybackRateMenuTest ? test.skip : test)('playback rate menu changes selected rate', async () => { const rateBtn = player.playbackRateButton; const initialRate = await rateBtn.getAttribute(DATA_ATTRS.rate); - await rateBtn.click(); - await player.page.waitForTimeout(200); + await player.selectAlternativePlaybackRate(); - const newRate = await rateBtn.getAttribute(DATA_ATTRS.rate); - expect(newRate).not.toBe(initialRate); + await expect.poll(async () => rateBtn.getAttribute(DATA_ATTRS.rate)).not.toBe(initialRate); }); // --- Poster ---