Fix the open bugs: read state, Unread tab, feed errors, theme button, log button, relative images

- Opening an item stays read: a list refresh that crossed with the write no longer
  puts the unread dot back (#16).
- On the Unread tab the item you were reading goes when you move to the next (#17).
- Feed errors mark the feed with a red ! instead of a toast per failure (#20).
- The theme is chosen in Settings only (#15).
- The server leaves the Log button out of a non-admin's page, so it no longer flashes (#29).
- Relative images and links in a post resolve against the post's link (#28).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 12:10:05 +00:00
parent 0443177471
commit d7a4a0b663
4 changed files with 133 additions and 53 deletions

View File

@@ -20,27 +20,6 @@ test('the page loads and lists the configured feeds', async ({ page }) => {
expect(errors, 'the page script must not throw at load').toEqual([]);
});
test('the theme toggle actually changes the theme', async ({ page }) => {
// Regression: this button was wired after a line that threw, so it did nothing.
const before = await page.evaluate(() => document.documentElement.dataset.theme || 'system');
await page.locator('#theme').click();
await expect
.poll(() => page.evaluate(() => document.documentElement.dataset.theme))
.not.toBe(before);
});
test('the theme button steps through dark, light and classic, and remembers', async ({ page }) => {
const theme = () => page.evaluate(() => document.documentElement.dataset.theme);
for (let i = 0; i < 3 && (await theme()) !== 'classic'; i++) await page.locator('#theme').click();
expect(await theme()).toBe('classic');
await expect(page.locator('#theme')).toHaveAttribute('title', /Classic.*Click for Auto/);
await page.reload();
await expect.poll(theme).toBe('classic');
// The 2004 Mac app set its type in Lucida Grande.
expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toContain('Lucida Grande');
});
test('the theme dropdown in Settings jumps straight to a theme, including Auto', async ({ page }) => {
const theme = () => page.evaluate(() => document.documentElement.dataset.theme);
await page.locator('#prefs').click();
@@ -57,10 +36,12 @@ test('the theme dropdown in Settings jumps straight to a theme, including Auto',
await expect.poll(() => page.evaluate(() => getComputedStyle(document.body).backgroundColor))
.toBe('rgb(14, 19, 27)'); // the bare :root is already dark; Auto adds nothing here
// The header button and the dropdown are the same one setting, not two.
await page.locator('#modalCard .cardacts .btn').first().click(); // Cancel, closing the modal
await page.locator('#theme').click();
expect(await theme()).toBe('dark');
await page.locator('#stheme').selectOption('classic');
await page.reload();
await expect.poll(theme).toBe('classic');
// The 2004 Mac app set its type in Lucida Grande.
expect(await page.evaluate(() => getComputedStyle(document.body).fontFamily)).toContain('Lucida Grande');
expect(await page.locator('#theme').count(), 'the theme lives in Settings only').toBe(0);
});
test('settings opens and saves the global schedule', async ({ page }) => {
@@ -233,6 +214,32 @@ test('the filter tabs change what is listed', async ({ page }) => {
await expect(page.locator('#count')).toContainText('0 items');
});
test('on the Unread tab an item stays while you read it and goes when you move on', async ({ page }) => {
await page.locator('.feed', { hasText: 'Test Show' }).click();
await page.locator('.tabs button', { hasText: 'All' }).first().click();
await expect(page.locator('.ep').nth(1)).toBeVisible({ timeout: 20_000 });
// Earlier tests read things; make the first two unread with their own dots.
for (const i of [0, 1]) {
const row = page.locator('.ep').nth(i);
if (await row.evaluate(r => r.classList.contains('read'))) {
await row.locator('[data-a="read"]').click();
await expect(page.locator('.ep').nth(i)).not.toHaveClass(/\bread\b/);
}
}
await page.locator('.tabs button', { hasText: 'Unread' }).first().click();
const first = page.locator('.ep').first();
const guid = await first.getAttribute('data-guid');
await first.click();
const it = page.locator(`.ep[data-guid="${guid}"]`);
await expect(it).toHaveClass(/\bread\b/);
await page.waitForTimeout(1500); // past the SSE refresh debounce and loadFeeds
await expect(it).toBeVisible();
await page.locator('.ep').nth(1).click();
await expect(it).toHaveCount(0);
await page.locator('.tabs button', { hasText: 'All' }).first().click();
});
test('a feed URL is editable and has a copy button', async ({ page }) => {
await page.locator('.feed', { hasText: 'Test Show' }).click();
await page.locator('#content .acts [data-a="settings"]').click();