Nothing measured what the tests reach. scripts/coverage.sh runs the cargo tests under cargo-llvm-cov in the ipx-coverage image (Dockerfile.coverage, built by build-image.sh coverage), in a container held to the image builder's 6 GB and six threads, one crate at a time, with the same room and load checks: an instrumented build rebuilds every dependency, the kind of build that took Tower down on 2026-11-04. The first run stayed under load 8 with 11 GB free; 69% of lines. web.rs and daemon.rs read low because the browser suite drives them against a daemon that is not instrumented. npm run coverage:page runs the browser suite on a build of the page (IPX_COVERAGE=1) whose script keeps a source map to each web/src file: web/concat-map.mts maps the joined files line by line and swc takes it as its input map. Each test's page records V8's coverage (tests/ui/coverage.ts, which the specs now take test and expect from), and monocart-coverage-reports writes it file by file: 73% of lines. build.mjs requires the map helper only for that build, since Dockerfile.web's Node does not run TypeScript, and not with a top-level await, which stopped the CommonJS tests requiring build.mjs. The offline tests move to offline.spec.ts, in TypeScript, and stand alone (they add their feed and set the read state they need), with what #391 found untested: another account signing in clears what was kept, a position saved offline waits and is sent, and pictures past the budget go oldest first (keepPictures takes the budget, and trims whether or not anything new came). The fixture JPEGs were a few bytes no browser could draw; they are real now, and the artwork test checks one draws. tsconfig.tools.json type-checks the tests' and build's TypeScript, in npm run typecheck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
// With the page's coverage recorded when asked for (#390).
|
||||
const { test, expect } = require('./coverage.ts');
|
||||
const { TOKEN } = require('./global-setup');
|
||||
|
||||
// Signals' tests run only when the Signals code has changed since each last passed: they spend the
|
||||
@@ -1285,6 +1286,8 @@ test('artwork comes from iPX, kept, and only as an image', async ({ page }) => {
|
||||
expect(r.headers()['content-type']).toMatch(/^image\//);
|
||||
expect(r.headers()['content-security-policy']).toContain('sandbox');
|
||||
}
|
||||
// And it draws: the fixture's JPEGs were once a few bytes that only said they were one (#391).
|
||||
expect(await page.evaluate((src) => new Promise(r => { const i = new Image(); i.onload = () => r(i.naturalWidth); i.onerror = () => r(0); i.src = src; }), src)).toBe(96);
|
||||
// An address no feed names is not fetched.
|
||||
expect((await page.request.get('/api/art?u=' + encodeURIComponent('http://127.0.0.1:8792/show.xml'))).status()).toBe(404);
|
||||
});
|
||||
@@ -2485,61 +2488,3 @@ test('an error toast is announced and stays until it is dismissed', async ({ pag
|
||||
await t.getByRole('button', { name: 'Dismiss' }).click();
|
||||
await expect(t).toHaveCount(0);
|
||||
});
|
||||
|
||||
// Last: it adds a feed, which the feed counts of earlier tests do not expect.
|
||||
test.describe('offline (#389)', () => {
|
||||
test.use({ serviceWorkers: 'allow' });
|
||||
const outbox = page => page.evaluate(async () => (await (await caches.open('ipx-outbox')).keys()).length);
|
||||
|
||||
test('iPX opens offline with what it read and its pictures, and sends a read made offline once back', async ({ page, context }) => {
|
||||
const api = (p, init) => page.evaluate(([p, init]) => fetch(p, { headers: { 'Content-Type': 'application/json' }, ...init })
|
||||
.then((r) => r.json().catch(() => null)), [p, init]);
|
||||
const added = await api('/api/feeds', { method: 'POST', body: JSON.stringify({ url: 'http://127.0.0.1:8792/offline.xml' }) });
|
||||
const id = added.id || added.feed?.id || 'offline-blog';
|
||||
await page.evaluate(() => navigator.serviceWorker.ready);
|
||||
await page.reload();
|
||||
await expect.poll(() => page.evaluate(() => !!navigator.serviceWorker.controller)).toBe(true);
|
||||
await page.locator('#feedlist .feed', { hasText: 'Offline Blog' }).click({ timeout: 20_000 });
|
||||
await page.locator('#content .tabs button[data-f="unread"]').click();
|
||||
await expect(page.locator('.ep', { hasText: 'Read on a Plane' })).toBeVisible();
|
||||
// The unread item's picture, kept through ipx: no feed names it as artwork, only its text.
|
||||
await expect.poll(() => page.evaluate(async () => (await (await caches.open('ipx-pics')).keys()).length)).toBe(1);
|
||||
|
||||
await context.setOffline(true);
|
||||
await page.reload();
|
||||
// The page, the feed list and the list kept, with the feed and tab open before.
|
||||
await page.locator('.ep', { hasText: 'Read on a Plane' }).click({ timeout: 20_000 });
|
||||
await expect(page.locator('#detail .dbody')).toContainText('Read on a plane, with no connection.');
|
||||
await expect.poll(() => page.locator('#detail .dbody img').evaluate(i => i.complete ? i.naturalWidth : 0)).toBeGreaterThan(0);
|
||||
// Opening it read it, which waits to be sent.
|
||||
await expect.poll(() => outbox(page)).toBe(1);
|
||||
|
||||
await context.setOffline(false);
|
||||
await expect.poll(() => outbox(page), { timeout: 15_000 }).toBe(0);
|
||||
const list = await api(`/api/feeds/${encodeURIComponent(id)}/entries?offset=0&limit=50&filter=all`);
|
||||
expect(list.entries.find(e => e.guid === 'offline-1').read).toBe(true);
|
||||
});
|
||||
|
||||
test('online, a picture in an item\'s text still comes from its publisher, through the worker', async ({ page }) => {
|
||||
await page.evaluate(() => navigator.serviceWorker.ready);
|
||||
await page.reload();
|
||||
await page.locator('#feedlist .feed', { hasText: 'Offline Blog' }).click({ timeout: 20_000 });
|
||||
await page.locator('#content .tabs button[data-f="all"]').click();
|
||||
await page.locator('.ep', { hasText: 'Read on a Plane' }).click();
|
||||
const img = page.locator('#detail .dbody img');
|
||||
await expect.poll(() => img.evaluate(i => i.complete ? i.naturalWidth : 0)).toBeGreaterThan(0);
|
||||
// Past the worker, not round it: what it passes on online is what it would keep.
|
||||
expect(await page.evaluate(() => performance.getEntriesByName('http://127.0.0.1:8792/pic.png')[0]?.workerStart > 0)).toBe(true);
|
||||
});
|
||||
|
||||
test('signing out leaves nothing kept for offline on the device', async ({ page }) => {
|
||||
await page.evaluate(() => navigator.serviceWorker.ready);
|
||||
await page.reload();
|
||||
await expect.poll(() => page.evaluate(async () => (await caches.keys()).filter(k => k.startsWith('ipx-')).length)).toBeGreaterThan(0);
|
||||
await page.locator('#signout').evaluate(b => b.click());
|
||||
await page.waitForURL(u => !u.pathname.endsWith('/') || u.pathname === '/login' || u.pathname.includes('signed-out'));
|
||||
// What was read, its pictures and unsent writes. The sign-in page's own script and styles are
|
||||
// kept again as it loads: the same files for everyone, nothing of the account's.
|
||||
expect(await page.evaluate(async () => (await caches.keys()).filter(k => k.startsWith('ipx-') && k !== 'ipx-page'))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user