import { addXtreamPortal, closeElectronApp, expect, expectPortalDebugSuccess, expectRendererFramesAdvance, launchElectronApp, resetMockServers, test, waitForDbOperationEvent, waitForXtreamCatalog, xtreamMockServer, } from './electron-test-fixtures'; const stressPortalName = 'Stress Xtream Portal'; const stressXtreamUsername = 'stress'; const stressXtreamPassword = 'stress'; const dbWorkerBatchDelayMs = '20'; function playlistRowByName(name: string) { return `app-playlist-item:has-text("${name}")`; } async function openSources(page: Parameters[0]) { await page.getByRole('link', { name: 'Sources', exact: true }).click(); await page.waitForURL(/\/workspace\/sources$/); } function xtreamBlockedStateTitle(page: Parameters[0]) { return page.locator('.xtream-content-gate .error-view__title'); } test.describe('Electron Xtream Responsiveness', () => { test.describe.configure({ mode: 'serial' }); test.setTimeout(120000); test('keeps the UI responsive during large Xtream imports', async ({ dataDir, request, }) => { await resetMockServers(request, ['xtream']); const app = await launchElectronApp(dataDir, { env: { IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: dbWorkerBatchDelayMs, }, }); try { await addXtreamPortal(app.mainWindow, { name: stressPortalName, password: stressXtreamPassword, username: stressXtreamUsername, }); const overlay = app.mainWindow.locator( '.workspace-loading-overlay' ); await expect(overlay).toBeVisible({ timeout: 15000 }); await expect( overlay.getByRole('button', { name: /stop sync|cancel import/i, }) ).toBeVisible({ timeout: 20000 }); await waitForDbOperationEvent(app.mainWindow, { operation: 'save-content', predicate: (event) => event.status === 'progress' && (event.current ?? 0) > 0 && (event.total ?? 0) > 0, timeoutMs: 30000, }); await expect .poll( async () => (await app.mainWindow.evaluate( () => (window.__dbOperationEvents ?? []).filter( (event) => event.operation === 'save-content' && event.status === 'progress' ).length )) > 2, { timeout: 30000 } ) .toBeTruthy(); await expectRendererFramesAdvance(app.mainWindow, { minimumDelta: 5, sampleMs: 400, }); await expect .poll( async () => (await overlay.locator('p').allInnerTexts()).some( (text) => /(Live TV|Movies|Series).*?\d+\s*\/\s*\d+/.test( text ) ), { timeout: 30000 } ) .toBeTruthy(); await waitForXtreamCatalog(app.mainWindow); await expectPortalDebugSuccess(app.mainWindow, 'xtream'); } finally { await closeElectronApp(app); } }); test('stops importing after cancel until the user explicitly retries', async ({ dataDir, request, }) => { await resetMockServers(request, ['xtream']); const app = await launchElectronApp(dataDir, { env: { IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: dbWorkerBatchDelayMs, }, }); try { await addXtreamPortal(app.mainWindow, { name: stressPortalName, password: stressXtreamPassword, username: stressXtreamUsername, }); const overlay = app.mainWindow.locator( '.workspace-loading-overlay' ); const cancelButton = overlay.getByRole('button', { name: /stop sync|cancel import/i, }); await expect(overlay).toBeVisible({ timeout: 15000 }); await expect(cancelButton).toBeVisible({ timeout: 20000 }); await waitForDbOperationEvent(app.mainWindow, { operation: 'save-content', predicate: (event) => event.status === 'progress' && (event.current ?? 0) > 0 && (event.total ?? 0) > 0, timeoutMs: 30000, }); await cancelButton.click(); await expect(overlay).toHaveCount(0, { timeout: 20000 }); await expect(xtreamBlockedStateTitle(app.mainWindow)).toHaveText( 'Import cancelled', { timeout: 20000 } ); await app.mainWindow.waitForTimeout(1200); await expect(overlay).toHaveCount(0); const retryButton = app.mainWindow.getByRole('button', { name: 'Retry', exact: true, }); await retryButton.click(); await waitForXtreamCatalog(app.mainWindow); } finally { await closeElectronApp(app); } }); test('blocks expired and inactive Xtream accounts before import starts', async ({ dataDir, request, }) => { await resetMockServers(request, ['xtream']); const app = await launchElectronApp(dataDir); try { await addXtreamPortal(app.mainWindow, { name: 'Expired Xtream Portal', password: 'expired', username: 'expired', }); await expect(xtreamBlockedStateTitle(app.mainWindow)).toHaveText( 'Account expired', { timeout: 20000 } ); await expect( app.mainWindow.locator('.workspace-loading-overlay') ).toHaveCount(0); await openSources(app.mainWindow); await addXtreamPortal(app.mainWindow, { name: 'Inactive Xtream Portal', password: 'inactive', username: 'inactive', }); await expect(xtreamBlockedStateTitle(app.mainWindow)).toHaveText( 'Account inactive', { timeout: 20000 } ); await expect( app.mainWindow.locator('.workspace-loading-overlay') ).toHaveCount(0); } finally { await closeElectronApp(app); } }); test('blocks unreachable Xtream portals immediately without an import loop', async ({ dataDir, request, }) => { await resetMockServers(request, ['xtream']); const app = await launchElectronApp(dataDir); try { await addXtreamPortal(app.mainWindow, { name: 'Unavailable Xtream Portal', password: stressXtreamPassword, serverUrl: xtreamMockServer.replace(/:\d+$/, ':65530'), username: stressXtreamUsername, }); await expect(xtreamBlockedStateTitle(app.mainWindow)).toHaveText( 'Portal unavailable', { timeout: 20000 } ); await expect( app.mainWindow.locator('.workspace-loading-overlay') ).toHaveCount(0); await app.mainWindow.waitForTimeout(1200); await expect( app.mainWindow.locator('.workspace-loading-overlay') ).toHaveCount(0); } finally { await closeElectronApp(app); } }); test('keeps the UI responsive during large Xtream playlist deletion', async ({ dataDir, request, }) => { await resetMockServers(request, ['xtream']); const app = await launchElectronApp(dataDir, { env: { IPTVNATOR_DB_WORKER_BATCH_DELAY_MS: dbWorkerBatchDelayMs, }, }); try { await addXtreamPortal(app.mainWindow, { name: stressPortalName, password: stressXtreamPassword, username: stressXtreamUsername, }); await waitForXtreamCatalog(app.mainWindow); await openSources(app.mainWindow); const playlistRow = app.mainWindow.locator( playlistRowByName(stressPortalName) ); await expect(playlistRow).toBeVisible({ timeout: 20000 }); await playlistRow.locator('.delete-btn').click(); const confirmDialog = app.mainWindow.locator( 'mat-dialog-container' ); await expect(confirmDialog).toBeVisible(); await confirmDialog .getByRole('button', { name: 'Yes', exact: true }) .click(); await waitForDbOperationEvent(app.mainWindow, { operation: 'delete-playlist', status: 'started', timeoutMs: 20000, }); await expect( playlistRow.locator('.busy-state__message') ).toBeVisible({ timeout: 20000 }); await expect(playlistRow.locator('.cancel-btn')).toBeVisible(); await waitForDbOperationEvent(app.mainWindow, { operation: 'delete-playlist', predicate: (event) => event.status === 'progress' && (event.current ?? 0) > 0 && (event.total ?? 0) > 0, timeoutMs: 30000, }); await expectRendererFramesAdvance(app.mainWindow, { minimumDelta: 5, sampleMs: 400, }); await expect(playlistRow).toHaveCount(0, { timeout: 60000 }); await expect( app.mainWindow.locator(playlistRowByName(stressPortalName)) ).toHaveCount(0); } finally { await closeElectronApp(app); } }); });