From 5e13f739122dc42ce2cfe2b75d3bc5cad7f109ab Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 4 Aug 2026 16:33:25 +0200 Subject: [PATCH] test(playback): cover temporary player recommendation --- apps/web-e2e/eslint.config.mjs | 3 + apps/web-e2e/src/fixtures/playback/corrupt.ts | 1 + .../src/fixtures/playback/fatal-media.m3u8 | 7 + .../src/playback-recommendations.e2e.ts | 141 ++++++++++++++++++ apps/web-e2e/tsconfig.json | 3 +- 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 apps/web-e2e/src/fixtures/playback/corrupt.ts create mode 100644 apps/web-e2e/src/fixtures/playback/fatal-media.m3u8 create mode 100644 apps/web-e2e/src/playback-recommendations.e2e.ts diff --git a/apps/web-e2e/eslint.config.mjs b/apps/web-e2e/eslint.config.mjs index b2e9fac09..2cff1f885 100644 --- a/apps/web-e2e/eslint.config.mjs +++ b/apps/web-e2e/eslint.config.mjs @@ -2,6 +2,9 @@ import playwright from 'eslint-plugin-playwright'; import baseConfig from '../../eslint.config.mjs'; export default [ + { + ignores: ['**/fixtures/playback/corrupt.ts'], + }, playwright.configs['flat/recommended'], ...baseConfig, { diff --git a/apps/web-e2e/src/fixtures/playback/corrupt.ts b/apps/web-e2e/src/fixtures/playback/corrupt.ts new file mode 100644 index 000000000..5a480f848 --- /dev/null +++ b/apps/web-e2e/src/fixtures/playback/corrupt.ts @@ -0,0 +1 @@ +This is intentionally not an MPEG transport stream segment. diff --git a/apps/web-e2e/src/fixtures/playback/fatal-media.m3u8 b/apps/web-e2e/src/fixtures/playback/fatal-media.m3u8 new file mode 100644 index 000000000..697d64b1d --- /dev/null +++ b/apps/web-e2e/src/fixtures/playback/fatal-media.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:1 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:1, +corrupt.ts +#EXT-X-ENDLIST diff --git a/apps/web-e2e/src/playback-recommendations.e2e.ts b/apps/web-e2e/src/playback-recommendations.e2e.ts new file mode 100644 index 000000000..dcd73cbc1 --- /dev/null +++ b/apps/web-e2e/src/playback-recommendations.e2e.ts @@ -0,0 +1,141 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import type { Page } from '@playwright/test'; +import { expect, test } from './fixtures'; + +const FIXTURE_DIR = join(__dirname, 'fixtures/playback'); +const FIXTURE_HOST = 'https://playback-fixture.local'; +const FATAL_HLS_PLAYLIST = [ + '#EXTM3U', + '#EXTINF:-1 tvg-id="fatal-hls" group-title="Playback",Fatal HLS', + `${FIXTURE_HOST}/fatal-media.m3u8`, +].join('\n'); + +const PLAYBACK_FIXTURES = new Map([ + [ + '/fatal-media.m3u8', + { + body: readFileSync(join(FIXTURE_DIR, 'fatal-media.m3u8')), + contentType: 'application/vnd.apple.mpegurl', + }, + ], + [ + '/corrupt.ts', + { + body: readFileSync(join(FIXTURE_DIR, 'corrupt.ts')), + contentType: 'video/mp2t', + }, + ], +]); + +test.use({ + launchOptions: { + args: ['--autoplay-policy=no-user-gesture-required'], + }, + serviceWorkers: 'block', +}); + +test.skip( + ({ browserName }) => browserName !== 'chromium', + 'HLS recovery coverage targets Chromium' +); + +async function servePlaybackFixtures(page: Page): Promise { + await page.route(`${FIXTURE_HOST}/**`, async (route) => { + const fixture = PLAYBACK_FIXTURES.get( + new URL(route.request().url()).pathname + ); + if (!fixture) { + await route.fulfill({ + status: 404, + contentType: 'text/plain', + body: 'not found', + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: fixture.contentType, + body: fixture.body, + }); + }); +} + +async function openSettings(page: Page): Promise { + await page.getByRole('link', { name: 'Open settings' }).click(); + await page.waitForURL(/\/workspace\/settings$/); + await expect( + page.getByRole('button', { name: 'Back', exact: true }) + ).toBeVisible(); +} + +async function selectAndSaveHtml5(page: Page): Promise { + const playerSelect = page.locator('[data-test-id="select-video-player"]'); + await playerSelect.click(); + await page + .getByRole('option', { name: 'HTML5 video player', exact: true }) + .click(); + + const saveButton = page.getByRole('button', { name: 'Save changes' }); + await saveButton.click(); + await expect(saveButton).toBeDisabled(); +} + +async function importFatalHlsPlaylist(page: Page): Promise { + await page.getByRole('button', { name: 'Add playlist' }).click(); + const dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible(); + await dialog.getByRole('radio', { name: /Raw m3u text/i }).click(); + await dialog + .getByLabel('Insert m3u(8) playlist as text') + .fill(FATAL_HLS_PLAYLIST); + await Promise.all([ + page.waitForURL(/\/workspace\/playlists\/.+\/all$/), + dialog.getByRole('button', { name: 'Import', exact: true }).click(), + ]); + await expect(page.getByText('1 channel')).toBeVisible(); +} + +test('@web @m3u @playback temporarily switches to the recommended player', async ({ + page, +}) => { + await servePlaybackFixtures(page); + await page.goto('/'); + + await openSettings(page); + await selectAndSaveHtml5(page); + await page.getByRole('button', { name: 'Back', exact: true }).click(); + await page.waitForURL(/\/workspace\/dashboard$/); + + await importFatalHlsPlaylist(page); + await page.getByText('1. Fatal HLS', { exact: true }).click(); + + const banner = page.locator('[data-test-id="playback-diagnostic-banner"]'); + const videoJsRecommendation = page.locator( + '[data-test-id="playback-recommendation-videojs"]' + ); + await expect(banner).toBeVisible({ timeout: 15_000 }); + await expect( + banner.locator('[data-test-id="playback-diagnostic-details"]') + ).toContainText( + /HLS\.js[\s\S]*type=mediaError[\s\S]*details=fragParsingError[\s\S]*disposition=fatal/ + ); + await expect(videoJsRecommendation).toBeVisible(); + await expect(videoJsRecommendation).toHaveClass( + /web-player-diagnostic__player-card--primary/ + ); + + await videoJsRecommendation.click(); + await expect(page.locator('app-vjs-player')).toBeVisible(); + + await openSettings(page); + await page.reload(); + await expect(page).toHaveURL(/\/workspace\/settings$/); + const persistedPlayerSelect = page.locator( + '[data-test-id="select-video-player"]' + ); + await expect(persistedPlayerSelect).toBeVisible(); + await expect(persistedPlayerSelect).toBeEnabled(); + await expect(persistedPlayerSelect).toHaveText('HTML5 video player'); +}); diff --git a/apps/web-e2e/tsconfig.json b/apps/web-e2e/tsconfig.json index dcc6815b6..d35c2908f 100644 --- a/apps/web-e2e/tsconfig.json +++ b/apps/web-e2e/tsconfig.json @@ -17,5 +17,6 @@ "playwright.config.ts", "src/**/*.d.ts", "src/**/*.e2e.ts" - ] + ], + "exclude": ["src/fixtures/playback/corrupt.ts"] }