The original iPodderX layout: toolbar, places, item table, Files pane
- A toolbar across the window with the original's groups: add and unsubscribe, play, mark read and keep for the selected item, scan, a search box for what is showing, and Settings and Log (admins only). - Directory, Popular and All Subscriptions sit at the top of the feed list and open in the main pane; the Popular and Directory buttons and their dialogs are gone. - All Subscriptions lists every item from every feed you subscribe to: GET /api/entries, the per-feed query with its scope widened. The enclosure lookup after it matches files to rows by feed and guid, since a page can now span feeds. - Items are a table (unread, kept, item, feed, file, published) with a Files pane beside it, the text below, and a status bar with totals. On a phone the files follow the text and the table is title and date. - Tests: enclosures are checked in #files; the toolbar's read, keep and play act on the selected item; All Subscriptions holds only your feeds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HdTEWQNrzyFULijigkmMn
This commit is contained in:
@@ -69,20 +69,21 @@ test('the three panes are there and the item text lands in the bottom one', asyn
|
||||
|
||||
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);
|
||||
// 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', { hasText: 'downloaded' }).first();
|
||||
await downloaded.click();
|
||||
await expect(page.locator('#detail audio')).toBeVisible();
|
||||
await expect(page.locator('#detail .encbox .btn', { hasText: 'Save' })).toBeVisible();
|
||||
await expect(page.locator('#files audio')).toBeVisible();
|
||||
await expect(page.locator('#files .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);
|
||||
await expect(page.locator('#files audio')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('a downloaded file that is not audio gets no player', async ({ page }) => {
|
||||
@@ -96,12 +97,12 @@ test('a downloaded file that is not audio gets no player', async ({ page }) => {
|
||||
await row.click();
|
||||
|
||||
await expect(page.locator('#detail .dt')).toHaveText('An Article');
|
||||
await expect(page.locator('#detail audio')).toHaveCount(0);
|
||||
await expect(page.locator('#detail .encbox')).toContainText('image');
|
||||
await expect(page.locator('#detail .encbox')).toContainText('downloaded');
|
||||
await expect(page.locator('#files audio')).toHaveCount(0);
|
||||
await expect(page.locator('#files .encbox')).toContainText('image');
|
||||
await expect(page.locator('#files .encbox')).toContainText('downloaded');
|
||||
// Still offered as a file, just not as an episode: viewable and keepable.
|
||||
await expect(page.locator('#detail .btn', { hasText: 'Save' })).toBeVisible();
|
||||
const view = page.locator('#detail a', { hasText: 'View' });
|
||||
await expect(page.locator('#files .btn', { hasText: 'Save' })).toBeVisible();
|
||||
const view = page.locator('#files a', { hasText: 'View' });
|
||||
await expect(view).toHaveAttribute('target', '_blank');
|
||||
await expect(view).toHaveAttribute('rel', /noopener/);
|
||||
await expect(view).toHaveAttribute('href', /\/media\/\d+/);
|
||||
@@ -115,9 +116,9 @@ test('an item with several enclosures lists them all', async ({ page }) => {
|
||||
await expect(row).toContainText('+1 more file');
|
||||
|
||||
await row.click();
|
||||
// The pane below lists every one: the audio and the image.
|
||||
await expect(page.locator('#detail .encbox')).toHaveCount(2);
|
||||
await expect(page.locator('#detail .encbox').nth(1)).toContainText('image');
|
||||
// The Files pane lists every one: the audio and the image.
|
||||
await expect(page.locator('#files .encbox')).toHaveCount(2);
|
||||
await expect(page.locator('#files .encbox').nth(1)).toContainText('image');
|
||||
});
|
||||
|
||||
test('the filter tabs change what is listed', async ({ page }) => {
|
||||
@@ -291,6 +292,30 @@ test('opening an item marks it read, and the toggle flips it back', async ({ pag
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('the toolbar acts on the selected item', async ({ page }) => {
|
||||
await page.getByText('Test Show').click();
|
||||
const row = () => page.locator('.ep', { hasText: 'Second Episode' });
|
||||
await expect(row()).toBeVisible({ timeout: 20_000 });
|
||||
// Nothing selected, nothing to act on.
|
||||
await expect(page.locator('#tbRead')).toBeDisabled();
|
||||
|
||||
await row().click(); // opening it reads it
|
||||
await expect(row()).toHaveClass(/read/);
|
||||
await page.locator('#tbRead').click();
|
||||
await expect(row()).not.toHaveClass(/read/);
|
||||
|
||||
await page.locator('#tbFlag').click();
|
||||
await expect(row().locator('.fl')).toHaveClass(/on/);
|
||||
await page.locator('#tbFlag').click(); // and back, so later tests see it unkept
|
||||
await expect(row().locator('.fl')).not.toHaveClass(/on/);
|
||||
|
||||
// Second Episode is the one the daemon downloaded, so it plays from the toolbar.
|
||||
await expect(page.locator('#tbPlay')).toBeEnabled();
|
||||
await page.locator('#tbPlay').click();
|
||||
await expect(page.locator('#player')).toBeVisible();
|
||||
await page.locator('#pclose').click();
|
||||
});
|
||||
|
||||
test('a second person has their own feeds and their own read state', async ({ browser }) => {
|
||||
const { execFileSync } = require('child_process');
|
||||
const setup = require('./global-setup');
|
||||
@@ -350,7 +375,7 @@ test('deleting a shared file warns that it is everyone\'s copy', async ({ page }
|
||||
await expect(row).toBeVisible({ timeout: 20_000 });
|
||||
await row.click();
|
||||
|
||||
const del = page.locator('#detail button', { hasText: 'Delete' });
|
||||
const del = page.locator('#files button', { hasText: 'Delete' });
|
||||
await expect(del).toHaveText('Delete for everyone');
|
||||
|
||||
// Two prompts: the page's own, then the server's, because someone else has not played
|
||||
@@ -566,7 +591,7 @@ test('Popular lists what everyone here reads, but never a private feed', async (
|
||||
await piper.locator('button[type=submit]').click();
|
||||
await expect(piper.locator('#feedlist')).toContainText('No feeds.');
|
||||
|
||||
await piper.locator('#popularFeeds').click();
|
||||
await piper.locator('#feedlist .place', { hasText: 'Popular' }).click();
|
||||
const offered = piper.locator('#popular .childrow');
|
||||
await expect(offered.filter({ hasText: 'Test Show' })).toBeVisible({ timeout: 20_000 });
|
||||
// An OPML's own feeds ride on the OPML, and a key in a URL marks someone's paid feed.
|
||||
@@ -589,8 +614,8 @@ test('Popular lists what everyone here reads, but never a private feed', async (
|
||||
expect(dir.map(p => p.id)).not.toContain('paid-show');
|
||||
|
||||
// Subscribe from the directory this time; the popular list shares the same rows.
|
||||
await piper.keyboard.press('Escape');
|
||||
await piper.locator('#directoryFeeds').click();
|
||||
await piper.locator('#feedlist .place', { hasText: 'Directory' }).click();
|
||||
await expect(piper.locator('#count')).toContainText(`Directory: ${dir.length} feed`);
|
||||
await expect(offered.filter({ hasText: 'Test Show' })).toBeVisible();
|
||||
await expect(offered.filter({ hasText: /Paid Show|paid-show/ })).toHaveCount(0);
|
||||
|
||||
@@ -603,7 +628,16 @@ test('Popular lists what everyone here reads, but never a private feed', async (
|
||||
|
||||
// Everyone counts, you included: it stays listed, marked as yours, with one more subscriber.
|
||||
expect(await row()).toMatchObject({ subscribed: true, subscribers: before.subscribers + 1 });
|
||||
await piper.locator('#popularFeeds').click();
|
||||
await piper.locator('#feedlist .place', { hasText: 'Popular' }).click();
|
||||
await expect(offered.filter({ hasText: 'Test Show' })).toContainText('Subscribed');
|
||||
|
||||
// All Subscriptions is every item from piper's feeds and only those: the admin's Picture
|
||||
// Blog is not among them.
|
||||
await piper.locator('#feedlist .place', { hasText: 'All Subscriptions' }).click();
|
||||
const first = piper.locator('.ep', { hasText: 'First Episode' });
|
||||
await expect(first).toBeVisible({ timeout: 20_000 });
|
||||
await expect(first.locator('.fd')).toHaveText('Test Show');
|
||||
await expect(piper.locator('.ep', { hasText: 'An Article' })).toHaveCount(0);
|
||||
await expect(piper.locator('#count')).toContainText('All Subscriptions:');
|
||||
await ctx.close();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user