The Directory (#405): a search nothing matches says so and shows no tiles; a show's page draws what the directory says of it, so with a long description More opens it and Less closes it, a failing feed says why, its latest items are listed, someone who is not the directory's admin is offered neither Refresh nor Remove, and Subscribe there says so and opens the feed. Search, Back, Subscribe from a tile and an admin's Refresh and Remove were already tested. The page (#406), on a fixture show of four linked episodes: a schedule's bar tapped lights that many of the newest; Download latest 3 queues three and they download; v opens the item's own page; Unsubscribe from the feed's dialog leaves no feed open. Shift J and K move between feeds, Right and Left open and close a folder from the keyboard, r takes the lists again. The player keeps its speed and volume across a reload, reports a stall of over half a second to the server's log, and marks an item played to its end read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1353,6 +1353,129 @@ test('a download by hand fills its bar and names its file, and only on its own s
|
||||
}
|
||||
});
|
||||
|
||||
test('Download latest queues the newest, a schedule\'s bars choose how many, v opens an item, and Unsubscribe leaves no feed open', async ({ page, context }) => {
|
||||
// #406. The server's default downloads the newest one on its own; the rest wait.
|
||||
const { id: feed } = await (await page.request.post('/api/feeds', { data: { url: 'http://127.0.0.1:8792/latest.xml' } })).json();
|
||||
const here = async () => (await (await page.request.get(`/api/feeds/${encodeURIComponent(feed)}/entries`)).json() as ListPage)
|
||||
.entries.filter(e => e.enclosures.some(x => x.path)).map(e => e.title).sort();
|
||||
try {
|
||||
await expect.poll(here, { timeout: 20_000 }).toEqual(['Latest 1']);
|
||||
await page.reload();
|
||||
await page.locator('#feedlist .feed', { hasText: 'Latest Show' }).click();
|
||||
await expect(page.locator('.ep', { hasText: 'Latest 4' })).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
// The settings' strip of the newest items: tapping the third lights three.
|
||||
await page.locator('#content .acts [data-a="settings"]').click();
|
||||
const bars = page.locator('#sepwin i');
|
||||
await expect(bars).toHaveCount(4);
|
||||
await bars.nth(2).dispatchEvent('pointerdown', { pointerId: 1, clientX: (await bars.nth(2).boundingBox())!.x + 1 });
|
||||
await expect(page.locator('[name=swin][value=newest]')).toBeChecked();
|
||||
await expect(page.locator('#smax')).toHaveValue('3');
|
||||
await expect(page.locator('#sepwin i.on')).toHaveCount(3);
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.locator('#content .acts [data-a="dl"]').click();
|
||||
await page.locator('#dcount').fill('3');
|
||||
await page.locator('#dgo').click();
|
||||
await expect(page.locator('.toast', { hasText: 'Queued 3 items' })).toBeVisible();
|
||||
await expect.poll(here, { timeout: 20_000 }).toEqual(['Latest 1', 'Latest 2', 'Latest 3', 'Latest 4']);
|
||||
|
||||
// v opens the item's own page.
|
||||
await page.locator('.ep', { hasText: 'Latest 4' }).click();
|
||||
await page.locator('#detail').click({ position: { x: 1, y: 1 } });
|
||||
const opened = context.waitForEvent('page');
|
||||
await page.keyboard.press('v');
|
||||
expect((await opened).url()).toBe('http://127.0.0.1:8792/latest-4.html');
|
||||
|
||||
await page.locator('#content .acts [data-a="rm"]').click();
|
||||
await page.locator('#rgo').click();
|
||||
await expect(page.locator('.toast', { hasText: 'Unsubscribed' })).toBeVisible();
|
||||
await expect(page.locator('#feedlist .feed', { hasText: 'Latest Show' })).toHaveCount(0);
|
||||
await expect(page.locator('#content h2', { hasText: 'Latest Show' })).toHaveCount(0);
|
||||
} finally {
|
||||
await page.request.delete(`/api/feeds/${encodeURIComponent(feed)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the feed list\'s keys: Shift J and K between feeds, the arrows on a folder, r to refresh', async ({ page }) => {
|
||||
// #406.
|
||||
const at = () => page.evaluate(() => S.feed);
|
||||
await page.locator('#feedlist .feed', { hasText: 'Test Show' }).first().click();
|
||||
const rows = await page.locator('#feedlist [data-id]').evaluateAll(r => r.map(x => (x as HTMLElement).dataset.id));
|
||||
const i = rows.indexOf(await at() ?? '');
|
||||
await page.locator('#content').click({ position: { x: 1, y: 1 } });
|
||||
await page.keyboard.press('Shift+J');
|
||||
await expect.poll(at).toBe(rows[i + 1]);
|
||||
await page.keyboard.press('Shift+K');
|
||||
await expect.poll(at).toBe(rows[i]);
|
||||
|
||||
// A folder opens with Right and closes with Left, from the keyboard alone.
|
||||
const folder = page.locator('#feedlist .group').first();
|
||||
const chev = folder.locator('.chev');
|
||||
const open = await chev.getAttribute('aria-expanded') === 'true';
|
||||
await folder.focus();
|
||||
await page.keyboard.press(open ? 'ArrowLeft' : 'ArrowRight');
|
||||
await expect(page.locator('#feedlist .group').first().locator('.chev')).toHaveAttribute('aria-expanded', String(!open));
|
||||
await page.locator('#feedlist .group').first().focus();
|
||||
await page.keyboard.press(open ? 'ArrowRight' : 'ArrowLeft');
|
||||
await expect(page.locator('#feedlist .group').first().locator('.chev')).toHaveAttribute('aria-expanded', String(open));
|
||||
|
||||
// r takes the lists again; v opens the item's own page.
|
||||
await page.locator('#feedlist .feed', { hasText: 'Test Show' }).first().click();
|
||||
await page.locator('.tabs button', { hasText: 'All' }).first().click();
|
||||
await page.locator('.ep').first().click();
|
||||
await page.locator('#detail').click({ position: { x: 1, y: 1 } });
|
||||
const again = page.waitForRequest(r => new URL(r.url()).pathname === '/api/feeds');
|
||||
await page.keyboard.press('r');
|
||||
await again;
|
||||
});
|
||||
|
||||
test('the player keeps its speed and volume, marks an item played to its end read, and reports a stall', async ({ page }) => {
|
||||
// #406.
|
||||
// Hidden until something plays, so set as the page's own handlers see a change.
|
||||
await page.locator('#rate').evaluate((r: HTMLSelectElement) => { r.value = '1.5'; r.dispatchEvent(new Event('change')); });
|
||||
await page.locator('#vol').evaluate((v: HTMLInputElement) => { v.value = '40'; v.dispatchEvent(new Event('input')); });
|
||||
await page.reload();
|
||||
await expect(page.locator('#rate')).toHaveValue('1.5');
|
||||
await expect(page.locator('#vol')).toHaveValue('40');
|
||||
expect(await page.evaluate(() => [audio.playbackRate, audio.volume])).toEqual([1.5, 0.4]);
|
||||
|
||||
await page.locator('.feed', { hasText: 'Test Show' }).click();
|
||||
await page.locator('.tabs button', { hasText: 'All' }).first().click();
|
||||
const row = page.locator('.ep', { has: page.locator('.kind.here') }).first();
|
||||
await expect(row).toBeVisible({ timeout: 20_000 });
|
||||
await row.click();
|
||||
// A player with no file behind it, as the play buttons' test has, events and all.
|
||||
await page.evaluate(() => {
|
||||
let paused = true;
|
||||
Object.defineProperty(audio, 'paused', { get: () => paused, configurable: true });
|
||||
audio.play = async () => { paused = false; audio.dispatchEvent(new Event('play')); };
|
||||
audio.pause = () => { paused = true; audio.dispatchEvent(new Event('pause')); };
|
||||
});
|
||||
await page.locator('#files [data-a="play"]').click();
|
||||
await expect.poll(() => page.evaluate(() => !audio.paused)).toBe(true);
|
||||
const key = await page.evaluate(() => [player.feed!, player.guid!]);
|
||||
const read = async () => (await (await page.request.get(`/api/entries/${encodeURIComponent(key[0])}/${encodeURIComponent(key[1])}`)).json()).read;
|
||||
|
||||
// Started, then held up for more than half a second: the server's log hears how long.
|
||||
await page.evaluate(() => audio.dispatchEvent(new Event('playing')));
|
||||
const stall = page.waitForRequest(r => r.url().endsWith('/api/client') && r.postDataJSON()?.ev === 'stall');
|
||||
await page.evaluate(async () => {
|
||||
audio.dispatchEvent(new Event('waiting'));
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
audio.dispatchEvent(new Event('playing'));
|
||||
});
|
||||
expect((await stall).postDataJSON().ms).toBeGreaterThanOrEqual(500);
|
||||
|
||||
// Marked unread while it plays, and played to its end: read again.
|
||||
await page.locator('#detail [data-a="read"]').click();
|
||||
await expect.poll(read).toBe(false);
|
||||
await page.evaluate(() => audio.dispatchEvent(new Event('ended')));
|
||||
await expect.poll(read).toBe(true);
|
||||
await page.locator('#pclose').click();
|
||||
await page.evaluate(() => { localStorage.removeItem('ipx.rate'); localStorage.removeItem('ipx.vol'); });
|
||||
});
|
||||
|
||||
test('a deleted file looks as if it was never downloaded', async ({ page }) => {
|
||||
// Other people subscribe to Picture Blog by now, so both prompts come; take them.
|
||||
page.on('dialog', d => d.accept());
|
||||
@@ -1635,6 +1758,61 @@ test('the admin page sets up the directory, and a feed marked private leaves it
|
||||
if (paid) expect(paid.private_by_address).toBe(true);
|
||||
});
|
||||
|
||||
test('a show\'s page in the Directory: More and Less, why it fails, and Subscribe from there', async ({ browser }) => {
|
||||
// #405. Someone who subscribes to little, so a listed feed opens its Directory page.
|
||||
const env = { ...process.env, IPX_CONFIG: `${setup.root}/config/config.toml`, IPX_DATA_DIR: `${setup.root}/data` };
|
||||
try { execFileSync(setup.IPX_BIN, ['user', 'add', 'piper'], { input: 'piperpassword', env }); }
|
||||
catch (e) { if (!said(e).includes('already exists')) throw e; }
|
||||
const ctx = await browser.newContext();
|
||||
const piper = await ctx.newPage();
|
||||
try {
|
||||
await piper.goto('/login');
|
||||
await piper.locator('#name').fill('piper');
|
||||
await piper.locator('#pw').fill('piperpassword');
|
||||
await piper.locator('button[type=submit]').click();
|
||||
await expect(piper.locator('#feedlist')).toBeVisible();
|
||||
const listed = async () => (await (await piper.request.get('/api/directory')).json() as DirFeed[]).find(p => p.title === 'Grouped Show');
|
||||
await expect.poll(listed, { timeout: 20_000 }).toBeTruthy();
|
||||
const show = (await listed())!;
|
||||
// The page draws what the directory says of a feed; here, a long description and a failure.
|
||||
await piper.route(url => url.pathname === `/api/directory/${show.id}`, r => r.fulfill({ json: {
|
||||
description: 'A show about many things. '.repeat(60), failing: 'The feed has answered HTTP 404 Not Found since Monday.',
|
||||
items: [{ guid: 'g', title: 'Latest one', link: null, description: 'What it was about.', published: null, duration: null }],
|
||||
can_admin: false,
|
||||
} }));
|
||||
|
||||
await piper.locator('#feedlist .place', { hasText: 'Directory' }).click();
|
||||
const tiles = piper.locator('#dtiles .tile');
|
||||
await piper.locator('#epSearch').fill('no show is called this');
|
||||
await expect(piper.locator('#directory h2')).toHaveText('Results for “no show is called this”');
|
||||
await expect(piper.locator('#directory .sub').first()).toHaveText('No feed here has that in its name.');
|
||||
await expect(tiles).toHaveCount(0);
|
||||
|
||||
await piper.locator('#epSearch').fill('grouped');
|
||||
await tiles.filter({ hasText: 'Grouped Show' }).locator('.txt').click();
|
||||
await expect(piper.locator('.show h2')).toHaveText('Grouped Show');
|
||||
await expect(piper.locator('#dshow .lep', { hasText: 'Latest one' })).toBeVisible();
|
||||
const more = piper.locator('#dabout button');
|
||||
await expect(more).toHaveText('More');
|
||||
await more.click();
|
||||
await expect(more).toHaveText('Less');
|
||||
await expect(more).toHaveAttribute('aria-expanded', 'true');
|
||||
await expect(piper.locator('#dabout')).toHaveClass(/open/);
|
||||
await more.click();
|
||||
await expect(more).toHaveText('More');
|
||||
await expect(piper.locator('#dfail')).toHaveText('The feed has answered HTTP 404 Not Found since Monday.');
|
||||
// Not an admin of the directory: neither of its buttons.
|
||||
await expect(piper.locator('.show [data-a="drefresh"], .show [data-a="dremove"]')).toHaveCount(0);
|
||||
|
||||
await piper.locator('.show [data-a="sub"]').click();
|
||||
await expect(piper.locator('.toast', { hasText: 'Subscribed to Grouped Show' })).toBeVisible();
|
||||
await expect(piper.locator('#feedlist .feed.sel', { hasText: 'Grouped Show' })).toBeVisible({ timeout: 20_000 });
|
||||
} finally {
|
||||
await piper.request.delete('/api/feeds/grouped-show');
|
||||
await ctx.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('an admin removes a feed from the directory, and unblocks it (#206)', async ({ page, browser }) => {
|
||||
const all = () => page.evaluate((): Promise<DirFeed[]> => api('/api/directory'));
|
||||
const listed = async () => (await all()).map(p => p.title);
|
||||
|
||||
Reference in New Issue
Block a user