One place to get an episode's file where you are (#457)
Getting an episode onto a phone took two buttons in two states: download it to the server, wait, then keep it on the device, beside Save to this computer, Share and Delete, five buttons that did not say where each copy went. Now a file has one button that brings it to where you are. On a phone or in the iOS app it ends on the device; when iPX has not got the file yet, iPX fetches it and the device's copy follows once iPX says it is done (remembered in localStorage, so a page closed meanwhile catches up when a list next shows the file). On a computer it has iPX fetch the file, and with iPX's copy there saves one. A line beside the size says where the file is, and the rarer actions, two of them destructive, are under a menu. isPlayable is false for any file not on iPX, so the phone's button is decided by playsKind, the same test without the path. download_done now marks the file as on iPX in what the page holds, so the open file's box changes at once rather than after the list is fetched again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RqkrVA8TdTxe556JB3sR1n
This commit is contained in:
@@ -297,7 +297,7 @@ test('the three panes are there and the item text lands in the bottom one', asyn
|
||||
const downloaded = page.locator('.ep', { has: page.locator('.kind.here') }).first();
|
||||
await downloaded.click();
|
||||
await expect(page.locator('#files [data-a="play"]')).toBeVisible();
|
||||
await expect(page.locator('#files .encbox [title="Save to this computer"]')).toBeVisible();
|
||||
await expect(page.locator('#files .encbox [title="Save a copy to this computer"]')).toBeVisible();
|
||||
|
||||
// Selecting another item replaces the pane rather than stacking.
|
||||
await page.locator('.ep', { hasText: 'First Episode' }).click();
|
||||
@@ -349,7 +349,7 @@ test('a downloaded file that is not audio gets no player', async ({ page }) => {
|
||||
// What it is and that it is here: one icon, green, with the words in its tooltip.
|
||||
await expect(page.locator('#files .encbox .kind.here')).toHaveAttribute('title', 'image, downloaded');
|
||||
// Still offered as a file, just not as an episode: viewable and keepable.
|
||||
await expect(page.locator('#files [title="Save to this computer"]')).toBeVisible();
|
||||
await expect(page.locator('#files [title="Save a copy to this computer"]')).toBeVisible();
|
||||
const view = page.locator('#files a[title="View in a new tab"]');
|
||||
await expect(view).toHaveAttribute('target', '_blank');
|
||||
await expect(view).toHaveAttribute('rel', /noopener/);
|
||||
@@ -860,9 +860,10 @@ test('deleting a shared file warns that it is everyone\'s copy', async ({ page }
|
||||
await expect(row).toBeVisible({ timeout: 20_000 });
|
||||
await row.click();
|
||||
|
||||
// An icon now; what it does, and to whom, is in its tooltip.
|
||||
const del = page.locator('#files button[data-a="del"]');
|
||||
await expect(del).toHaveAttribute('title', /^Delete for everyone \(shared with 1 other person/);
|
||||
// Under the file's menu (#457), saying what it does, and to whom.
|
||||
await page.locator('#files .encbox [data-a="more"]').last().click();
|
||||
const del = page.locator('#modalCard [data-m="del"]');
|
||||
await expect(del).toHaveText(/^Delete from iPX, for everyone \(shared with 1 other person/);
|
||||
|
||||
// Two prompts: the page's own, then the server's, because someone else has not played
|
||||
// it. Accept the first, decline the second, and the file must survive.
|
||||
@@ -1345,7 +1346,7 @@ test('a download by hand fills its bar and names its file, and only on its own s
|
||||
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 page.locator('#files button[title="Download to iPX"]').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 });
|
||||
@@ -1358,7 +1359,7 @@ test('a download by hand fills its bar and names its file, and only on its own s
|
||||
// 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 page.locator('#files button[title="Download to iPX"]').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);
|
||||
@@ -1499,14 +1500,16 @@ test('a deleted file looks as if it was never downloaded', async ({ page }) => {
|
||||
const row = page.locator('.ep', { hasText: 'An Article' });
|
||||
await expect(row).toBeVisible({ timeout: 20_000 });
|
||||
await row.click();
|
||||
await page.locator('#files button[data-a="del"]').click();
|
||||
await page.locator('#files .encbox [data-a="more"]').last().click();
|
||||
await page.locator('#modalCard [data-m="del"]').click();
|
||||
|
||||
// No "reaped", no chip at all: just the way to get it again.
|
||||
await expect(row.locator('.kind.here')).toHaveCount(0);
|
||||
await expect(row).not.toContainText(/reaped/i);
|
||||
await row.click();
|
||||
await expect(page.locator('#files')).not.toContainText(/reaped/i);
|
||||
await expect(page.locator('#files button[title="Download to the server"]')).toBeVisible();
|
||||
await expect(page.locator('#files button[title="Download to iPX"]')).toBeVisible();
|
||||
await expect(page.locator('#files .encbox .meta')).toContainText('Publisher only');
|
||||
});
|
||||
|
||||
test('one action, one icon: the toolbar, the page and every dialog agree', async ({ page }) => {
|
||||
@@ -1923,7 +1926,8 @@ test.describe('on a phone', () => {
|
||||
await page.locator('.feed', { hasText: 'Test Show' }).click();
|
||||
await page.locator('.tabs button', { hasText: 'Downloaded' }).first().click();
|
||||
await page.locator('.ep').first().click();
|
||||
const del = page.locator('#detail [data-a="del"]');
|
||||
// Delete is under the file's menu (#457), which sits above the notes.
|
||||
const del = page.locator('#detail .encbox [data-a="more"]').first();
|
||||
await expect(del).toBeInViewport();
|
||||
// Below a long set of notes it was screens down and looked missing (issue #21).
|
||||
expect(await page.locator('#detail').evaluate(d =>
|
||||
@@ -2130,6 +2134,31 @@ test.describe('an item with no files, on a phone', () => {
|
||||
await expect(page.locator('#detail')).not.toContainText('No files');
|
||||
});
|
||||
|
||||
test('one press of a file\'s button brings it from the publisher, through iPX, to the phone (#457)', async ({ page }) => {
|
||||
// A file iPX does not have: Test Show's downloaded one, deleted first, so the test owns it.
|
||||
const enc = await page.evaluate(async () => {
|
||||
const list = await api('/api/feeds/test-show/entries?filter=all') as ListPage;
|
||||
const x = list.entries.flatMap(e => e.enclosures).find(x => x.path && playsKind(x))!;
|
||||
await api(`/api/enclosures/${x.id}?force=true`, { method: 'DELETE' });
|
||||
return x.id;
|
||||
});
|
||||
await page.locator('#burger').click();
|
||||
await page.locator('.feed', { hasText: 'Test Show' }).click();
|
||||
await page.locator('.tabs button', { hasText: 'All' }).first().click();
|
||||
// The row of a file not on iPX carries its download bar, by the file's id.
|
||||
await page.locator('.ep').filter({ has: page.locator(`.dlbar[data-bar="${enc}"]`) }).click();
|
||||
const box = page.locator('#detail .encbox');
|
||||
await expect(box.locator('.meta')).toContainText('Publisher only');
|
||||
await box.locator('[data-a="want"]').click();
|
||||
await expect(box.locator('.meta')).toHaveText(/On iPX and this device$/, { timeout: 20_000 });
|
||||
await expect(box.locator('[data-a="more"][aria-pressed="true"]')).toBeVisible();
|
||||
expect(await page.evaluate(() => localStorage.getItem('ipx.wanted'))).toBe('[]');
|
||||
// Taken off the phone from its menu; iPX keeps its copy.
|
||||
await box.locator('[data-a="more"][aria-pressed="true"]').click();
|
||||
await page.locator('#modalCard [data-m="offline"]').click();
|
||||
await expect(box.locator('.meta')).toHaveText(/On iPX$/);
|
||||
});
|
||||
|
||||
test('back and the item\'s actions stay at hand anywhere in a long post (#456)', async ({ page }) => {
|
||||
await page.locator('#burger').click();
|
||||
await page.locator('#feedlist .place', { hasText: 'All Subscriptions' }).click();
|
||||
@@ -2585,9 +2614,10 @@ test('a downloaded file\'s buttons sit on one row in the files pane', async ({ p
|
||||
await expect(page.locator('.ep').first()).toBeVisible();
|
||||
await page.evaluate(() => showDetail(S.entries.find(e => e.enclosures.some(x => x.path)) ?? null));
|
||||
const box = page.locator('#files .encbox').first();
|
||||
await expect(box.locator('.btn')).toHaveCount(5);
|
||||
// Play, the one button that brings it here, share, and the rest under ⋯ (#457).
|
||||
await expect(box.locator('.btn')).toHaveCount(4);
|
||||
const tops = await box.locator('.btn').evaluateAll(bs => bs.map(b => Math.round(b.getBoundingClientRect().top)));
|
||||
expect(new Set(tops).size, 'play, keep, save, share and delete wrapped onto two rows').toBe(1);
|
||||
expect(new Set(tops).size, 'play, save, share and more wrapped onto two rows').toBe(1);
|
||||
// And on a phone, where the pane is the whole width (#412 added keep).
|
||||
await page.setViewportSize({ width: 375, height: 800 });
|
||||
const phone = await box.locator('.btn').evaluateAll(bs => bs.map(b => Math.round(b.getBoundingClientRect().top)));
|
||||
|
||||
Reference in New Issue
Block a user