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. await expect(page.locator('.feed')).toHaveCount(2, { timeout: 15_000 }); await expect(page.getByText('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 toggle actually changes the theme', async ({ page }) => { // Regression: this button was wired after a line that threw, so it did nothing. const before = await page.evaluate(() => document.documentElement.dataset.theme || 'system'); await page.locator('#theme').click(); await expect .poll(() => page.evaluate(() => document.documentElement.dataset.theme)) .not.toBe(before); }); 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 notes expand', async ({ page }) => { await page.getByText('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'); await first.locator('.t').click(); await expect(page.locator('.notes').first()).toContainText('Show notes for the first one'); }); test('the filter tabs change what is listed', async ({ page }) => { await page.getByText('Test Show').click(); await expect(page.locator('.ep').first()).toBeVisible({ timeout: 20_000 }); const unread = await page.locator('.ep').count(); await page.locator('.tabs button', { hasText: 'All' }).first().click(); await expect(page.locator('#count')).toContainText('episode'); expect(await page.locator('.ep').count()).toBeGreaterThanOrEqual(unread); await page.locator('.tabs button', { hasText: 'Flagged' }).first().click(); await expect(page.locator('#count')).toContainText('0 episodes'); }); test('a feed URL is editable and has a copy button', async ({ page }) => { await page.getByText('Test Show').click(); await page.locator('.btn', { hasText: 'Settings' }).first().click(); await expect(page.locator('#surl')).toHaveValue(/show\.xml/); await expect(page.locator('#scopy')).toBeVisible(); // navigator.clipboard is absent over plain http, so the button must not throw. const errors = []; page.on('pageerror', e => errors.push(e.message)); await page.locator('#scopy').click(); await expect(page.locator('#scopy')).toHaveText(/Copied|Failed/); expect(errors).toEqual([]); }); test('the log view has tabs and shows daemon traffic', async ({ page }) => { await page.locator('#logs').click(); await expect(page.locator('#logbox')).toBeVisible(); await expect(page.locator('#logtabs button')).toHaveCount(4); // Generate traffic, then check the Daemon I/O tab shows both directions. await page.locator('#logtabs button', { hasText: 'Daemon I/O' }).click(); await page.evaluate(() => fetch('/api/fetch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: true }), })); await expect(page.locator('#logbox .l').first()).toBeVisible({ timeout: 15_000 }); await expect(page.locator('#logbox')).toContainText('"cmd":"fetch"', { timeout: 15_000 }); await expect(page.locator('#logbox')).toContainText('"ev":', { timeout: 15_000 }); }); test('an OPML subscription is a collapsible folder', async ({ page }) => { // Read the subscription so its feeds exist. await page.evaluate(() => fetch('/api/fetch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ feed: 'test-subscriptions', force: true }), })); const chev = page.locator('.feed .chev'); await expect(chev).toBeVisible({ timeout: 20_000 }); // Closed by default: the child is not listed until the folder is opened. const before = await page.locator('.feed').count(); await chev.click(); await expect(page.locator('.feed')).toHaveCount(before + 1); // Scoped to the sidebar: the name also appears as the page heading once selected. await expect(page.locator('#feedlist').getByText('Grouped Show')).toBeVisible(); // The subscription's own page lists what is inside it. await page.locator('.feed', { hasText: 'Test Subscriptions' }).first().click(); await expect(page.locator('.childrow')).toHaveCount(1); });