const { test, expect } = require('@playwright/test'); const { TOKEN } = require('./global-setup'); // The token sets a cookie, so every test starts by presenting it once. test.beforeEach(async ({ page }) => { await page.goto(`/?token=${TOKEN}`); await expect(page.locator('#feedlist')).toBeVisible(); }); test('the page loads and lists the configured feeds', async ({ page }) => { // Regression: a ReferenceError in the script left the shell rendered and the sidebar // empty, with every handler below the error dead. Server-side checks all passed. // Four top-level feeds in the fixture config; the OPML's children are inside a closed folder. await expect(page.locator('.feed')).toHaveCount(5, { timeout: 15_000 }); await expect(page.locator('.feed', { hasText: 'Test Show' })).toBeVisible(); const errors = []; page.on('pageerror', e => errors.push(e.message)); await page.reload(); await expect(page.locator('.feed').first()).toBeVisible(); expect(errors, 'the page script must not throw at load').toEqual([]); }); test('Settings picks a theme and, where it has both, light, dark or Auto', async ({ page }) => { const root = () => page.evaluate(() => [document.documentElement.dataset.theme, document.documentElement.dataset.mode]); const bg = () => page.evaluate(() => getComputedStyle(document.body).backgroundColor); await page.locator('#prefs').click(); await expect(page.locator('#stheme')).toHaveValue((await root())[0]); await page.locator('#stheme').selectOption('modern'); await page.locator('#smode').selectOption('auto'); // Auto follows the system, live, with no reload. await page.emulateMedia({ colorScheme: 'light' }); await expect.poll(root).toEqual(['modern', 'light']); await expect.poll(bg).toBe('rgb(242, 244, 247)'); // Modern's light --bg await page.emulateMedia({ colorScheme: 'dark' }); await expect.poll(root).toEqual(['modern', 'dark']); await expect.poll(bg).toBe('rgb(14, 19, 27)'); // Modern's dark --bg // Dracula, then its light half, Alucard. await page.locator('#stheme').selectOption('dracula'); await page.locator('#smode').selectOption('dark'); await expect.poll(bg).toBe('rgb(40, 42, 54)'); // #282A36 await page.locator('#smode').selectOption('light'); await expect.poll(bg).toBe('rgb(255, 251, 235)'); // #FFFBEB // Classic and Paper come one way only, so there is nothing to choose. await page.locator('#stheme').selectOption('paper'); await expect(page.locator('#smode')).toBeHidden(); await expect.poll(bg).toBe('rgb(242, 238, 222)'); // #F2EEDE await page.locator('#stheme').selectOption('classic'); await expect(page.locator('#smode')).toBeHidden(); await page.reload(); await expect.poll(root).toEqual(['classic', 'light']); // The 2004 Mac app set its type in Lucida Grande. expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toContain('Lucida Grande'); expect(await page.locator('#theme').count(), 'the theme lives in Settings only').toBe(0); // Back to Nordic, dark: the mode chosen before Classic is kept for the themes that have one. await page.locator('#prefs').click(); await page.locator('#stheme').selectOption('nordic'); await expect(page.locator('#smode')).toHaveValue('light'); await page.locator('#smode').selectOption('dark'); await expect.poll(bg).toBe('rgb(46, 52, 64)'); // nord0 }); test('a theme saved before there was light and dark carries over', async ({ page }) => { await page.evaluate(() => { localStorage.setItem('ipx.theme', 'light'); localStorage.removeItem('ipx.mode'); }); await page.reload(); expect(await page.evaluate(() => [document.documentElement.dataset.theme, document.documentElement.dataset.mode])) .toEqual(['modern', 'light']); }); test('settings opens and saves the global schedule', async ({ page }) => { await page.locator('#prefs').click(); await expect(page.locator('#modal.on')).toBeVisible(); await expect(page.locator('#gnum')).toBeVisible(); await page.locator('#gnum').fill('4'); await page.locator('#gunit').selectOption('h'); await page.locator('#gsave').click(); await expect(page.locator('#modal.on')).toBeHidden(); // It must survive a reload, i.e. actually reach the config. await page.locator('#prefs').click(); await expect(page.locator('#gnum')).toHaveValue('4'); await expect(page.locator('#gunit')).toHaveValue('h'); }); test('episodes show with their metadata, and the text opens below', async ({ page }) => { await page.locator('.feed', { hasText: 'Test Show' }).click(); await expect(page.locator('.ep').first()).toBeVisible({ timeout: 20_000 }); await expect(page.getByText('First Episode')).toBeVisible(); // Newest first, so target the episode by name rather than by position. const first = page.locator('.ep', { hasText: 'First Episode' }); await expect(first).toContainText('S1E1'); await expect(first).toContainText('30:30'); // itunes:duration 1830 await expect(page.locator('.ep', { hasText: 'Second Episode' })).toContainText('15:00'); // Selecting an item shows its text in the pane below, not inline in the row. await first.click(); await expect(first).toHaveClass(/sel/); await expect(page.locator('#detail')).toContainText('Show notes for the first one'); await expect(page.locator('#detail .dt')).toHaveText('First Episode'); }); test('the three panes are there and the item text lands in the bottom one', async ({ page }) => { await page.locator('.feed', { hasText: 'Test Show' }).click(); await expect(page.locator('#list')).toBeVisible(); await expect(page.locator('#grab')).toBeVisible(); // the draggable divider await expect(page.locator('#detail')).toContainText('Pick an item'); await page.locator('.ep', { hasText: 'First Episode' }).click(); await expect(page.locator('#detail .dt')).toHaveText('First Episode'); // The enclosure goes to the Files pane beside the list, as the original's did. await expect(page.locator('#files')).toBeVisible(); await expect(page.locator('#files .encbox')).toHaveCount(1); // Only the downloaded one gets a player, and max_new_per_check is 1, so find it by // its chip rather than assuming which episode the daemon happened to fetch. const downloaded = page.locator('.ep', { has: page.locator('.kind.here') }).first(); await downloaded.click(); await expect(page.locator('#files [data-a="play"]')).toBeVisible(); await expect(page.locator('#files .encbox [title="Save to this computer"]')).toBeVisible(); // Selecting another item replaces the pane rather than stacking. await page.locator('.ep', { hasText: 'First Episode' }).click(); await expect(page.locator('#detail .dt')).toHaveText('First Episode'); await expect(page.locator('#files [data-a="play"]')).toHaveCount(0); }); test('a downloaded file that is not audio gets no player', async ({ page }) => { // Regression: anything with a file got an