test(sandbox): adapt playback rate E2E to menu UX (#1545)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Sam Potts
2026-05-18 18:03:16 -07:00
committed by GitHub
co-authored by Cursor
parent e71014d2f6
commit 2ad9fdf7ed
4 changed files with 41 additions and 11 deletions
+20 -1
View File
@@ -9,6 +9,12 @@
*
* Each selector uses a CSS `,` (or) to match either renderer.
*/
/** Toolbar: HTML wraps controls in `<media-controls>`, React in `<div class="media-controls">`. */
function withinControls(selector: string): string {
return `media-controls ${selector}, .media-controls ${selector}`;
}
export const SELECTORS = {
// Player containers
// HTML: <video-player>, 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 `<media-playback-rate-menu>` and React/Tailwind without `media-menu--playback-rate`).
*/
playbackRateMenuPanel: '[data-rate][role="menu"]',
// Sliders
// HTML: <media-time-slider>, React: horizontal .media-slider inside .media-time-controls
+13
View File
@@ -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<void> {
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<void> {
// Use the play button as anchor — it's always inside the player
+3 -5
View File
@@ -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
+5 -5
View File
@@ -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 ---