Files
iptvnator/apps/web-e2e/src/playback-recommendations.e2e.ts
T
4gray fd96b85c19 feat(playback): recommend recovery actions (#1374)
* docs(playback): design recovery recommendations

* docs(playback): plan recovery recommendations

* refactor(playback): extract diagnostic utilities

* feat(playback): define recovery recommendation contracts

* feat(playback): rank recovery recommendations

* feat(playback): track session recovery attempts

* feat(playback): identify content recovery sessions

* feat(ui): add ranked playback diagnostic panel

* feat(playback): switch temporarily to recommended players

* test(playback): cover temporary player recommendation

* test(playback): verify recommendation capability guards

* docs(playback): document recovery recommendations

* fix(playback): keep recovery keys credential-free

* fix(playback): remove derived tracking ownership

* fix(playback): preserve distinct recovery fallbacks

* fix(playback): reset resume for new sources

* fix(playback): preserve desktop recovery guidance

* docs(playback): clarify recovery policy exceptions

* fix(playback): reject stale progress updates

* fix(playback): keep protected recovery guidance neutral

* test(playback): cover stale progress output

* fix(playback): neutralize protected diagnostic copy

* fix(playback): harden runtime guidance ownership

* fix(playback): stabilize recovery application ownership

* fix(ci): classify playback util coverage

* fix(e2e): preserve playback fixture bytes
2026-08-08 01:04:39 +02:00

142 lines
4.6 KiB
TypeScript

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<void> {
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<void> {
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<void> {
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<void> {
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');
});