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

265 lines
8.7 KiB
TypeScript

import type { Page } from '@playwright/test';
import { applyTheme, expectTextContrast } from './theme-contrast';
import {
addStalkerPortal,
addXtreamPortal,
closeElectronApp,
defaultStalkerMacAddress,
defaultStalkerPortalName,
defaultXtreamPortalName,
expect,
expectPortalDebugSuccess,
goToDashboard,
launchElectronApp,
resetMockServers,
stalkerMockServer,
test,
waitForXtreamImportToFinish,
waitForStalkerCatalog,
waitForXtreamCatalog,
} from './electron-test-fixtures';
test.describe('Electron Provider Smoke Tests', () => {
test('@xtream @theme @electron keeps sync overlay text readable in both themes', async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['xtream']);
const app = await launchElectronApp(dataDir);
try {
// Hold the cache read so the real local-library overlay stays
// mounted while its theme and Material interaction states change.
await app.electronApp.evaluate(({ ipcMain }) => {
ipcMain.removeHandler('DB_GET_CATEGORIES');
ipcMain.handle(
'DB_GET_CATEGORIES',
() => new Promise(() => {
// Intentionally pending until this isolated app closes.
})
);
});
await addXtreamPortal(app.mainWindow);
const overlay = app.mainWindow.locator(
'app-workspace-shell-import-overlay'
);
await expect(overlay).toContainText('Loading the saved catalog');
const action = overlay.getByRole('button', { name: 'Stop sync' });
for (const theme of ['light', 'dark', 'light'] as const) {
await applyTheme(app.mainWindow, theme);
for (const selector of [
'h3',
'.workspace-loading-overlay__badge',
'.workspace-loading-overlay__phase',
'.workspace-loading-overlay__detail',
]) {
await expectTextContrast(overlay.locator(selector));
}
// Leave headroom for Material's translucent hover/focus layer.
await expectTextContrast(action, 6);
await action.hover();
await expectTextContrast(action);
await action.focus();
await expectTextContrast(action);
await app.mainWindow.screenshot({
path: test.info().outputPath(`sync-overlay-${theme}.png`),
});
}
} finally {
await closeElectronApp(app);
}
});
test('@xtream @electron loads Xtream content through the Electron IPC path', async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['xtream']);
const app = await launchElectronApp(dataDir);
try {
await addXtreamPortal(app.mainWindow);
await waitForXtreamCatalog(app.mainWindow);
await expectPortalDebugSuccess(app.mainWindow, 'xtream');
await goToDashboard(app.mainWindow);
await expectRecentSourceCard(
app.mainWindow,
defaultXtreamPortalName
);
} finally {
await closeElectronApp(app);
}
});
test('@stalker @electron loads Stalker content through the Electron IPC path', async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['stalker']);
const app = await launchElectronApp(dataDir);
try {
await addStalkerPortal(app.mainWindow, {
portalUrl: `${stalkerMockServer}/portal.php`,
});
await waitForStalkerCatalog(app.mainWindow);
await expectPortalDebugSuccess(app.mainWindow, 'stalker');
await goToDashboard(app.mainWindow);
await expectRecentSourceCard(
app.mainWindow,
defaultStalkerPortalName
);
} finally {
await closeElectronApp(app);
}
});
test('@stalker @electron delivers cmd to the portal decoded exactly once with query injection blocked', async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['stalker']);
const app = await launchElectronApp(dataDir);
try {
// Stored cmd with a pre-encoded token (%3A), a literal '+', and a
// query-injection attempt (&injected=1#frag).
const storedCmd =
'ffrt3 http://example.com/ch/123?token=a%3Ab+c&injected=1#frag';
const response = await app.mainWindow.evaluate(
async ({ url, macAddress, cmd }) =>
window.electron.stalkerRequest({
url,
macAddress,
params: { action: 'create_link', type: 'itv', cmd },
}),
{
url: `${stalkerMockServer}/portal.php`,
macAddress: defaultStalkerMacAddress,
cmd: storedCmd,
}
);
const js = (
response as {
js: { cmd_received: string; query_keys_received: string[] };
}
).js;
// The portal must see the stored cmd decoded exactly once —
// %3A → ':', '+' → space — the same view it gets from a real STB.
// The old encodeURIComponent transport double-encoded '%' and
// delivered the %3A/+ sequences still encoded.
expect(js.cmd_received).toBe(
'ffrt3 http://example.com/ch/123?token=a:b c&injected=1#frag'
);
// The '&'/'#' inside cmd stayed inside the cmd value instead of
// restructuring the portal query.
expect(js.query_keys_received).toEqual([
'JsHttpRequest',
'action',
'cmd',
'type',
]);
} finally {
await closeElectronApp(app);
}
});
test('@xtream @electron shows refresh overlay immediately from the dashboard Xtream source menu', async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['xtream']);
const app = await launchElectronApp(dataDir);
try {
await addXtreamPortal(app.mainWindow);
await waitForXtreamCatalog(app.mainWindow);
await expectPortalDebugSuccess(app.mainWindow, 'xtream');
await goToDashboard(app.mainWindow);
await refreshRecentXtreamSourceFromDashboard(
app.mainWindow,
defaultXtreamPortalName
);
await waitForXtreamImportToFinish(app.mainWindow);
} finally {
await closeElectronApp(app);
}
});
});
async function expectRecentSourceCard(
page: Page,
title: string
): Promise<void> {
await expect(page.getByTestId('dashboard-recent-sources-rail')).toBeVisible(
{
timeout: 20000,
}
);
await expect(
page
.getByTestId('dashboard-recent-sources-rail-card')
.filter({
hasText: title,
})
.first()
).toBeVisible({
timeout: 20000,
});
}
async function refreshRecentXtreamSourceFromDashboard(
page: Page,
title: string
): Promise<void> {
const sourceCard = page
.getByTestId('dashboard-recent-sources-rail-card')
.filter({
hasText: title,
})
.first();
await expect(sourceCard).toBeVisible({ timeout: 20000 });
await sourceCard.hover();
await sourceCard
.getByTestId('dashboard-recent-sources-rail-card-actions')
.click();
await page
.getByRole('menuitem', {
name: 'Refresh Xtream playlist from remote',
exact: true,
})
.click();
const dialog = page.locator('mat-dialog-container');
await expect(dialog).toBeVisible();
await dialog.getByRole('button', { name: 'Yes', exact: true }).click();
const refreshOverlay = page.locator('app-workspace-shell-import-overlay');
await expect(refreshOverlay).toBeVisible({ timeout: 5000 });
await expect(
refreshOverlay.getByRole('heading', {
name: 'Refreshing playlist',
exact: true,
})
).toBeVisible();
await expect(refreshOverlay).toContainText(/Local library/);
await expect(refreshOverlay).toContainText(
/Preserving your library data|Removing cached streams|Removing cached categories/
);
await page.waitForSelector('mat-dialog-container', { state: 'detached' });
}