Feeds beside, the feed's items above, and the selected item's text with its enclosures below -- the shape iPodderX used. Selecting a row fills the pane below instead of expanding inline; enclosures render there as a player when the file is present and a labelled download when it is not. The divider drags and its position is remembered. The archived site kept no usable screenshot of the original window, only marketing panels, so this follows the description rather than reference art. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
155 lines
7.1 KiB
JavaScript
155 lines
7.1 KiB
JavaScript
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 the text opens below', 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');
|
|
|
|
// 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.getByText('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 episode');
|
|
|
|
await page.locator('.ep', { hasText: 'First Episode' }).click();
|
|
await expect(page.locator('#detail .dt')).toHaveText('First Episode');
|
|
// The enclosure travels with the item, into the same pane.
|
|
await expect(page.locator('#detail .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', { hasText: 'downloaded' }).first();
|
|
await downloaded.click();
|
|
await expect(page.locator('#detail audio')).toBeVisible();
|
|
await expect(page.locator('#detail .encbox .btn', { hasText: 'Save' })).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('#detail audio')).toHaveCount(0);
|
|
});
|
|
|
|
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);
|
|
});
|