import { Page } from '@playwright/test'; import { buildM3uContent, channelItemByTitle, closeElectronApp, createMutableTextServer, expect, importM3uPlaylistFromUrl, launchElectronApp, openWorkspaceSection, openSettings, test, } from './electron-test-fixtures'; const fallbackLogoSvg = Buffer.from( `GN` ).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; }); }