Files
iptvnator/apps/electron-backend-e2e/src/xtream-epg.e2e.ts
T
4grayand4gray ef900d0f2a [codex] Add scoped coverage reporting (#1024)
* add scoped coverage reporting

* fix coverage review feedback

---------

Co-authored-by: 4gray <fourgray@proton.me>
2026-06-06 17:41:40 +02:00

254 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
addXtreamPortal,
channelItemByTitle,
clickCategoryByNameExact,
closeElectronApp,
expect,
launchElectronApp,
openWorkspaceSection,
resetMockServers,
test,
waitForXtreamWorkspaceReady,
} from './electron-test-fixtures';
import {
fetchXtreamEpgFixture,
XtreamNormalizedEpgListing,
} from './portal-mock-fixtures';
const epgPortalName = 'Xtream EPG Fixture';
const epgCredentials = {
username: 'epg',
password: 'epg',
};
for (const timeZone of ['UTC', 'Europe/Berlin'] as const) {
test(`@epg @xtream @electron renders Xtream EPG previews and selected-channel schedule in ${timeZone}`, async ({
dataDir,
request,
}) => {
await resetMockServers(request, ['xtream']);
const fixture = await fetchXtreamEpgFixture(request, epgCredentials);
const currentProgram = fixture.shortEpg[0];
if (!currentProgram) {
throw new Error(
'Expected the Xtream EPG fixture to include a current program.'
);
}
const app = await launchElectronApp(dataDir, {
env: { TZ: timeZone },
});
try {
await addXtreamPortal(app.mainWindow, {
name: `${epgPortalName} ${timeZone}`,
username: epgCredentials.username,
password: epgCredentials.password,
});
await waitForXtreamWorkspaceReady(app.mainWindow);
await openWorkspaceSection(app.mainWindow, 'Live TV');
await clickCategoryByNameExact(
app.mainWindow,
fixture.categoryName
);
const channelRow = channelItemByTitle(
app.mainWindow,
fixture.stream.name ?? ''
).first();
await expect(channelRow).toBeVisible({ timeout: 20000 });
await expect
.poll(async () =>
(
(await channelRow
.locator('.epg-title')
.textContent()) ?? ''
).trim()
)
.toBe(currentProgram.title);
await expect
.poll(async () => {
return (
await channelRow.locator('.epg-time').allInnerTexts()
).map((value) => value.trim());
})
.toEqual([
formatTimeInZone(currentProgram.startTimestamp, timeZone),
formatTimeInZone(currentProgram.stopTimestamp, timeZone),
]);
await expect
.poll(async () => getProgressWidthPercent(channelRow))
.toBeGreaterThan(0);
await channelRow.click();
await expect(app.mainWindow.locator('app-epg-list')).toBeVisible({
timeout: 20000,
});
const currentDateKey = formatDateKeyInZone(Date.now(), timeZone);
const currentDayTitles = titlesForDate(
fixture.fullEpg,
currentDateKey,
timeZone
);
await expect
.poll(() => visibleProgramTitles(app.mainWindow))
.toEqual(currentDayTitles);
await expectCurrentProgram(
app.mainWindow,
currentProgram,
timeZone
);
const firstFutureDateKey = getFirstFutureDateKey(
fixture.fullEpg,
currentDateKey,
timeZone
);
expect(firstFutureDateKey).not.toBeNull();
const futureDayTitles = titlesForDate(
fixture.fullEpg,
firstFutureDateKey!,
timeZone
);
const nextDayClicks = dayDifference(
currentDateKey,
firstFutureDateKey!
);
for (let index = 0; index < nextDayClicks; index += 1) {
await app.mainWindow
.locator(
'app-live-epg-panel .next-day, app-epg-list .next-day'
)
.click();
}
await expect
.poll(() => visibleProgramTitles(app.mainWindow))
.toEqual(futureDayTitles);
} finally {
await closeElectronApp(app);
}
});
}
function formatTimeInZone(timestampSeconds: number, timeZone: string): string {
return new Intl.DateTimeFormat('en-US', {
timeZone,
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(new Date(timestampSeconds * 1000));
}
function formatDateKeyInZone(
timestampMilliseconds: number,
timeZone: string
): string {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(new Date(timestampMilliseconds));
const year = parts.find((part) => part.type === 'year')?.value;
const month = parts.find((part) => part.type === 'month')?.value;
const day = parts.find((part) => part.type === 'day')?.value;
if (!year || !month || !day) {
throw new Error(
`Failed to format a date key for timezone ${timeZone}.`
);
}
return `${year}-${month}-${day}`;
}
function titlesForDate(
listings: XtreamNormalizedEpgListing[],
dateKey: string,
timeZone: string
): string[] {
return listings
.filter(
(listing) =>
formatDateKeyInZone(listing.startTimestamp * 1000, timeZone) ===
dateKey
)
.map((listing) => listing.title);
}
function getFirstFutureDateKey(
listings: XtreamNormalizedEpgListing[],
currentDateKey: string,
timeZone: string
): string | null {
const uniqueDateKeys = Array.from(
new Set(
listings.map((listing) =>
formatDateKeyInZone(listing.startTimestamp * 1000, timeZone)
)
)
).sort();
return uniqueDateKeys.find((dateKey) => dateKey > currentDateKey) ?? null;
}
function dayDifference(fromDateKey: string, toDateKey: string): number {
const from = Date.parse(`${fromDateKey}T00:00:00.000Z`);
const to = Date.parse(`${toDateKey}T00:00:00.000Z`);
return Math.round((to - from) / (24 * 60 * 60 * 1000));
}
async function visibleProgramTitles(
page: Parameters<typeof channelItemByTitle>[0]
) {
return page
.locator('app-epg-list .program-title')
.allInnerTexts()
.then((titles) => titles.map((title) => title.trim()).filter(Boolean));
}
async function expectCurrentProgram(
page: Parameters<typeof channelItemByTitle>[0],
currentProgram: XtreamNormalizedEpgListing,
timeZone: string
): Promise<void> {
const startTime = formatTimeInZone(currentProgram.startTimestamp, timeZone);
const stopTime = formatTimeInZone(currentProgram.stopTimestamp, timeZone);
const currentProgramRow = page
.locator('app-epg-list .program-item.current-program')
.first();
if (await currentProgramRow.isVisible().catch(() => false)) {
await expect(currentProgramRow.locator('.program-title')).toHaveText(
currentProgram.title
);
await expect(currentProgramRow.locator('.time')).toHaveText(
`${startTime}–${stopTime}`
);
return;
}
const summary = page.locator('app-live-epg-panel').first();
await expect(summary.locator('.live-epg-panel__title')).toHaveText(
currentProgram.title
);
await expect(summary.locator('.live-epg-panel__time')).toHaveText(
`${startTime} - ${stopTime}`
);
}
async function getProgressWidthPercent(
row: ReturnType<typeof channelItemByTitle>
) {
const style = await row.locator('.epg-progress-fill').getAttribute('style');
const width = style?.match(/width:\s*([\d.]+)%/)?.[1] ?? '0';
return Number.parseFloat(width);
}