Files
iptvnator/apps/electron-backend-e2e/src/xtream-responsiveness.e2e.ts
T

321 lines
10 KiB
TypeScript

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<typeof addXtreamPortal>[0]) {
await page.getByRole('link', { name: 'Sources', exact: true }).click();
await page.waitForURL(/\/workspace\/sources$/);
}
function xtreamBlockedStateTitle(page: Parameters<typeof addXtreamPortal>[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);
}
});
});