fix(test): remove seek tests, add missing tests (#1892)

This commit is contained in:
Sam Potts
2026-07-31 11:38:48 +10:00
committed by GitHub
parent 68e96079e5
commit 115229b3bc
13 changed files with 232 additions and 42 deletions
+2 -3
View File
@@ -69,7 +69,7 @@ export const SELECTORS = {
// Sliders
// HTML: <media-time-slider>, React: horizontal .media-slider inside .media-time-controls
timeSlider: 'media-time-slider, .media-time-controls .media-slider',
volumeSlider: 'media-volume-slider, .media-slider[data-orientation="vertical"]',
volumeSlider: 'media-volume-slider, .media-popover--volume .media-slider',
sliderThumb: 'media-slider-thumb, .media-slider__thumb',
// Display elements
@@ -81,11 +81,10 @@ export const SELECTORS = {
'[data-type="duration"].media-time',
'[data-type="remaining"].media-time',
].join(', '),
poster: 'media-poster, img[data-visible]',
poster: 'media-poster, img[data-loaded]',
bufferingIndicator: 'media-buffering-indicator, .media-buffering-indicator',
thumbnail: 'media-slider-thumbnail, .media-thumbnail__image',
// Popover & tooltip
tooltip: 'media-tooltip, .media-tooltip',
popover: 'media-popover, .media-popover',
errorDialog: 'media-error-dialog, .media-error',
+8
View File
@@ -98,6 +98,14 @@ export class PlayerPage {
return this.page.locator(SELECTORS.thumbnail).first();
}
get tooltip(): Locator {
return this.page.locator(SELECTORS.tooltip);
}
get playTooltip(): Locator {
return this.tooltip.filter({ hasText: 'Play' }).first();
}
get popover(): Locator {
return this.page.locator(SELECTORS.popover).first();
}
+20
View File
@@ -37,4 +37,24 @@ test.describe('Captions', () => {
const options = page.locator(SELECTORS.activeMenuOptions);
await expect(options).toHaveCount(2, { timeout: 5_000 });
});
test('captions button toggles captions', async ({ page }) => {
await page.evaluate(() => {
const video = document.querySelector('video') as HTMLVideoElement;
if (!video) return;
const track = document.createElement('track');
track.kind = 'subtitles';
track.label = 'English';
track.srclang = 'en';
track.src = `data:text/vtt,${encodeURIComponent('WEBVTT\n\n00:00:00.000 --> 00:00:30.000\nTest caption')}`;
video.appendChild(track);
});
await player.showControls();
await player.captionsButton.click();
await expect(player.captionsButton).toHaveAttribute(DATA_ATTRS.active, '');
await player.captionsButton.click();
await expect(player.captionsButton).not.toHaveAttribute(DATA_ATTRS.active);
});
});
+185 -24
View File
@@ -1,9 +1,89 @@
import { expect, test } from '@playwright/test';
import { ALL_VIDEO_PAGES, type PageEntry } from '../fixtures/media';
import { expect, type Page, test } from '@playwright/test';
import { ALL_VIDEO_PAGES, type PageEntry, VIDEO_PAGES } from '../fixtures/media';
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[]) {
const UI_VIDEO_PAGES = VIDEO_PAGES.filter(({ media }) => media === 'video');
function getMediaVolume(page: Page): Promise<number> {
return page.evaluate((selector) => {
const media = document.querySelector(selector) as HTMLMediaElement | null;
const actual = (media?.querySelector?.('video') as HTMLMediaElement) ?? media;
return actual?.volume ?? 1;
}, SELECTORS.media);
}
async function mockPresentation(page: Page): Promise<void> {
await page.addInitScript(() => {
let fullscreenElement: Element | null = null;
let pipElement: Element | null = null;
Object.defineProperties(document, {
fullscreenElement: { configurable: true, get: () => fullscreenElement },
fullscreenEnabled: { configurable: true, get: () => true },
pictureInPictureElement: { configurable: true, get: () => pipElement },
pictureInPictureEnabled: { configurable: true, get: () => true },
});
Object.defineProperty(HTMLElement.prototype, 'requestFullscreen', {
configurable: true,
value: async function requestFullscreen(this: HTMLElement) {
fullscreenElement = this;
document.dispatchEvent(new Event('fullscreenchange'));
},
});
Object.defineProperty(document, 'exitFullscreen', {
configurable: true,
value: async () => {
fullscreenElement = null;
document.dispatchEvent(new Event('fullscreenchange'));
},
});
Object.defineProperty(HTMLVideoElement.prototype, 'requestPictureInPicture', {
configurable: true,
value: async function requestPictureInPicture(this: HTMLVideoElement) {
pipElement = this;
this.dispatchEvent(new Event('enterpictureinpicture'));
return {};
},
});
Object.defineProperties(HTMLVideoElement.prototype, {
webkitPresentationMode: {
configurable: true,
get: function webkitPresentationMode(this: HTMLVideoElement) {
return pipElement === this ? 'picture-in-picture' : 'inline';
},
},
webkitSetPresentationMode: {
configurable: true,
value: function webkitSetPresentationMode(this: HTMLVideoElement, mode: string) {
const wasPip = pipElement === this;
pipElement = mode === 'picture-in-picture' ? this : null;
if (!wasPip && pipElement === this) {
this.dispatchEvent(new Event('enterpictureinpicture'));
} else if (wasPip && pipElement !== this) {
this.dispatchEvent(new Event('leavepictureinpicture'));
}
},
},
});
Object.defineProperty(document, 'exitPictureInPicture', {
configurable: true,
value: async () => {
const video = pipElement;
pipElement = null;
video?.dispatchEvent(new Event('leavepictureinpicture'));
},
});
});
}
for (const { name, path, skipBrowsers } of ALL_VIDEO_PAGES as readonly PageEntry[]) {
const rateMenu = !path.includes('/cdn-video') && !path.includes('/ejected');
test.describe(`Video Controls — ${name}`, () => {
test.skip(({ browserName }) => {
@@ -20,10 +100,6 @@ for (const { name, path, media, skipBrowsers } of ALL_VIDEO_PAGES as readonly Pa
// --- Grouped: control presence & attributes (one navigation) ---
test('all controls are present with correct attributes', async () => {
await expect(player.seekForward).toBeAttached();
await expect(player.seekForward).toHaveAttribute(DATA_ATTRS.direction, 'forward');
await expect(player.seekBackward).toBeAttached();
await expect(player.seekBackward).toHaveAttribute(DATA_ATTRS.direction, 'backward');
await expect(player.muteButton).toHaveAttribute(DATA_ATTRS.volumeLevel);
await expect(player.fullscreenButton).toHaveAttribute(DATA_ATTRS.availability);
await expect(player.pipButton).toHaveAttribute(DATA_ATTRS.availability);
@@ -47,15 +123,6 @@ for (const { name, path, media, skipBrowsers } of ALL_VIDEO_PAGES as readonly Pa
await expect(player.playButton).toHaveAttribute(DATA_ATTRS.paused, '');
});
// --- Seek ---
test('seek forward advances playback', async () => {
test.skip(media === 'native-hls-video', 'seek before playback not yet supported');
await player.seekForward.click();
await expect(player.playButton).toHaveAttribute(DATA_ATTRS.started, '');
});
// --- Time Slider ---
test('time slider allows seeking', async ({ page }) => {
@@ -102,18 +169,112 @@ for (const { name, path, media, skipBrowsers } of ALL_VIDEO_PAGES as readonly Pa
// --- Poster ---
test('poster hides after playback starts', async ({ page }) => {
test('poster hides after playback starts', async () => {
await expect(player.poster).toBeAttached();
await player.play();
const posterVisible = await page.evaluate((attr) => {
const htmlPoster = document.querySelector('media-poster');
if (htmlPoster) return htmlPoster.hasAttribute(attr);
await expect(player.poster).not.toHaveAttribute(DATA_ATTRS.visible);
});
});
}
const reactPoster = document.querySelector(`img[${attr}]`);
return !!reactPoster;
}, DATA_ATTRS.visible);
for (const { name, path } of UI_VIDEO_PAGES) {
test.describe(`Video Controls — ${name} UI`, () => {
let player: PlayerPage;
expect(posterVisible).toBe(false);
test.beforeEach(async ({ page }) => {
await mockPresentation(page);
player = new PlayerPage(page);
await page.goto(path);
await player.waitForMediaReady();
});
test('volume slider changes volume', async ({ page }) => {
await player.showControls();
await player.muteButton.hover();
await expect(player.volumeSlider).toBeVisible();
const box = await player.volumeSlider.boundingBox();
if (!box) throw new Error('Volume slider not visible');
await page.mouse.click(box.x + box.width / 2, box.y + box.height * 0.75);
await expect.poll(() => getMediaVolume(page)).toBeLessThan(0.5);
});
test('buffering indicator follows waiting state', async ({ page }) => {
await player.play();
await page.evaluate((selector) => {
const media = document.querySelector(selector) as HTMLMediaElement | null;
const actual = (media?.querySelector?.('video') as HTMLMediaElement) ?? media;
if (!actual) return;
Object.defineProperties(actual, {
paused: { configurable: true, get: () => false },
readyState: { configurable: true, get: () => HTMLMediaElement.HAVE_CURRENT_DATA },
});
actual.dispatchEvent(new Event('waiting'));
}, SELECTORS.media);
await expect(player.bufferingIndicator).toHaveAttribute(DATA_ATTRS.visible, '', { timeout: 2_000 });
await page.evaluate((selector) => {
const media = document.querySelector(selector) as HTMLMediaElement | null;
const actual = (media?.querySelector?.('video') as HTMLMediaElement) ?? media;
if (!actual) return;
Object.defineProperty(actual, 'readyState', {
configurable: true,
get: () => HTMLMediaElement.HAVE_ENOUGH_DATA,
});
actual.dispatchEvent(new Event('playing'));
}, SELECTORS.media);
await expect(player.bufferingIndicator).not.toHaveAttribute(DATA_ATTRS.visible);
});
test('play button shows its tooltip on hover', async ({ page }) => {
await player.showControls();
await player.playButton.hover();
await expect(player.playTooltip).toHaveAttribute(DATA_ATTRS.open, '', { timeout: 2_000 });
});
test('play button reflects ended playback', async ({ page }) => {
await page.evaluate((selector) => {
const media = document.querySelector(selector) as HTMLMediaElement | null;
const actual = (media?.querySelector?.('video') as HTMLMediaElement) ?? media;
if (!actual) return;
Object.defineProperties(actual, {
ended: { configurable: true, get: () => true },
paused: { configurable: true, get: () => true },
});
actual.dispatchEvent(new Event('ended'));
}, SELECTORS.media);
await expect(player.playButton).toHaveAttribute(DATA_ATTRS.ended, '');
});
test('fullscreen button toggles fullscreen', async () => {
await expect(player.fullscreenButton).toHaveAttribute(DATA_ATTRS.availability, 'available');
await player.fullscreenButton.click();
await expect(player.fullscreenButton).toHaveAttribute(DATA_ATTRS.fullscreen, '');
await player.fullscreenButton.click();
await expect(player.fullscreenButton).not.toHaveAttribute(DATA_ATTRS.fullscreen);
});
test('PiP button toggles picture-in-picture', async () => {
await expect(player.pipButton).toHaveAttribute(DATA_ATTRS.availability, 'available');
await player.pipButton.click();
await expect(player.pipButton).toHaveAttribute(DATA_ATTRS.pip, '');
await player.pipButton.click();
await expect(player.pipButton).not.toHaveAttribute(DATA_ATTRS.pip);
});
});
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

+2 -3
View File
@@ -41,9 +41,8 @@ for (const { name, path } of VISUAL_PAGES) {
await player.hoverTimeSlider(50);
// Wait for thumbnail to finish loading (deterministic, no fixed timeout)
const thumbnail = page.locator(SELECTORS.thumbnail).first();
await expect(thumbnail).toBeAttached({ timeout: 10_000 });
await expect(thumbnail).not.toHaveAttribute(DATA_ATTRS.loading, { timeout: 10_000 });
await expect(player.thumbnail).toBeAttached({ timeout: 10_000 });
await expect(player.thumbnail).not.toHaveAttribute(DATA_ATTRS.loading, { timeout: 10_000 });
await expect(player.playerRoot).toHaveScreenshot(`video-${name.toLowerCase()}-storyboard.png`);
});
-12
View File
@@ -6,18 +6,6 @@ media-tooltip-group {
display: contents;
}
.media-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
white-space: nowrap;
border: 0;
clip: rect(0, 0, 0, 0);
}
:host {
/* `display:grid` fixes a weird issue with Safari when setting aspect-ratio */
display: grid;
@@ -1,3 +1,5 @@
@import "../../../shared/css/sr-only.css";
/* ==========================================================================
Reset
========================================================================== */
@@ -1,3 +1,5 @@
@import "../../../shared/css/sr-only.css";
/* ==========================================================================
Reset
========================================================================== */
+11
View File
@@ -0,0 +1,11 @@
.media-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
white-space: nowrap;
border: 0;
clip: rect(0, 0, 0, 0);
}