feat: add e2e test harness (#1237)
Co-authored-by: Christian Pillsbury <cpillsbury@mux.com>
@@ -0,0 +1,31 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { PlayerPage } from '../../page-objects/player';
|
||||
|
||||
/**
|
||||
* Visual snapshot tests for the audio skin.
|
||||
*
|
||||
* Verifies the audio skin's CSS and layout aren't broken.
|
||||
*/
|
||||
|
||||
const VISUAL_PAGES = [
|
||||
{ name: 'HTML', path: '/pages/html-audio-mp4.html' },
|
||||
{ name: 'React', path: '/pages/react-audio-mp4.html' },
|
||||
];
|
||||
|
||||
for (const { name, path } of VISUAL_PAGES) {
|
||||
test.describe(`Visual — Audio Skin (${name})`, () => {
|
||||
let player: PlayerPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
player = new PlayerPage(page);
|
||||
await page.goto(path);
|
||||
await player.waitForMediaReady();
|
||||
});
|
||||
|
||||
test('default paused state', async ({ page }) => {
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(player.playerRoot).toHaveScreenshot(`audio-${name.toLowerCase()}-default.png`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,106 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { DATA_ATTRS, SELECTORS } from '../../fixtures/selectors';
|
||||
import { PlayerPage } from '../../page-objects/player';
|
||||
|
||||
/**
|
||||
* Visual snapshot tests for the video skin.
|
||||
*
|
||||
* These verify the skin's CSS and layout aren't broken — not UX interactions.
|
||||
* Strategy:
|
||||
* - Screenshot the skin container in its initial paused state
|
||||
* - Generous pixel thresholds absorb cross-platform rendering differences
|
||||
* - Animations disabled globally (configured in playwright.config.ts)
|
||||
*/
|
||||
|
||||
const VISUAL_PAGES = [
|
||||
{ name: 'HTML', path: '/pages/html-video-mp4.html' },
|
||||
{ name: 'React', path: '/pages/react-video-mp4.html' },
|
||||
{ name: 'Ejected-HTML', path: '/pages/ejected-html-video-mp4.html' },
|
||||
{ name: 'Ejected-React', path: '/pages/ejected-react-video-mp4.html' },
|
||||
{ name: 'CDN', path: '/pages/cdn-video-mp4.html' },
|
||||
];
|
||||
|
||||
for (const { name, path } of VISUAL_PAGES) {
|
||||
test.describe(`Visual — Video Skin (${name})`, () => {
|
||||
let player: PlayerPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
player = new PlayerPage(page);
|
||||
await page.goto(path);
|
||||
await player.waitForMediaReady();
|
||||
});
|
||||
|
||||
test('default paused state', async ({ page }) => {
|
||||
await player.showControls();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(player.playerRoot).toHaveScreenshot(`video-${name.toLowerCase()}-default.png`);
|
||||
});
|
||||
|
||||
test('storyboard thumbnail on hover', async ({ page }) => {
|
||||
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.playerRoot).toHaveScreenshot(`video-${name.toLowerCase()}-storyboard.png`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Captions snapshot (dedicated page with subtitle track baked in) ---
|
||||
|
||||
test.describe('Visual — Captions', () => {
|
||||
let player: PlayerPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
player = new PlayerPage(page);
|
||||
await page.goto('/pages/html-video-captions.html');
|
||||
await player.waitForMediaReady();
|
||||
});
|
||||
|
||||
test('captions enabled', async ({ page, browserName }) => {
|
||||
// WebKit doesn't render data:text/vtt subtitle tracks in headless mode
|
||||
test.skip(browserName === 'webkit', 'WebKit headless does not render data:text/vtt captions');
|
||||
// Captions button should be available (subtitle track is in the HTML)
|
||||
const captionsBtn = page.locator(SELECTORS.captionsButton).first();
|
||||
await expect(captionsBtn).toHaveAttribute(DATA_ATTRS.availability, 'available', {
|
||||
timeout: 5_000,
|
||||
});
|
||||
|
||||
// Enable captions
|
||||
await captionsBtn.click();
|
||||
await expect(captionsBtn).toHaveAttribute(DATA_ATTRS.active, '');
|
||||
|
||||
// Play briefly so the caption cue at 0:00 activates, then pause
|
||||
await player.play();
|
||||
await page.waitForTimeout(500);
|
||||
await player.pause();
|
||||
await player.showControls();
|
||||
|
||||
await expect(player.playerRoot).toHaveScreenshot('captions-enabled.png');
|
||||
});
|
||||
});
|
||||
|
||||
// --- Mobile viewport snapshot (375×667) ---
|
||||
|
||||
test.describe('Visual — Mobile Layout', () => {
|
||||
test.use({ viewport: { width: 375, height: 667 } });
|
||||
|
||||
let player: PlayerPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
player = new PlayerPage(page);
|
||||
await page.goto('/pages/html-video-mp4.html');
|
||||
await player.waitForMediaReady();
|
||||
});
|
||||
|
||||
test('mobile layout', async ({ page }) => {
|
||||
await player.showControls();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
await expect(player.playerRoot).toHaveScreenshot('mobile-default.png');
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 598 KiB |
|
After Width: | Height: | Size: 573 KiB |
|
After Width: | Height: | Size: 411 KiB |
|
After Width: | Height: | Size: 595 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 491 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 598 KiB |
|
After Width: | Height: | Size: 573 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 572 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 598 KiB |
|
After Width: | Height: | Size: 574 KiB |
|
After Width: | Height: | Size: 411 KiB |
|
After Width: | Height: | Size: 582 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 598 KiB |
|
After Width: | Height: | Size: 602 KiB |
|
After Width: | Height: | Size: 411 KiB |
|
After Width: | Height: | Size: 582 KiB |