The Playwright suite had no linter and no formatter — only tsc. Add flat-config ESLint
(typescript-eslint, type-aware) and Prettier (2-space, matching the suite's style).
Rules keep the ones that catch real TEST bugs and drop the noise:
- no-floating-promises KEPT — an un-awaited request/assertion can let a test end before it runs,
passing vacuously. It caught one: the SSE reader loop in sse-listener is now explicitly `void`.
- no-unused-vars KEPT — caught three dead bindings (an unused adminToken fixture arg, an unused
`api` arg, an unused JPEG_MAGIC import), all removed.
- no-explicit-any OFF — all test code; `any` is the honest type for an untyped res.json() body or
a page.evaluate() return.
- no-empty-pattern OFF — Playwright's dependency-free fixtures are `async ({}, use) => {}`.
Refactor: `const BASE = process.env.E2E_FRONTEND_URL ?? '...'` was redeclared verbatim in 23
files — extracted to helpers/env.ts and imported, so a port/scheme change is one edit not a sweep.
Then `prettier --write`. Verified: eslint clean, tsc clean, prettier clean, desktop suite 210
passed / 1 skipped. (One mobile spec flaked once under retries:0 — a pre-existing cross-test
reflow-timing vector from the flakiness audit, not this change: the each-key edit is stable across
16 isolated runs and a clean full mobile re-run.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
3.1 KiB
TypeScript
70 lines
3.1 KiB
TypeScript
/**
|
||
* Covers the HTML export's VIDEO path (perf/stability fix P4). The export used to
|
||
* copy every original — including full-size videos — into a temp dir and then re-read
|
||
* them into the ZIP (transient 2× disk). It now streams video originals straight from
|
||
* disk into the archive. The image-only export specs never exercised that branch, so
|
||
* this drives a real video upload → export → and proves the video entry lands in
|
||
* Memories.zip (i.e. the streamed-from-original path works and the video isn't dropped).
|
||
*
|
||
* The fixture clip is <1s, so ffmpeg extracts no thumbnail frame — but exits 0, so the
|
||
* compression worker keeps the upload (it isn't auto-cleaned). That's the intended
|
||
* shape here: the full video is exported even when its thumbnail is absent.
|
||
*/
|
||
import { test, expect } from '../../fixtures/test';
|
||
import { uploadRaw } from '../../helpers/upload-client';
|
||
import { readFileSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import { BASE } from '../../helpers/env';
|
||
|
||
test.describe('Export — video streaming (P4)', () => {
|
||
test('a real video upload is streamed into Memories.zip (not dropped)', async ({ host }) => {
|
||
test.setTimeout(60_000);
|
||
const bearer = { Authorization: `Bearer ${host.jwt}` };
|
||
|
||
// Seed a real video upload owned by the host.
|
||
const mp4 = readFileSync(join(process.cwd(), 'fixtures', 'media', 'sample.mp4'));
|
||
const up = await uploadRaw(host.jwt, mp4, { filename: 'clip.mp4', contentType: 'video/mp4' });
|
||
expect(up.status).toBe(201);
|
||
const { id } = await up.json();
|
||
|
||
// Release the gallery → spawns the real zip + html export jobs.
|
||
const rel = await fetch(`${BASE}/api/v1/host/gallery/release`, {
|
||
method: 'POST',
|
||
headers: bearer,
|
||
});
|
||
expect(rel.status).toBe(204);
|
||
|
||
// Wait for the HTML (Memories.zip) job — that's the one whose media staging P4 changed.
|
||
await expect
|
||
.poll(
|
||
async () => {
|
||
const res = await fetch(`${BASE}/api/v1/export/status`, { headers: bearer });
|
||
return (await res.json()).html?.status;
|
||
},
|
||
{ timeout: 45_000, intervals: [500] }
|
||
)
|
||
.toBe('done');
|
||
|
||
// Download Memories.zip via the gated single-use ticket.
|
||
const ticketRes = await fetch(`${BASE}/api/v1/export/ticket`, {
|
||
method: 'POST',
|
||
headers: bearer,
|
||
});
|
||
const { ticket } = await ticketRes.json();
|
||
const dl = await fetch(`${BASE}/api/v1/export/html?ticket=${encodeURIComponent(ticket)}`);
|
||
expect(dl.status).toBe(200);
|
||
|
||
const bytes = new Uint8Array(await dl.arrayBuffer());
|
||
// Valid ZIP (PK local-file-header magic).
|
||
expect(Array.from(bytes.subarray(0, 2))).toEqual([0x50, 0x4b]);
|
||
|
||
// ZIP entry names are stored verbatim (uncompressed) in the archive, so the video's
|
||
// media entry name appears as plaintext bytes. Its presence means the streamed
|
||
// video entry was written; if the stream had failed, entry.close() would have
|
||
// errored and the job would never have reached 'done'.
|
||
const needle = `media/${id}.mp4`;
|
||
const haystack = new TextDecoder('latin1').decode(bytes);
|
||
expect(haystack.includes(needle), `Memories.zip must contain ${needle}`).toBe(true);
|
||
});
|
||
});
|