Ctrl K
ComunidadGuías…

e2e/local-pdf-compat.spec.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { test, expect, type Frame } from "@playwright/test";
import { createTestApi, loginAsDefault } from "./helpers";

const apiBase = process.env.NEXT_PUBLIC_API_URL || `http://localhost:${process.env.PORT || "8080"}`;
// The full browser includes the native PDF viewer; headless-shell does not.
test.use({ channel: "chromium" });

function twoPagePDF(): Buffer {
  const stream = (text: string) => {
    const body = `BT /F1 24 Tf 72 700 Td (${text}) Tj ET`;
    return `<< /Length ${body.length} >>\nstream\n${body}\nendstream`;
  };
  const objects = [
    "<< /Type /Catalog /Pages 2 0 R >>",
    "<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>",
    "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 7 0 R >> >> /Contents 4 0 R >>",
    stream("Attachment preview - page one"),
    "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 7 0 R >> >> /Contents 6 0 R >>",
    stream("Keep reading - page two"),
    "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
  ];
  let pdf = "%PDF-1.4\n";
  const offsets = [0];
  objects.forEach((object, index) => {
    offsets.push(Buffer.byteLength(pdf));
    pdf += `${index + 1} 0 obj\n${object}\nendobj\n`;
  });
  const xref = Buffer.byteLength(pdf);
  pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
  pdf += offsets.slice(1).map((offset) => `${String(offset).padStart(10, "0")} 00000 n \n`).join("");
  pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
  return Buffer.from(pdf);
}

test("cross-origin local PDF keeps its reading position past capability TTL", 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");
  test.skip(new URL(baseURL!).origin === new URL(apiBase).origin, "Cross-origin PDF path only");
  const api = await createTestApi();
  try {
    const slug = await loginAsDefault(page);
    const issue = await api.createIssue("Local PDF reading position");
    const form = new FormData();
    form.append("file", new Blob([twoPagePDF()], { type: "application/pdf" }), "reading.pdf");
    form.append("issue_id", issue.id);
    const uploaded = await fetch(`${apiBase}/api/upload-file`, {
      method: "POST", headers: { Authorization: `Bearer ${api.getToken()}`, "X-Workspace-Slug": slug }, body: form,
    });
    expect(uploaded.status).toBe(200);
    const att = await uploaded.json();
    await page.goto(`/${slug}/attachments/${att.id}/preview`);
    const frame = page.locator('iframe[title="reading.pdf"]');
    await expect(frame).toHaveAttribute("src", /^blob:/);
    let viewerFrame: Frame | undefined;
    await expect.poll(async () => {
      for (const child of page.frames()) {
        if (await child.locator("pdf-viewer").count()) { viewerFrame = child; return true; }
      }
      return false;
    }).toBe(true);
    const pageNumber = viewerFrame!.getByRole("textbox", { name: "Page number" });
    await pageNumber.fill("2");
    await pageNumber.press("Enter");
    await expect(pageNumber).toHaveValue("2");
    const source = await frame.getAttribute("src");
    const timeOrigin = await viewerFrame!.evaluate(() => performance.timeOrigin);
    const before = await page.screenshot({ path: testInfo.outputPath("pdf-page-two-before.png") });
    await testInfo.attach("pdf-page-two-before", { body: before, contentType: "image/png" });
    const openedAt = Date.now();
    await expect.poll(() => Date.now(), { timeout: 70000 }).toBeGreaterThan(openedAt + 65000);
    await page.evaluate(() => document.dispatchEvent(new Event("visibilitychange")));
    expect(await frame.getAttribute("src")).toBe(source);
    await expect(pageNumber).toHaveValue("2");
    expect(await viewerFrame!.evaluate(() => performance.timeOrigin)).toBe(timeOrigin);
    const after = await page.screenshot({ path: testInfo.outputPath("pdf-page-two-after.png") });
    await testInfo.attach("pdf-page-two-after-65s", { body: after, contentType: "image/png" });
  } finally { await api.cleanup(); }
});

for (const [mode, inlineURL] of [
  ["presign", "https://storage.example.test/reading.pdf?X-Amz-Signature=fresh"],
  ["CloudFront", "https://cdn.example.test/reading.pdf?Signature=fresh&Key-Pair-Id=test"],
] as const) {
  test(`same-origin ${mode} PDF opens inline instead of downloading`, async ({ page, baseURL }, testInfo) => {
    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");
    test.skip(new URL(baseURL!).origin !== new URL(apiBase).origin, "Run with a same-origin Web API base");
    const api = await createTestApi();
    try {
      const slug = await loginAsDefault(page);
      const issue = await api.createIssue(`Same-origin ${mode} PDF preview`);
      const pdf = twoPagePDF();
      const form = new FormData();
      form.append("file", new Blob([pdf], { type: "application/pdf" }), "reading.pdf");
      form.append("issue_id", issue.id);
      const uploaded = await fetch(`${apiBase}/api/upload-file`, {
        method: "POST", headers: { Authorization: `Bearer ${api.getToken()}`, "X-Workspace-Slug": slug }, body: form,
      });
      expect(uploaded.status).toBe(200);
      const att = await uploaded.json();
      let forcedDownloads = 0;
      const downloads: string[] = [];
      page.on("download", (download) => downloads.push(download.suggestedFilename()));
      // Model the production storage contracts without AWS credentials: metadata
      // offers an inline signature, while the durable download route forces save.
      await page.route(`${apiBase}/api/attachments/${att.id}`, (route) => route.fulfill({
        json: { ...att, download_url: inlineURL, attachment_download_url: `${inlineURL}&response-content-disposition=attachment` },
      }));
      await page.route(`${apiBase}/api/attachments/${att.id}/download`, (route) => {
        forcedDownloads++;
        return route.fulfill({ status: 302, headers: { Location: `${inlineURL}&response-content-disposition=attachment` } });
      });
      await page.route(`${inlineURL}&response-content-disposition=attachment`, (route) => route.fulfill({
        contentType: "application/pdf", headers: { "Content-Disposition": "attachment; filename=reading.pdf" }, body: pdf,
      }));
      await page.route(inlineURL, (route) => route.fulfill({
        contentType: "application/pdf", headers: { "Content-Disposition": "inline; filename=reading.pdf" }, body: pdf,
      }));
      await page.goto(`/${slug}/attachments/${att.id}/preview`);
      await expect(page.locator('iframe[title="reading.pdf"]')).toHaveAttribute("src", inlineURL);
      let viewerFrame: Frame | undefined;
      await expect.poll(async () => {
        for (const child of page.frames()) {
          if (await child.locator("pdf-viewer").count()) { viewerFrame = child; return true; }
        }
        return false;
      }).toBe(true);
      const pageNumber = viewerFrame!.getByRole("textbox", { name: "Page number" });
      await pageNumber.fill("2");
      await pageNumber.press("Enter");
      await expect(pageNumber).toHaveValue("2");
      expect(forcedDownloads).toBe(0);
      expect(downloads).toEqual([]);
      await page.screenshot({ path: testInfo.outputPath(`same-origin-${mode}-pdf.png`) });
    } finally { await api.cleanup(); }
  });
}