From f229b3b1f2e1497d95d7ef2768361ad73b1d9cd6 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 6 Apr 2026 12:56:23 +0200 Subject: [PATCH] test(e2e): enhance E2E tests for Xtream and Stalker portals, improve category selection logic, and streamline IndexedDB cleanup --- .../src/category-management.e2e.ts | 130 +++-- .../src/electron-test-fixtures.ts | 30 ++ .../src/playlist-switcher.e2e.ts | 503 ++++++++++++++++++ apps/electron-backend-e2e/src/search.e2e.ts | 30 ++ apps/web-e2e/src/settings.e2e.ts | 13 +- apps/web-e2e/src/stalker.e2e.ts | 18 +- apps/web-e2e/src/xtream.e2e.ts | 18 +- 7 files changed, 647 insertions(+), 95 deletions(-) create mode 100644 apps/electron-backend-e2e/src/playlist-switcher.e2e.ts diff --git a/apps/electron-backend-e2e/src/category-management.e2e.ts b/apps/electron-backend-e2e/src/category-management.e2e.ts index 63d72d9ac..c4b9c8a50 100644 --- a/apps/electron-backend-e2e/src/category-management.e2e.ts +++ b/apps/electron-backend-e2e/src/category-management.e2e.ts @@ -267,72 +267,98 @@ async function expectVisibleSidebarCategoryNames( page: Page, expectedNames: string[] ): Promise { - const categories = page.locator( - 'app-workspace-context-panel .category-item:visible' - ); - const actualNames: string[] = []; - const count = await categories.count(); + await expect + .poll(async () => { + const categories = page.locator( + 'app-workspace-context-panel .category-item:visible' + ); + const actualNames: string[] = []; + const count = await categories.count(); - for (let index = 0; index < count; index += 1) { - const categoryName = - ( - await categories - .nth(index) - .locator('.nav-item-label') - .textContent() - )?.trim() ?? ''; + for (let index = 0; index < count; index += 1) { + const categoryName = + ( + await categories + .nth(index) + .locator('.nav-item-label') + .textContent() + )?.trim() ?? ''; - if (categoryName) { - actualNames.push(categoryName); - } - } + if (categoryName) { + actualNames.push(categoryName); + } + } - expect(actualNames).toEqual(expectedNames); + return actualNames; + }) + .toEqual(expectedNames); } async function pickSidebarCategory( page: Page ): Promise<{ id: string; itemCount: number; name: string }> { - const categories = page.locator( - 'app-workspace-context-panel .category-item:visible' - ); - const count = await categories.count(); - const candidates: Array<{ id: string; itemCount: number; name: string }> = - []; + let preferredCandidate: + | { id: string; itemCount: number; name: string } + | null = null; - for (let index = 0; index < count; index += 1) { - const category = categories.nth(index); - const id = - (await category.getAttribute('data-category-id'))?.trim() ?? ''; - const name = - (await category.locator('.nav-item-label').textContent())?.trim() ?? - ''; - const countText = - (await category.locator('.item-count').textContent())?.trim() ?? ''; - const itemCount = Number.parseInt(countText, 10) || 0; + await expect + .poll(async () => { + const categories = page.locator( + 'app-workspace-context-panel .category-item:visible' + ); + const count = await categories.count(); + const candidates: Array<{ + id: string; + itemCount: number; + name: string; + }> = []; - if (id && name && itemCount > 0) { - candidates.push({ id, itemCount, name }); - } - } + for (let index = 0; index < count; index += 1) { + const category = categories.nth(index); + const id = + (await category.getAttribute('data-category-id'))?.trim() ?? + ''; + const name = + ( + await category.locator('.nav-item-label').textContent() + )?.trim() ?? ''; + const countText = + ( + await category.locator('.item-count').textContent() + )?.trim() ?? ''; + const itemCount = Number.parseInt(countText, 10) || 0; - const nameCounts = new Map(); - for (const candidate of candidates) { - nameCounts.set( - candidate.name, - (nameCounts.get(candidate.name) ?? 0) + 1 - ); - } + if (id && name && itemCount > 0) { + candidates.push({ id, itemCount, name }); + } + } - const preferredCandidate = - candidates.find((candidate) => nameCounts.get(candidate.name) === 1) ?? - candidates[0]; + if (candidates.length === 0) { + preferredCandidate = null; + return false; + } - if (!preferredCandidate) { - throw new Error('No visible Xtream category with content was found.'); - } + const nameCounts = new Map(); + for (const candidate of candidates) { + nameCounts.set( + candidate.name, + (nameCounts.get(candidate.name) ?? 0) + 1 + ); + } - return preferredCandidate; + preferredCandidate = + candidates.find( + (candidate) => nameCounts.get(candidate.name) === 1 + ) ?? candidates[0]; + + return preferredCandidate !== null; + }) + .toBe(true, { + message: 'No visible Xtream category with content was found.', + timeout: 15000, + }); + + return preferredCandidate!; } async function toggleManagedCategory( diff --git a/apps/electron-backend-e2e/src/electron-test-fixtures.ts b/apps/electron-backend-e2e/src/electron-test-fixtures.ts index 8353aef3d..b8eeb4892 100644 --- a/apps/electron-backend-e2e/src/electron-test-fixtures.ts +++ b/apps/electron-backend-e2e/src/electron-test-fixtures.ts @@ -681,6 +681,20 @@ export async function clickCategoryById( await expect(category.first()).toBeVisible({ timeout: 20000 }); await category.first().scrollIntoViewIfNeeded(); await category.first().click(); + await expect + .poll(async () => { + const pathname = new URL(page.url()).pathname; + const isSelected = + (await category.first().getAttribute('aria-current')) === + 'true'; + + return ( + isSelected || + pathname.endsWith(`/${categoryId}`) || + pathname.includes(`/${categoryId}/`) + ); + }) + .toBe(true); } export async function clickCategoryByNameExact( @@ -698,7 +712,23 @@ export async function clickCategoryByNameExact( await expect(category).toBeVisible(); await category.scrollIntoViewIfNeeded(); + const categoryId = + (await category.getAttribute('data-category-id'))?.trim() ?? ''; await category.click(); + await expect + .poll(async () => { + const pathname = new URL(page.url()).pathname; + const isSelected = + (await category.getAttribute('aria-current')) === 'true'; + + return ( + isSelected || + (categoryId.length > 0 && + (pathname.endsWith(`/${categoryId}`) || + pathname.includes(`/${categoryId}/`))) + ); + }) + .toBe(true); } export function sourceRowByTitle(page: Page, title: string): Locator { diff --git a/apps/electron-backend-e2e/src/playlist-switcher.e2e.ts b/apps/electron-backend-e2e/src/playlist-switcher.e2e.ts new file mode 100644 index 000000000..ca8cffcb7 --- /dev/null +++ b/apps/electron-backend-e2e/src/playlist-switcher.e2e.ts @@ -0,0 +1,503 @@ +import { Page } from '@playwright/test'; +import { + channelItemByTitle, + addStalkerPortal, + addXtreamPortal, + closeElectronApp, + defaultXtreamPassword, + defaultXtreamUsername, + expect, + importM3uPlaylistFromNativeDialog, + launchElectronApp, + openPlaylistFavorites, + openPlaylistRecent, + openSettings, + openSourceEditor, + openSources, + openWorkspaceSection, + resetMockServers, + restartElectronApp, + saveSettings, + saveSourceDialog, + sourceRowByTitle, + test, + updateSourceDialog, + waitForDbOperationEvent, + waitForM3uCatalog, + waitForStalkerCatalog, + waitForXtreamCatalog, + writeTemporaryM3uFile, + xtreamMockServer, +} from './electron-test-fixtures'; + +const xtreamStressUsername = 'stress'; +const xtreamStressPassword = 'stress'; +const dbWorkerBatchDelayMs = '20'; + +const localAFileName = 'switcher-local-a.m3u'; +const localADisplayName = 'switcher-local-a'; +const localBFileName = 'switcher-local-b.m3u'; +const localBDisplayName = 'switcher-local-b'; +const localAChannelName = 'Switcher Favorite A'; +const localBChannelName = 'Switcher Favorite B'; +const offlineLocalFileName = 'offline-local-source.m3u'; +const offlineLocalDisplayName = 'offline-local-source'; +const xtreamAName = 'Switcher Xtream A'; +const xtreamBName = 'Switcher Xtream B'; +const stalkerAName = 'Switcher Stalker A'; +const stalkerBName = 'Switcher Stalker B'; +const offlineXtreamName = 'Offline Cached Xtream'; +const importingXtreamName = 'Importing Xtream Portal'; + +test.describe('Electron Playlist Switcher', () => { + test.describe.configure({ mode: 'serial' }); + test.setTimeout(120000); + + test('preserves same-type sections and cross-type mappings when switching from the workspace header', async ({ + dataDir, + request, + }) => { + await resetMockServers(request, ['xtream', 'stalker']); + const localAPath = writeTemporaryM3uFile(dataDir, localAFileName, [ + { + groupTitle: 'News', + name: 'Switcher Local A', + url: 'https://streams.example.test/switcher-local-a.m3u8', + }, + ]); + const localBPath = writeTemporaryM3uFile(dataDir, localBFileName, [ + { + groupTitle: 'Sports', + name: 'Switcher Local B', + url: 'https://streams.example.test/switcher-local-b.m3u8', + }, + ]); + const app = await launchElectronApp(dataDir); + + try { + await importM3uPlaylistFromNativeDialog(app, localAPath); + await importM3uPlaylistFromNativeDialog(app, localBPath); + await addXtreamPortal(app.mainWindow, { + name: xtreamAName, + }); + await addXtreamPortal(app.mainWindow, { + name: xtreamBName, + }); + await addStalkerPortal(app.mainWindow, { + name: stalkerAName, + }); + await addStalkerPortal(app.mainWindow, { + name: stalkerBName, + }); + await openSources(app.mainWindow); + + await sourceRowByTitle(app.mainWindow, localADisplayName) + .first() + .click(); + await waitForM3uCatalog(app.mainWindow); + await openWorkspaceSection(app.mainWindow, 'Recently viewed'); + await expectPathname( + app.mainWindow, + /\/workspace\/playlists\/[^/]+\/recent$/ + ); + + await switchPlaylistFromHeader(app.mainWindow, localBDisplayName); + await expectPathname( + app.mainWindow, + /\/workspace\/playlists\/[^/]+\/recent$/ + ); + + await switchPlaylistFromHeader(app.mainWindow, xtreamAName); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/recent$/ + ); + + await openWorkspaceSection(app.mainWindow, 'Live TV'); + await waitForXtreamCatalog(app.mainWindow); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/live$/ + ); + + await switchPlaylistFromHeader(app.mainWindow, xtreamBName); + await waitForXtreamCatalog(app.mainWindow); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/live$/ + ); + + await openWorkspaceSection(app.mainWindow, 'Recently added'); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/recently-added$/ + ); + + await switchPlaylistFromHeader(app.mainWindow, stalkerAName); + await expectPathname( + app.mainWindow, + /\/workspace\/stalker\/[^/]+\/recent$/ + ); + + await openWorkspaceSection(app.mainWindow, 'Live TV'); + await waitForStalkerCatalog(app.mainWindow); + await expectPathname( + app.mainWindow, + /\/workspace\/stalker\/[^/]+\/itv$/ + ); + + await switchPlaylistFromHeader(app.mainWindow, stalkerBName); + await waitForStalkerCatalog(app.mainWindow); + await expectPathname( + app.mainWindow, + /\/workspace\/stalker\/[^/]+\/itv$/ + ); + + await switchPlaylistFromHeader(app.mainWindow, xtreamAName); + await waitForXtreamCatalog(app.mainWindow); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/live$/ + ); + + await switchPlaylistFromHeader(app.mainWindow, localADisplayName); + await waitForM3uCatalog(app.mainWindow); + await expectPathname( + app.mainWindow, + /\/workspace\/playlists\/[^/]+\/all$/ + ); + + await openWorkspaceSection(app.mainWindow, 'Favorites'); + await expectPathname( + app.mainWindow, + /\/workspace\/playlists\/[^/]+\/favorites$/ + ); + + await switchPlaylistFromHeader(app.mainWindow, stalkerBName); + await expectPathname( + app.mainWindow, + /\/workspace\/stalker\/[^/]+\/favorites$/ + ); + } finally { + await closeElectronApp(app); + } + }); + + test('refreshes M3U unified collections immediately when switching same-type playlists from the header', async ({ + dataDir, + }) => { + const localAPath = writeTemporaryM3uFile(dataDir, localAFileName, [ + { + groupTitle: 'News', + name: localAChannelName, + url: 'https://streams.example.test/switcher-favorite-a.m3u8', + }, + ]); + const localBPath = writeTemporaryM3uFile(dataDir, localBFileName, [ + { + groupTitle: 'Sports', + name: localBChannelName, + url: 'https://streams.example.test/switcher-favorite-b.m3u8', + }, + ]); + const app = await launchElectronApp(dataDir); + + try { + await importM3uPlaylistFromNativeDialog(app, localAPath); + await importM3uPlaylistFromNativeDialog(app, localBPath); + + await openSources(app.mainWindow); + await sourceRowByTitle(app.mainWindow, localADisplayName) + .first() + .click(); + await waitForM3uCatalog(app.mainWindow); + await toggleFavoriteForChannel(app.mainWindow, localAChannelName); + await channelItemByTitle(app.mainWindow, localAChannelName) + .first() + .click(); + + await openSources(app.mainWindow); + await sourceRowByTitle(app.mainWindow, localBDisplayName) + .first() + .click(); + await waitForM3uCatalog(app.mainWindow); + await toggleFavoriteForChannel(app.mainWindow, localBChannelName); + await channelItemByTitle(app.mainWindow, localBChannelName) + .first() + .click(); + + await openSources(app.mainWindow); + await sourceRowByTitle(app.mainWindow, localADisplayName) + .first() + .click(); + await waitForM3uCatalog(app.mainWindow); + + await openPlaylistFavorites(app.mainWindow); + await expect(channelItemByTitle(app.mainWindow, localAChannelName)).toHaveCount( + 1 + ); + await expect(channelItemByTitle(app.mainWindow, localBChannelName)).toHaveCount( + 0 + ); + + await switchPlaylistFromHeader(app.mainWindow, localBDisplayName); + await expectPathname( + app.mainWindow, + /\/workspace\/playlists\/[^/]+\/favorites$/ + ); + await expect(channelItemByTitle(app.mainWindow, localBChannelName)).toHaveCount( + 1 + ); + await expect(channelItemByTitle(app.mainWindow, localAChannelName)).toHaveCount( + 0 + ); + + await openPlaylistRecent(app.mainWindow); + await expect(channelItemByTitle(app.mainWindow, localBChannelName)).toHaveCount( + 1 + ); + await expect(channelItemByTitle(app.mainWindow, localAChannelName)).toHaveCount( + 0 + ); + + await switchPlaylistFromHeader(app.mainWindow, localADisplayName); + await expectPathname( + app.mainWindow, + /\/workspace\/playlists\/[^/]+\/recent$/ + ); + await expect(channelItemByTitle(app.mainWindow, localAChannelName)).toHaveCount( + 1 + ); + await expect(channelItemByTitle(app.mainWindow, localBChannelName)).toHaveCount( + 0 + ); + } finally { + await closeElectronApp(app); + } + }); + + test('switches away from importing Xtream playlists, returns before completion, and avoids redundant reimport after caching', async ({ + dataDir, + request, + }) => { + await resetMockServers(request, ['xtream']); + const localAPath = writeTemporaryM3uFile(dataDir, localAFileName, [ + { + groupTitle: 'News', + name: 'Import Switch Local', + url: 'https://streams.example.test/import-switch-local.m3u8', + }, + ]); + const app = await launchElectronApp(dataDir, { + env: { + IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: dbWorkerBatchDelayMs, + }, + }); + + try { + await importM3uPlaylistFromNativeDialog(app, localAPath); + await waitForM3uCatalog(app.mainWindow); + + await addXtreamPortal(app.mainWindow, { + name: importingXtreamName, + password: xtreamStressPassword, + username: xtreamStressUsername, + }); + + const overlay = app.mainWindow.locator('.workspace-loading-overlay'); + await expect(overlay).toBeVisible({ timeout: 15000 }); + await waitForDbOperationEvent(app.mainWindow, { + operation: 'save-content', + predicate: (event) => + event.status === 'progress' && + (event.current ?? 0) > 0 && + (event.total ?? 0) > 0, + timeoutMs: 30000, + }); + + await switchPlaylistFromHeader(app.mainWindow, localADisplayName); + await waitForM3uCatalog(app.mainWindow); + await expect(overlay).toHaveCount(0); + + await switchPlaylistFromHeader(app.mainWindow, importingXtreamName); + await expectPathname( + app.mainWindow, + /\/workspace\/xtreams\/[^/]+\/vod$/ + ); + await Promise.race([ + overlay.waitFor({ state: 'visible', timeout: 15000 }), + waitForXtreamCatalog(app.mainWindow), + ]); + + await waitForXtreamCatalog(app.mainWindow); + + await switchPlaylistFromHeader(app.mainWindow, localADisplayName); + await waitForM3uCatalog(app.mainWindow); + + const startedBeforeReturn = await countDbEvents( + app.mainWindow, + 'save-content', + 'started' + ); + + await switchPlaylistFromHeader(app.mainWindow, importingXtreamName); + await waitForXtreamCatalog(app.mainWindow); + await expect(overlay).toHaveCount(0); + + await app.mainWindow.waitForTimeout(1200); + + const startedAfterReturn = await countDbEvents( + app.mainWindow, + 'save-content', + 'started' + ); + expect(startedAfterReturn).toBe(startedBeforeReturn); + } finally { + await closeElectronApp(app); + } + }); + + test('keeps cached Xtream content browseable with an unavailable warning after the portal goes offline and after restart', async ({ + dataDir, + request, + }) => { + await resetMockServers(request, ['xtream']); + const offlineLocalPath = writeTemporaryM3uFile( + dataDir, + offlineLocalFileName, + [ + { + groupTitle: 'News', + name: 'Offline Local Channel', + url: 'https://streams.example.test/offline-local.m3u8', + }, + ] + ); + let app = await launchElectronApp(dataDir); + + try { + await openSettings(app.mainWindow); + await selectSettingsOption( + app.mainWindow, + 'select-startup-behavior', + 'restore-last-view' + ); + await saveSettings(app.mainWindow); + + await addXtreamPortal(app.mainWindow, { + name: offlineXtreamName, + password: defaultXtreamPassword, + username: defaultXtreamUsername, + }); + await openWorkspaceSection(app.mainWindow, 'Movies'); + await waitForXtreamCatalog(app.mainWindow); + + await importM3uPlaylistFromNativeDialog(app, offlineLocalPath); + await waitForM3uCatalog(app.mainWindow); + + await openSources(app.mainWindow); + const dialog = await openSourceEditor( + app.mainWindow, + offlineXtreamName + ); + await updateSourceDialog(dialog, { + serverUrl: xtreamMockServer.replace(/:\d+$/, ':65530'), + }); + await saveSourceDialog(app.mainWindow, dialog); + + await sourceRowByTitle(app.mainWindow, offlineLocalDisplayName) + .first() + .click(); + await waitForM3uCatalog(app.mainWindow); + + await switchPlaylistFromHeader(app.mainWindow, offlineXtreamName); + await waitForXtreamCatalog(app.mainWindow); + await expectXtreamUnavailableWarning(app.mainWindow); + await expect(app.mainWindow.locator('.xtream-content-gate')).toHaveCount( + 0 + ); + + app = await restartElectronApp(app, dataDir); + + await waitForXtreamCatalog(app.mainWindow); + await expectXtreamUnavailableWarning(app.mainWindow); + await expect(app.mainWindow.locator('.xtream-content-gate')).toHaveCount( + 0 + ); + } finally { + await closeElectronApp(app); + } + }); +}); + +async function openPlaylistSwitcher(page: Page): Promise { + await page + .locator('app-playlist-switcher .playlist-switcher-trigger') + .click(); + await expect( + page.locator('.cdk-overlay-pane .playlist-item').first() + ).toBeVisible(); +} + +async function switchPlaylistFromHeader( + page: Page, + title: string +): Promise { + await openPlaylistSwitcher(page); + const item = page + .locator('.cdk-overlay-pane .playlist-item') + .filter({ hasText: title }) + .first(); + + await expect(item).toBeVisible(); + await item.click(); + await expect(page.locator('.cdk-overlay-pane .playlist-item')).toHaveCount( + 0 + ); +} + +async function expectPathname(page: Page, pattern: RegExp): Promise { + await expect + .poll(() => new URL(page.url()).pathname, { timeout: 20000 }) + .toMatch(pattern); +} + +async function countDbEvents( + page: Page, + operation: string, + status: string +): Promise { + return page.evaluate( + ({ operation, status: desiredStatus }) => + (window.__dbOperationEvents ?? []).filter( + (event) => + event.operation === operation && + event.status === desiredStatus + ).length, + { operation, status } + ); +} + +async function selectSettingsOption( + page: Page, + selectTestId: string, + optionTestId: string +): Promise { + await page.getByTestId(selectTestId).click(); + await page.getByTestId(optionTestId).click(); +} + +async function expectXtreamUnavailableWarning(page: Page): Promise { + await expect( + page.getByTestId('xtream-offline-warning') + ).toBeVisible({ timeout: 20000 }); +} + +async function toggleFavoriteForChannel(page: Page, title: string): Promise { + const item = channelItemByTitle(page, title).first(); + + await expect(item).toBeVisible({ timeout: 20000 }); + await item.hover(); + await item.locator('.favorite-button').first().click(); + await expect(item.locator('.favorite-button mat-icon').first()).toHaveText(/star/); +} diff --git a/apps/electron-backend-e2e/src/search.e2e.ts b/apps/electron-backend-e2e/src/search.e2e.ts index 966399c6a..7f04cf703 100644 --- a/apps/electron-backend-e2e/src/search.e2e.ts +++ b/apps/electron-backend-e2e/src/search.e2e.ts @@ -1180,6 +1180,20 @@ async function clickCategoryById( await expect(category.first()).toBeVisible(); await category.first().click(); + await expect + .poll(async () => { + const pathname = new URL(page.url()).pathname; + const isSelected = + (await category.first().getAttribute('aria-current')) === + 'true'; + + return ( + isSelected || + pathname.endsWith(`/${categoryId}`) || + pathname.includes(`/${categoryId}/`) + ); + }) + .toBe(true); } async function clickCategoryByNameExact( @@ -1197,7 +1211,23 @@ async function clickCategoryByNameExact( await expect(category).toBeVisible(); await category.scrollIntoViewIfNeeded(); + const categoryId = + (await category.getAttribute('data-category-id'))?.trim() ?? ''; await category.click(); + await expect + .poll(async () => { + const pathname = new URL(page.url()).pathname; + const isSelected = + (await category.getAttribute('aria-current')) === 'true'; + + return ( + isSelected || + (categoryId.length > 0 && + (pathname.endsWith(`/${categoryId}`) || + pathname.includes(`/${categoryId}/`))) + ); + }) + .toBe(true); } async function openWorkspaceSection(page: Page, label: string): Promise { diff --git a/apps/web-e2e/src/settings.e2e.ts b/apps/web-e2e/src/settings.e2e.ts index 1e864e4f6..3b18bc6e1 100644 --- a/apps/web-e2e/src/settings.e2e.ts +++ b/apps/web-e2e/src/settings.e2e.ts @@ -17,18 +17,9 @@ async function saveSettings(page: Page) { test.describe('Settings', () => { test.beforeEach(async ({ page }) => { + // Playwright creates a fresh browser context per test, so extra + // IndexedDB cleanup here only risks racing with app-managed DB handles. await page.goto('/'); - // Clear IndexedDB before each test - await page.evaluate(async () => { - const dbNames = (await window.indexedDB.databases()).map( - (db) => db.name - ); - dbNames.forEach((name) => - name !== undefined - ? window.indexedDB.deleteDatabase(name) - : null - ); - }); }); test('Check settings page', async ({ page }) => { diff --git a/apps/web-e2e/src/stalker.e2e.ts b/apps/web-e2e/src/stalker.e2e.ts index 39e2b90d4..af4a5068a 100644 --- a/apps/web-e2e/src/stalker.e2e.ts +++ b/apps/web-e2e/src/stalker.e2e.ts @@ -90,20 +90,6 @@ async function addStalkerPortal( await page.waitForURL(/stalker.*vod/); } -/** - * Clear IndexedDB state between tests so each test starts fresh. - */ -async function clearStorage(page: Page): Promise { - await page.evaluate(async () => { - const dbs = await window.indexedDB.databases(); - await Promise.all( - dbs - .filter((db) => db.name != null) - .map((db) => window.indexedDB.deleteDatabase(db.name!)) - ); - }); -} - // --------------------------------------------------------------------------- // Test setup // --------------------------------------------------------------------------- @@ -112,9 +98,9 @@ test.beforeEach(async ({ page, request }) => { // Reset mock server state (clears in-memory favorites and cache) await request.post(`${MOCK_SERVER}/reset`); + // Playwright creates a fresh browser context per test, so extra + // IndexedDB cleanup here only risks racing with app-managed DB handles. await page.goto('/'); - await clearStorage(page); - await page.reload(); // Redirect backend proxy calls to the mock server await interceptStalkerRequests(page); diff --git a/apps/web-e2e/src/xtream.e2e.ts b/apps/web-e2e/src/xtream.e2e.ts index 6db10027f..e36ba9d86 100644 --- a/apps/web-e2e/src/xtream.e2e.ts +++ b/apps/web-e2e/src/xtream.e2e.ts @@ -70,20 +70,6 @@ async function addXtreamPortal( await page.waitForURL(/xtreams.*vod/); } -/** - * Clear IndexedDB state between tests so each test starts fresh. - */ -async function clearStorage(page: Page): Promise { - await page.evaluate(async () => { - const dbs = await window.indexedDB.databases(); - await Promise.all( - dbs - .filter((db) => db.name != null) - .map((db) => window.indexedDB.deleteDatabase(db.name!)) - ); - }); -} - // --------------------------------------------------------------------------- // Test setup // --------------------------------------------------------------------------- @@ -91,9 +77,9 @@ async function clearStorage(page: Page): Promise { test.beforeEach(async ({ page, request }) => { await request.post(`${MOCK_SERVER}/reset`); + // Playwright creates a fresh browser context per test, so extra + // IndexedDB cleanup here only risks racing with app-managed DB handles. await page.goto('/'); - await clearStorage(page); - await page.reload(); await interceptXtreamRequests(page); });