mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
New post at /blog/epg-wrong-program-fix/: a table separating the three symptoms (an empty row, a channel showing another station's schedule, and every programme shifted by a fixed amount), how the tvg-id → tvg-name → name lookup chain produces the first two, the manual "Map EPG channel" flow, and the display-only EPG time offset with the sign to use. Seven FAQ entries. The three screenshots are mock-backed. The Xtream mock gains /demo/guide.xml, an XMLTV guide for its marketing live channels whose ids deliberately match no playlist tvg-id, which is what makes the manual mapping worth showing; the capture imports it through the settings, accepting the private-network confirmation a loopback source raises, then right-clicks a channel of the M3U fixture and searches the dialog. The new actions live in capture-navigation-epg-actions.ts, alongside the setup, portal and download modules, and borrow one entry point from each of its two neighbours instead of duplicating their navigation. Also fixes assertMockServerIdentity: it checked both expected categories against get_vod_categories, but "Urban Drama" is a series category, so the reuse path rejected every already-running mock and a capture could only run when the port happened to be free. Each category is now checked against its own endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
122 lines
4.3 KiB
TypeScript
122 lines
4.3 KiB
TypeScript
/**
|
|
* Setup actions for the EPG-mapping guide shots: importing the mock's XMLTV
|
|
* guide, opening a channel's context menu and staging the mapping dialog.
|
|
* They sit between the settings pages (`capture-navigation-setup-actions.ts`)
|
|
* and the channel lists (`capture-navigation-portal-actions.ts`), so they
|
|
* borrow one entry point from each rather than duplicating the navigation.
|
|
*/
|
|
|
|
import type { Page } from '@playwright/test';
|
|
|
|
import { DEMO_EPG_URL } from './capture-fixtures';
|
|
import { type CaptureAction, settleUi } from './capture-navigation-helpers';
|
|
import { openM3uGroups } from './capture-navigation-portal-actions';
|
|
import { openEpgSettingsSection } from './capture-navigation-setup-actions';
|
|
|
|
async function openSettingsEpgOffset(page: Page): Promise<void> {
|
|
await openEpgSettingsSection(page);
|
|
const offset = page.locator('[data-test-id="epg-offset-minutes"]');
|
|
|
|
await offset.waitFor({ state: 'visible', timeout: 10_000 });
|
|
// Staged in the form only; discarded before the next action.
|
|
await offset.fill('60');
|
|
await page.waitForTimeout(400);
|
|
}
|
|
|
|
/**
|
|
* Imports the mock's XMLTV guide as an EPG source and saves the settings, so
|
|
* the mapping dialog has channels to search. Idempotent: a saved row with the
|
|
* demo URL means the guide is already in the isolated database.
|
|
*/
|
|
async function loadDemoEpg(page: Page): Promise<void> {
|
|
await openEpgSettingsSection(page);
|
|
const section = page.locator('#epg');
|
|
const existing = await section
|
|
.locator('.epg-source-row input')
|
|
.evaluateAll((inputs) =>
|
|
inputs.map((input) => (input as HTMLInputElement).value)
|
|
);
|
|
|
|
if (existing.includes(DEMO_EPG_URL)) {
|
|
return;
|
|
}
|
|
|
|
await section
|
|
.getByRole('button', { name: /add epg source/i })
|
|
.click({ timeout: 10_000 });
|
|
const field = section.locator('.epg-source-row input').last();
|
|
await field.waitFor({ state: 'visible', timeout: 10_000 });
|
|
await field.fill(DEMO_EPG_URL, { timeout: 10_000 });
|
|
// The row's first icon button fetches the source right away.
|
|
await section
|
|
.locator('.epg-source-row')
|
|
.last()
|
|
.locator('button')
|
|
.first()
|
|
.click();
|
|
// A loopback source trips the app's private-network confirmation; the
|
|
// mock is the one private address this capture trusts.
|
|
const allow = page.getByRole('button', { name: /allow source/i });
|
|
|
|
if (
|
|
await allow
|
|
.waitFor({ state: 'visible', timeout: 5_000 })
|
|
.then(() => true)
|
|
.catch(() => false)
|
|
) {
|
|
await allow.click();
|
|
}
|
|
|
|
await page
|
|
.locator(
|
|
'.epg-progress-panel .import-item.status-complete, .epg-progress-panel .stat-badge'
|
|
)
|
|
.first()
|
|
.waitFor({ state: 'visible', timeout: 60_000 });
|
|
|
|
const save = page.locator('[data-test-id="save-settings"]').first();
|
|
|
|
if (await save.isEnabled().catch(() => false)) {
|
|
await save.click();
|
|
}
|
|
|
|
await settleUi(page);
|
|
}
|
|
|
|
/** Right-clicks the first channel of the M3U groups view and waits for its menu. */
|
|
async function openM3uChannelContextMenu(page: Page): Promise<void> {
|
|
await openM3uGroups(page);
|
|
const channel = page.locator('app-channel-list-item').first();
|
|
|
|
await channel.waitFor({ state: 'visible', timeout: 30_000 });
|
|
await channel.click({ button: 'right' });
|
|
await page
|
|
.getByRole('menuitem', { name: /map epg channel/i })
|
|
.waitFor({ state: 'visible', timeout: 10_000 });
|
|
await page.waitForTimeout(400);
|
|
}
|
|
|
|
async function openEpgMappingDialog(page: Page): Promise<void> {
|
|
await loadDemoEpg(page);
|
|
await openM3uChannelContextMenu(page);
|
|
await page.getByRole('menuitem', { name: /map epg channel/i }).click();
|
|
|
|
const dialog = page.locator('mat-dialog-container').last();
|
|
|
|
await dialog.waitFor({ state: 'visible', timeout: 15_000 });
|
|
await dialog.locator('input').first().fill('Aurora');
|
|
|
|
const result = dialog.locator('.epg-mapping-result').first();
|
|
|
|
await result.waitFor({ state: 'visible', timeout: 15_000 });
|
|
await result.click();
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
export const EPG_ACTIONS: Readonly<Record<string, CaptureAction>> = {
|
|
'open-settings-epg-offset': openSettingsEpgOffset,
|
|
'load-demo-epg': loadDemoEpg,
|
|
'open-m3u-channel-menu': openM3uChannelContextMenu,
|
|
'open-epg-mapping-dialog': openEpgMappingDialog,
|
|
};
|