Browser tests for the page's live updates (#404)
events.ts's handlers for a download in progress, finished and failed, and for Signals' new hits, ran in no test. The fixture server has a show to download from by hand: one episode sent over two seconds and one that always fails, added with a word nothing has so that nothing downloads on its own. Downloading the slow one fills its bar, the count line says how far, the toast names the file and the row shows it downloaded; another person with a page open, not subscribed, hears of none of it (#294). The broken one says which item failed and what to do (#304), and its bar stops. A Signal open on its page shows its new hits and its count as soon as a pass picks them out, without a reload; its folder is opened from the keyboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1307,6 +1307,52 @@ test('an item without a title is named from its text, then its file, then its sh
|
||||
expect(names[4]).toEqual({ text: 'nobody', derived: true });
|
||||
});
|
||||
|
||||
test('a download by hand fills its bar and names its file, and only on its own subscriber\'s page', async ({ browser, page }) => {
|
||||
// #404. A word nothing has, so nothing downloads on its own and the download is the test's.
|
||||
const { id: feed } = await (await page.request.post('/api/feeds',
|
||||
{ data: { url: 'http://127.0.0.1:8792/live.xml', keywords: ['nothing-has-this-word'] } })).json();
|
||||
const ctx = await browser.newContext();
|
||||
try {
|
||||
// Someone else, who does not subscribe, has a page open meanwhile (#294).
|
||||
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 piper = await ctx.newPage();
|
||||
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();
|
||||
|
||||
await page.reload();
|
||||
await page.locator('#feedlist .feed', { hasText: 'Live Show' }).click();
|
||||
const slow = page.locator('.ep', { hasText: 'Slow episode' });
|
||||
await expect(slow).toBeVisible({ timeout: 20_000 });
|
||||
await slow.click();
|
||||
await page.locator('#files button[title="Download to the server"]').click();
|
||||
await expect(slow.locator('.dlbar.live')).toBeAttached({ timeout: 10_000 });
|
||||
await expect(page.locator('#count')).toContainText(/downloading .+ — \d+%/);
|
||||
await expect(page.locator('.toast', { hasText: /^Downloaded .+\.mp3/ })).toBeVisible({ timeout: 20_000 });
|
||||
await expect(slow.locator('.kind.here')).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Not a word of it on the other page: not its name, nor its progress.
|
||||
await expect(piper.locator('#count')).not.toContainText('downloading');
|
||||
await expect(piper.locator('.toast', { hasText: 'Downloaded' })).toHaveCount(0);
|
||||
|
||||
// A download that fails says which item, and what to do (#304).
|
||||
const broken = page.locator('.ep', { hasText: 'Broken episode' });
|
||||
await broken.click();
|
||||
await page.locator('#files button[title="Download to the server"]').click();
|
||||
await expect(page.locator('.toast', { hasText: 'Unable to download "Broken episode". Try again with its download button.' }))
|
||||
.toBeVisible({ timeout: 20_000 });
|
||||
await expect(broken.locator('.dlbar.live')).toHaveCount(0);
|
||||
await expect(piper.locator('.toast', { hasText: 'Unable to download' })).toHaveCount(0);
|
||||
} finally {
|
||||
await ctx.close();
|
||||
await page.request.delete(`/api/feeds/${encodeURIComponent(feed)}`);
|
||||
}
|
||||
});
|
||||
|
||||
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());
|
||||
@@ -2383,6 +2429,30 @@ signalsTest('a Signal with no key to judge it links to where one is got and wher
|
||||
await page.request.delete(`/api/signals/${id}`);
|
||||
});
|
||||
|
||||
signalsTest('a Signal\'s new hits reach its open page and its count without a reload', async ({ page }) => {
|
||||
// #404: signal_hits, sent once a pass picks items out.
|
||||
expect((await page.request.patch('/api/me', { data: { typesafe_key: 'test-key' } })).status()).toBe(204);
|
||||
const { id } = await (await page.request.post('/api/signals', { data: { name: 'Pastry', keywords: ['baking'] } })).json();
|
||||
let feed = '';
|
||||
try {
|
||||
await page.reload();
|
||||
await page.evaluate((id: number) => selectFeed(':signal:' + id), id);
|
||||
// The folder opened from the keyboard, as the feed list's Right arrow does.
|
||||
await page.locator('#feedlist .sigs').focus();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
const row = page.locator('#feedlist .feed.child', { hasText: 'Pastry' });
|
||||
await expect(row).toBeVisible();
|
||||
await expect(page.locator('.ep', { hasText: 'Baking bread' })).toHaveCount(0);
|
||||
feed = (await (await page.request.post('/api/feeds', { data: { url: 'http://127.0.0.1:8792/signal.xml' } })).json()).id;
|
||||
await expect(page.locator('.ep', { hasText: 'Baking bread' })).toBeVisible({ timeout: 20_000 });
|
||||
await expect(row.locator('.badge')).toHaveText('1');
|
||||
} finally {
|
||||
await page.request.delete(`/api/signals/${id}`);
|
||||
if (feed) await page.request.delete(`/api/feeds/${encodeURIComponent(feed)}`);
|
||||
await page.request.patch('/api/me', { data: { typesafe_key: null } });
|
||||
}
|
||||
});
|
||||
|
||||
signalsTest('a Signal is renamed from its own page and deleted from there', async ({ page }) => {
|
||||
// #398: signalHead's edit and delete, which nothing clicked.
|
||||
expect((await page.request.patch('/api/me', { data: { typesafe_key: 'test-key' } })).status()).toBe(204);
|
||||
|
||||
@@ -43,6 +43,30 @@ http.createServer((req, res) => {
|
||||
<title>Signal Show</title>${item('sig-1', 'Monaco qualifying')}${item('sig-2', 'Baking bread')}</channel></rss>`);
|
||||
return;
|
||||
}
|
||||
// A show to download from by hand (#404): one episode sent slowly enough for its progress to be
|
||||
// seen, and one that always fails. Their files are theirs alone, as enclosure URLs must be.
|
||||
if (req.url === '/live.xml') {
|
||||
const item = (guid: string, title: string, file: string) =>
|
||||
`<item><guid>${guid}</guid><title>${title}</title><enclosure url="http://127.0.0.1:${port}/${file}" type="audio/mpeg" length="40000"/></item>`;
|
||||
res.writeHead(200, { 'content-type': 'application/xml' }).end(`<?xml version="1.0"?><rss version="2.0"><channel>
|
||||
<title>Live Show</title>${item('live-1', 'Slow episode', 'live-slow.mp3')}${item('live-2', 'Broken episode', 'live-broken.mp3')}</channel></rss>`);
|
||||
return;
|
||||
}
|
||||
if (req.url === '/live-broken.mp3') { res.writeHead(500).end('broken'); return; }
|
||||
if (req.url === '/live-slow.mp3') {
|
||||
const body = fs.readFileSync(path.join(dir, 'ep1.mp3'));
|
||||
res.writeHead(200, { 'content-type': 'audio/mpeg', 'content-length': body.length });
|
||||
// Ten parts, a fifth of a second apart: two seconds of progress.
|
||||
let part = 0;
|
||||
const step = Math.ceil(body.length / 10);
|
||||
const next = () => {
|
||||
if (part * step >= body.length) { res.end(); return; }
|
||||
res.write(body.subarray(part * step, ++part * step));
|
||||
setTimeout(next, 200);
|
||||
};
|
||||
next();
|
||||
return;
|
||||
}
|
||||
const name = decodeURIComponent((req.url ?? '/').split('?')[0]!.replace(/^\//, '')) || 'index';
|
||||
const file = path.join(dir, path.basename(name));
|
||||
fs.readFile(file, (err, body) => {
|
||||
|
||||
Reference in New Issue
Block a user