// The signing tool keeps no signature on the device (bau-pm PLAN.md ยง2.77, // Gitea #1): PDF.js's signature editor saves signatures in localStorage for // this origin, not for a bau-pm user, so on a shared tablet the next person // would find the last one's signature one click away. // // node test/signatures.mjs [chromium-path] // // 1. A device that already holds a saved signature (written the way an older // image left it, from a page the glue does not run on) is offered none, // and the store is empty afterwards. // 2. In the signing dialog the "save" box and the image tab are hidden, and a // signature typed and added with the dialog as it opens is not stored. // 3. A fresh page on the same browser, the next person, is offered none. // // Exits 0 when nothing was kept, 1 otherwise, 2 when it could not run. Fed an // image without the glue's fifth duty (v0.1.1 or older) it must fail. import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); const base = (process.argv[2] || '').replace(/\/$/, ''); const executablePath = process.argv[3] || process.env.CHROMIUM || '/usr/bin/chromium'; if (!base) { console.error('usage: signatures.mjs [chromium]'); process.exit(2); } const { chromium } = await import(process.env.PLAYWRIGHT_CORE || 'playwright-core'); const KEY = 'pdfjs.signature'; const failures = []; const browser = await chromium.launch({ executablePath, args: ['--no-sandbox', `--unsafely-treat-insecure-origin-as-secure=${new URL(base).origin}`], }); // Wide enough for the viewer's toolbar to show the signature button. const context = await browser.newContext({ viewport: { width: 1280, height: 900 } }); async function viewer(page) { await page.goto(`${base}/de/sign-pdf`, { waitUntil: 'networkidle' }); await page.setInputFiles('#file-input', join(here, 'fixtures/probe.pdf')); const frame = await (await page.waitForSelector('iframe', { timeout: 60000 })).contentFrame(); await frame.waitForSelector('#editorSignatureButton:not([disabled])', { timeout: 60000 }); return frame; } const stored = (frame) => frame.evaluate((key) => window.localStorage.getItem(key), KEY); // The saved signatures the toolbar's menu offers, read from the menu itself. async function offered(frame) { await frame.click('#editorSignatureButton'); await frame.waitForSelector('#editorSignatureAddSignature', { state: 'visible', timeout: 10000 }); const titles = await frame.$$eval('#addSignatureDoorHanger .toolbarAddSignatureButtonContainer', (rows) => rows.map((row) => row.querySelector('button')?.getAttribute('title') || row.textContent.trim())); await frame.click('#editorSignatureButton'); return titles; } const shown = (frame, selector) => frame.$eval(selector, (e) => e.getClientRects().length > 0 && getComputedStyle(e).display !== 'none'); try { // --- 1. a device that saved one before -------------------------------------- // config.json is served without the glue, so this write is the browser's own. // The entry is a real one, typed in v0.1.0's dialog and read back from its // store: a made-up entry might simply fail to render in the menu, and the // check would pass on an image that still offers saved signatures. const seed = await context.newPage(); await seed.goto(`${base}/config.json`); await seed.evaluate(([key, entry]) => window.localStorage.setItem(key, JSON.stringify({ '00000000-0000-4000-8000-000000000000': JSON.parse(entry), })), [KEY, readFileSync(join(here, 'fixtures/saved-signature.json'), 'utf8')]); await seed.close(); const a = await context.newPage(); let frame = await viewer(a); const before = await offered(frame); if (before.length) failures.push(`a signature saved before is still offered: ${before.join(', ')}`); if (await stored(frame)) failures.push('a signature saved before is still stored'); // --- 2. signing -------------------------------------------------------------- // The toolbar button switches the signature mode on; the dialog's controls // are clicked through the DOM, because the dialog is taller than the frame. await frame.click('#editorSignatureButton'); await frame.$eval('#editorSignatureAddSignature', (e) => e.click()); await frame.waitForSelector('#addSignatureDialog[open]', { timeout: 10000 }); if (await shown(frame, '#addSignatureSaveContainer')) failures.push('the dialog offers "save signature"'); if (await shown(frame, '#addSignatureImageButton')) failures.push('the dialog offers the image tab'); if (!(await frame.$eval('#addSignatureFilePicker', (e) => e.disabled))) failures.push('the image file picker is enabled'); if (await frame.$eval('#addSignatureSaveCheckbox', (e) => e.checked)) failures.push('the "save" box reads ticked'); await frame.$eval('#addSignatureTypeInput', (e) => { e.value = 'Max Muster'; e.dispatchEvent(new Event('input', { bubbles: true })); }); await frame.waitForSelector('#addSignatureAddButton:not([disabled])', { state: 'attached', timeout: 10000 }); await frame.$eval('#addSignatureAddButton', (e) => e.click()); await frame.waitForSelector('#addSignatureDialog[open]', { state: 'detached', timeout: 10000 }).catch(() => {}); const placed = await frame.$$eval('.signatureEditor', (e) => e.length); if (placed === 0) failures.push('the typed signature was not placed (the check below would prove nothing)'); if (await stored(frame)) failures.push('the signature just added was stored'); await a.close(); // --- 3. the next person ------------------------------------------------------ const b = await context.newPage(); frame = await viewer(b); const after = await offered(frame); if (after.length) failures.push(`the next page is offered: ${after.join(', ')}`); if (await stored(frame)) failures.push('the next page finds a stored signature'); await b.close(); } catch (e) { console.error(`signatures: could not run: ${e.message}`); await browser.close(); process.exit(2); } await browser.close(); if (failures.length) { console.error(`signatures: ${failures.length} finding(s)`); for (const f of failures) console.error(` - ${f}`); process.exit(1); } console.log('signatures: nothing kept, nothing offered, no image tab');