mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
* 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
142 lines
4.6 KiB
TypeScript
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');
|
|
});
|