import { Page } from '@playwright/test';
import {
buildM3uContent,
channelItemByTitle,
closeElectronApp,
createMutableTextServer,
expect,
importM3uPlaylistFromUrl,
launchElectronApp,
openWorkspaceSection,
openSettings,
test,
} from './electron-test-fixtures';
const fallbackLogoSvg = Buffer.from(
``
).toString('base64');
const fallbackLogoDataUrl = `data:image/svg+xml;base64,${fallbackLogoSvg}`;
const epgFixtureXml = `
Guide News
Guide Bulletin
EPG refresh smoke test.
`;
test.describe('Electron EPG', () => {
test('adds an EPG source, fetches guide data, removes the source row, and clears stored EPG data', async ({
dataDir,
}) => {
const epgServer = await createMutableTextServer(epgFixtureXml, {
contentType: 'application/xml; charset=utf-8',
resourcePath: '/guide.xml',
});
const app = await launchElectronApp(dataDir);
try {
await openSettings(app.mainWindow);
await app.mainWindow
.getByRole('button', { name: 'Add EPG source' })
.click();
await app.mainWindow
.locator('.epg-source-row input')
.first()
.fill(epgServer.resourceUrl);
await app.mainWindow.locator('.epg-source-row button').first().click();
await expect(
app.mainWindow.locator('.epg-progress-panel')
).toBeVisible();
await expect
.poll(() => getEpgChannelCount(app.mainWindow), {
timeout: 30000,
})
.toBeGreaterThan(0);
await expect(
app.mainWindow.locator('.epg-progress-panel .stat-badge').first()
).toBeVisible();
await app.mainWindow.locator('.epg-source-row button').nth(1).click();
await expect(app.mainWindow.locator('.epg-source-row')).toHaveCount(0);
await app.mainWindow
.getByRole('button', { name: 'Clear EPG data' })
.click();
const dialog = app.mainWindow.locator('mat-dialog-container');
await expect(dialog).toBeVisible();
await dialog.getByRole('button', { name: 'Yes', exact: true }).click();
await app.mainWindow.waitForSelector('mat-dialog-container', {
state: 'detached',
});
await expect
.poll(() => getEpgChannelCount(app.mainWindow), {
timeout: 20000,
})
.toBe(0);
} finally {
await closeElectronApp(app);
await epgServer.close();
}
});
test('uses the XMLTV channel icon as a fallback when the playlist has no tvg-logo', async ({
dataDir,
}) => {
const playlistServer = await createMutableTextServer(
buildM3uContent([
{
name: 'Guide News Live',
tvgId: 'guide-news',
url: 'https://example.com/live/guide-news.m3u8',
},
]),
{
contentType: 'application/x-mpegurl; charset=utf-8',
resourcePath: '/playlist.m3u',
}
);
const epgServer = await createMutableTextServer(
`
Guide News Live
Guide Bulletin
EPG logo fallback test.
`,
{
contentType: 'application/xml; charset=utf-8',
resourcePath: '/guide.xml',
}
);
const app = await launchElectronApp(dataDir);
try {
await importM3uPlaylistFromUrl(
app.mainWindow,
playlistServer.resourceUrl
);
await openSettings(app.mainWindow);
await app.mainWindow
.getByRole('button', { name: 'Add EPG source' })
.click();
await app.mainWindow
.locator('.epg-source-row input')
.first()
.fill(epgServer.resourceUrl);
await app.mainWindow.locator('.epg-source-row button').first().click();
await expect
.poll(() => getEpgChannelCount(app.mainWindow), {
timeout: 30000,
})
.toBeGreaterThan(0);
await openWorkspaceSection(app.mainWindow, 'All channels');
const channelItem = channelItemByTitle(
app.mainWindow,
'Guide News Live'
);
await expect(channelItem).toBeVisible();
await expect(channelItem.locator('.channel-logo')).toHaveAttribute(
'src',
fallbackLogoDataUrl
);
await expect(
channelItem.locator('.channel-logo-fallback')
).toHaveCount(0);
} finally {
await closeElectronApp(app);
await playlistServer.close();
await epgServer.close();
}
});
});
async function getEpgChannelCount(page: Page): Promise {
return page.evaluate(async () => {
const channels = await window.electron?.getEpgChannelsByRange?.(0, 20);
return Array.isArray(channels) ? channels.length : 0;
});
}