mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
import path from "node:path";
|
|||
|
|
import type { Page } from "@playwright/test";
|
||
|
|
import { expect, test } from "./helpers";
|
||
|
|
|
||
|
|
const PDF_FIXTURE = path.join(
|
||
|
|
process.cwd(),
|
||
|
|
"tests",
|
||
|
|
"fixtures",
|
||
|
|
"document",
|
||
|
|
"valid",
|
||
|
|
"test-3page.pdf",
|
||
|
|
);
|
||
|
|
|
||
|
|
// Reuse an existing small PNG as the uploaded signature image. Any PNG works
|
||
|
|
// here; the pad's Upload tab accepts image/png,image/jpeg.
|
||
|
|
const SIGNATURE_PNG = path.join(
|
||
|
|
process.cwd(),
|
||
|
|
"tests",
|
||
|
|
"fixtures",
|
||
|
|
"image",
|
||
|
|
"valid",
|
||
|
|
"test-200x150.png",
|
||
|
|
);
|
||
|
|
|
||
|
|
// The PDF dropzone opens a native file chooser on click rather than exposing a
|
||
|
|
// persistent <input type="file">, so upload through the chooser event (matches
|
||
|
|
// pdf-to-image.spec.ts / document-mode.spec.ts).
|
||
|
|
async function uploadPdf(page: Page): Promise<void> {
|
||
|
|
const fileChooserPromise = page.waitForEvent("filechooser");
|
||
|
|
const uploadButton = page.getByRole("button", { name: /upload from computer/i }).first();
|
||
|
|
if (await uploadButton.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||
|
|
await uploadButton.click();
|
||
|
|
} else {
|
||
|
|
await page.locator("[class*='border-dashed']").first().click();
|
||
|
|
}
|
||
|
|
const fileChooser = await fileChooserPromise;
|
||
|
|
await fileChooser.setFiles(PDF_FIXTURE);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Open the signature pad, upload a PNG on the Upload tab, and place it. The pad
|
||
|
|
// drops the signature at page center on "Save & place" (no Konva drag needed).
|
||
|
|
// This whole path is pure client-side until the user clicks Apply & Download.
|
||
|
|
async function placeUploadedSignature(page: Page): Promise<void> {
|
||
|
|
await page.getByRole("button", { name: "+ New" }).click();
|
||
|
|
const dialog = page.getByRole("dialog");
|
||
|
|
await expect(dialog).toBeVisible();
|
||
|
|
await dialog.getByRole("button", { name: "Upload", exact: true }).click();
|
||
|
|
// The hidden file input only mounts once the Upload tab is active. Scope it to
|
||
|
|
// the dialog so it never collides with the tool dropzone's chooser.
|
||
|
|
await dialog.locator('input[accept="image/png,image/jpeg"]').setInputFiles(SIGNATURE_PNG);
|
||
|
|
const save = dialog.getByRole("button", { name: "Save & place" });
|
||
|
|
// The image is read via FileReader, so the button enables a tick after upload.
|
||
|
|
await expect(save).toBeEnabled();
|
||
|
|
await save.click();
|
||
|
|
// The pad closes once the signature is placed onto the canvas.
|
||
|
|
await expect(dialog).toBeHidden();
|
||
|
|
}
|
||
|
|
|
||
|
|
test.describe("Sign PDF tool", () => {
|
||
|
|
test("places a signature on a PDF (interactive flow)", async ({ loggedInPage: page }) => {
|
||
|
|
await page.goto("/pdf/sign-pdf");
|
||
|
|
await uploadPdf(page);
|
||
|
|
|
||
|
|
// pdf.js renders the uploaded page into the sign canvas.
|
||
|
|
await expect(page.getByTestId("sign-pdf-canvas")).toBeVisible({ timeout: 15_000 });
|
||
|
|
|
||
|
|
// Apply is gated on at least one placement, so it starts disabled.
|
||
|
|
const apply = page.getByRole("button", { name: /Apply & Download/ });
|
||
|
|
await expect(apply).toBeDisabled();
|
||
|
|
|
||
|
|
await placeUploadedSignature(page);
|
||
|
|
|
||
|
|
// A placement now exists -> Apply becomes enabled. addSignature loads the
|
||
|
|
// image asynchronously and only then bumps the count, so toBeEnabled
|
||
|
|
// auto-retries until the count propagates to the settings panel.
|
||
|
|
await expect(apply).toBeEnabled();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Stamping calls the Python sidecar (doc_sign, which needs PyMuPDF). The e2e
|
||
|
|
// webServer boots the API with the system Python, which has no PyMuPDF here
|
||
|
|
// (and PyMuPDF-less CI shards exist too), so the actual stamp + signed-PDF
|
||
|
|
// download cannot succeed in those environments. The integration test
|
||
|
|
// (tests/integration/sign-pdf.test.ts) already covers stamping where PyMuPDF
|
||
|
|
// is present. Gate this on an explicit flag so the default suite never depends
|
||
|
|
// on PyMuPDF.
|
||
|
|
test("stamps the PDF and offers a signed download", async ({ loggedInPage: page }) => {
|
||
|
|
test.skip(
|
||
|
|
!process.env.SIGN_PDF_E2E_STAMP,
|
||
|
|
"requires PyMuPDF in the API (set SIGN_PDF_E2E_STAMP=1)",
|
||
|
|
);
|
||
|
|
|
||
|
|
await page.goto("/pdf/sign-pdf");
|
||
|
|
await uploadPdf(page);
|
||
|
|
await expect(page.getByTestId("sign-pdf-canvas")).toBeVisible({ timeout: 15_000 });
|
||
|
|
await placeUploadedSignature(page);
|
||
|
|
|
||
|
|
await page.getByRole("button", { name: /Apply & Download/ }).click();
|
||
|
|
await expect(page.getByRole("link", { name: /Download signed PDF/ })).toBeVisible({
|
||
|
|
timeout: 60_000,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|