TypeScript · @playwright/test
Media streaming with Playwright
Pick a profile (Kids hides mature titles), browse scrolling rows, save to My List, and drive a custom player — play/pause, seek, ±10s, speed, volume, subtitles, skip intro — with progress that feeds “Continue watching”.
App under test
▶️ StreamBox
- URL
- /apps/stream
- Login
- viewer@streambox.tv / Stream@123
- Profiles
- Amir · Sara · Kids
- Skip intro
- shown 0:03–0:30
- Plans
- Basic · Standard · Premium
Scenarios to automate
Work top to bottom — each one adds a new technique.
- 1
Account login
BeginnerEmail + password (sb-login-id, sb-password, sb-login) before the profile picker; sb-logout returns to the login.
- 2
Profile gate
BeginnerNo profile → profile-picker; switch-profile returns to it. Kids hides A/UA titles and restricts /watch.
- 3
Unique row locators
IntermediateA title can appear in several rows, so cards are open-<row>-<id> (e.g. open-trending-t3).
- 4
Player state
Intermediateplayer-toggle flips aria-label Play/Pause and data-playing on player; player-time advances.
- 5
Transient button
Advancedskip-intro only exists between 0:03 and 0:30 — wait for it, click, and assert the time jumped to 0:30.
- 6
Range sliders
IntermediateDrive player-seek and player-volume (fill / keyboard / JS); mute toggles the volume to 0.
- 7
Continue watching
AdvancedWatch a bit, go home: row-continue shows the title with a progress bar.
- 8
Reorder a list
IntermediateIn My List, move-up/down-<id> or drag mylist-item to reorder; order persists.
- 9
Plan change modal
Beginnerchoose-<plan> shows an upgrade or downgrade note; confirm-plan updates current-plan.
Sample Playwright suite
A working starting point that finds elements every way Playwright can — by role, label, text, id, name, CSS, XPath and data-testid — so you practice them all. Pick your language, switch to data-testid only to compare, or try your own in the Locator Playground.
import { test, expect } from '@playwright/test';
// playwright.config.ts → use: { baseURL: 'https://www.practiceqaautomation.com' }
// Locators in this suite: getByRole ×3 · getByLabel ×1 · CSS ×1 · XPath ×2 · getByTestId ×14
test('watches a title and resumes it', async ({ page }) => {
await page.goto('/apps/stream');
// Sign in first
await page.getByLabel('Email').fill('viewer@streambox.tv');
await page.locator('#sb-password').fill('Stream@123');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.getByTestId('profile-amir').click();
await expect(page.getByTestId('active-profile')).toContainText('Amir');
await page.getByTestId('open-trending-t3').click();
await expect(page.getByTestId('title-modal')).toBeVisible();
await page.getByTestId('mylist-t3').click();
await expect(page.getByTestId('mylist-t3')).toContainText('In My List');
await page.getByTestId('play-t3').click();
await expect(page.getByTestId('watch-title')).toContainText('Orbit Nine');
await page.getByRole('combobox', { name: 'Playback speed' }).selectOption('2');
await page.getByTestId('player-toggle').click();
// Skip intro appears a moment after playback starts
await page.locator('xpath=//button[normalize-space()="Skip intro"]').click();
await expect(page.getByTestId('player-time')).toContainText('00:3');
await page.getByTestId('player-toggle').click();
await page.getByTestId('skip-forward-10').click();
await expect(page.getByTestId('player-time')).toContainText('00:4');
await page.locator('xpath=//a[normalize-space()="Home"]').click();
await expect(page.getByTestId('row-continue')).toContainText('Orbit Nine');
await page.getByRole('link', { name: 'My List' }).click();
await expect(page.getByTestId('mylist-item').nth(0)).toContainText('Orbit Nine');
});