diff --git a/tests/ui/app.spec.ts b/tests/ui/app.spec.ts index 2848b3e..f2477df 100644 --- a/tests/ui/app.spec.ts +++ b/tests/ui/app.spec.ts @@ -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); diff --git a/tests/ui/fixtures/serve.ts b/tests/ui/fixtures/serve.ts index c35e36c..d85d841 100644 --- a/tests/ui/fixtures/serve.ts +++ b/tests/ui/fixtures/serve.ts @@ -43,6 +43,30 @@ http.createServer((req, res) => { Signal Show${item('sig-1', 'Monaco qualifying')}${item('sig-2', 'Baking bread')}`); 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) => + `${guid}${title}`; + res.writeHead(200, { 'content-type': 'application/xml' }).end(` + Live Show${item('live-1', 'Slow episode', 'live-slow.mp3')}${item('live-2', 'Broken episode', 'live-broken.mp3')}`); + 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) => {