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('the theme dropdown in Settings jumps straight to a theme, including Auto', async ({ page }) => { const theme = () => page.evaluate(() => document.documentElement.dataset.theme); await page.locator('#prefs').click(); await expect(page.locator('#stheme')).toHaveValue(await theme()); await page.locator('#stheme').selectOption('auto'); await expect.poll(theme).toBe('auto'); // Auto follows the system; emulating a light system must show the light palette live, // no reload needed, since it is a media query rather than something JS picks per click. await page.emulateMedia({ colorScheme: 'light' }); await expect.poll(() => page.evaluate(() => getComputedStyle(document.body).backgroundColor)) .toBe('rgb(242, 244, 247)'); // --bg in the light palette await page.emulateMedia({ colorScheme: 'dark' }); await expect.poll(() => page.evaluate(() => getComputedStyle(document.body).backgroundColor)) .toBe('rgb(14, 19, 27)'); // the bare :root is already dark; Auto adds nothing here await page.locator('#stheme').selectOption('classic'); await page.reload(); await expect.poll(theme).toBe('classic'); // 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); }); 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