e2e/local-stream-compat.spec.ts
Rama main · réplica de lectura del repositorio original.
import { test, expect } from "@playwright/test";
import { createTestApi, loginAsDefault } from "./helpers";
const apiBase = process.env.NEXT_PUBLIC_API_URL || `http://localhost:${process.env.PORT || "8080"}`;
test("native local audio survives capability renewal", async ({ page, baseURL }, testInfo) => {
test.setTimeout(120000);
const local = (url: string) => ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname);
test.skip(!local(baseURL!) || !local(apiBase) || !!process.env.S3_BUCKET, "Isolated LocalStorage fixtures only");
const api = await createTestApi();
try {
const slug = await loginAsDefault(page);
const issue = await api.createIssue("Local streamed attachment");
const bytes = Buffer.alloc(44 + 8000 * 2 * 120);
bytes.write("RIFF", 0); bytes.writeUInt32LE(bytes.length - 8, 4);
bytes.write("WAVEfmt ", 8); bytes.writeUInt32LE(16, 16);
bytes.writeUInt16LE(1, 20); bytes.writeUInt16LE(1, 22);
bytes.writeUInt32LE(8000, 24); bytes.writeUInt32LE(16000, 28);
bytes.writeUInt16LE(2, 32); bytes.writeUInt16LE(16, 34);
bytes.write("data", 36); bytes.writeUInt32LE(bytes.length - 44, 40);
const form = new FormData();
form.append("file", new Blob([bytes], { type: "audio/wav" }), "renewal.wav");
form.append("issue_id", issue.id);
const headers = { Authorization: `Bearer ${api.getToken()}`, "X-Workspace-Slug": slug };
const uploaded = await fetch(`${apiBase}/api/upload-file`, { method: "POST", headers, body: form });
expect(uploaded.status).toBe(200);
const att = await uploaded.json();
await page.goto(`/${slug}/attachments/${att.id}/preview`);
const audio = page.locator("audio");
await expect(audio).toBeVisible();
await expect.poll(() => audio.evaluate((el: HTMLAudioElement) => el.readyState)).toBeGreaterThan(0);
const first = (await audio.getAttribute("src"))!;
expect(first).toContain("/signed-download?");
const expires = Number(new URL(first).searchParams.get("exp"));
expect(expires * 1000 - Date.now()).toBeLessThanOrEqual(60000);
await page.locator("body").click({ position: { x: 10, y: 100 } });
await audio.evaluate(async (el: HTMLAudioElement) => { el.currentTime = 12; await el.play(); });
// A loaded stream must not be replaced just because its capability expires.
await expect.poll(() => Date.now(), { timeout: 70000 }).toBeGreaterThan((expires + 2) * 1000);
await page.evaluate(() => document.dispatchEvent(new Event("visibilitychange")));
expect(await audio.getAttribute("src")).toBe(first);
expect(await audio.evaluate((el: HTMLAudioElement) => el.currentTime)).toBeGreaterThan(60);
expect((await fetch(first)).status).toBe(403);
// A late native error renews on demand. The first metadata request fails
// to cover bounded recovery from a transient network outage as well.
let outage = true;
await page.route(`**/api/attachments/${att.id}`, async (route) => {
if (outage) { outage = false; await route.abort("failed"); }
else await route.continue();
});
const position = await audio.evaluate((el: HTMLAudioElement) => el.currentTime);
await audio.dispatchEvent("error");
await expect.poll(() => audio.getAttribute("src"), { timeout: 10000 }).not.toBe(first);
await expect.poll(() => audio.evaluate((el: HTMLAudioElement) => el.readyState)).toBeGreaterThan(0);
expect(await audio.evaluate((el: HTMLAudioElement) => el.currentTime)).toBeGreaterThanOrEqual(position - 1);
expect(await audio.evaluate((el: HTMLAudioElement) => el.paused)).toBe(false);
await testInfo.attach("stream-after-renewal", { body: await page.screenshot(), contentType: "image/png" });
} finally { await api.cleanup(); }
});