form-tester 0.11.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/form-tester.js +136 -133
  2. package/package.json +1 -1
package/form-tester.js CHANGED
@@ -7,7 +7,7 @@ const { spawn, execSync } = require("child_process");
7
7
  const CONFIG_PATH = path.join(process.cwd(), "form-tester.config.json");
8
8
  const OUTPUT_BASE = path.resolve(process.cwd(), "output");
9
9
  const ISSUES_PATH = path.join(OUTPUT_BASE, "issues.jsonl");
10
- const LOCAL_VERSION = "0.11.4";
10
+ const LOCAL_VERSION = "0.12.0";
11
11
  const RECOMMENDED_PERSON = "Uromantisk Direktør";
12
12
 
13
13
  // Recording — persisted to disk so `form-tester exec` can append across processes
@@ -139,17 +139,39 @@ function formatIssue(issue) {
139
139
  return `[${time}] [${issue.category}]${formId}${url}\n ${issue.message}`;
140
140
  }
141
141
 
142
+ // Helper: find a person button ref inside the person picker region of a snapshot
143
+ function findPersonRefInSnapshot(snapshotText, personName) {
144
+ const lines = snapshotText.split(/\r?\n/);
145
+ let inRegion = false;
146
+ let firstRef = null;
147
+ for (const line of lines) {
148
+ if (line.includes('region "Hvem vil du bruke Helsenorge')) inRegion = true;
149
+ if (!inRegion) continue;
150
+ const btnMatch = line.match(/button "([^"]*)" \[ref=(e\d+)\]/);
151
+ if (btnMatch) {
152
+ if (!firstRef) firstRef = btnMatch[2];
153
+ if (btnMatch[1].includes(personName)) return btnMatch[2];
154
+ }
155
+ }
156
+ return firstRef; // fallback: first button in region
157
+ }
158
+
159
+ // Helper: detect what page we're on from a snapshot
160
+ function detectPage(snapshotText) {
161
+ if (!snapshotText) return "unknown";
162
+ if (snapshotText.includes("Hvem vil du bruke Helsenorge")) return "person-picker";
163
+ if (snapshotText.includes("cookie") || snapshotText.includes("informasjonskapsler")) return "cookies";
164
+ if (snapshotText.includes('main "Dokumenter"') || snapshotText.includes("Se detaljer")) return "document-list";
165
+ if (snapshotText.includes("Åpne dokumentet")) return "document-detail";
166
+ if (/href.*\/pdf\//i.test(snapshotText) || /blob:/i.test(snapshotText) || /\.pdf/i.test(snapshotText)) return "document-pdf";
167
+ if (snapshotText.length > 2000) return "document-html"; // substantial content, likely the doc
168
+ return "unknown";
169
+ }
170
+
142
171
  async function handleDocuments(config, flags = {}) {
143
172
  const v = flags.verbosity || "normal";
144
173
  const log = (msg) => { if (v !== "silent") console.log(msg); };
145
174
 
146
- const dokumenterUrl = resolveDokumenterUrl(config);
147
- if (!dokumenterUrl) {
148
- console.error("No Dokumenter URL available. Set pnr in config or URL.");
149
- logIssue("documents", "No Dokumenter URL — missing PNR", { url: config.lastTestUrl });
150
- return 1;
151
- }
152
-
153
175
  const outputDir = config.lastRunDir;
154
176
  if (!outputDir) {
155
177
  console.error("No output folder. Run a test first.");
@@ -157,150 +179,131 @@ async function handleDocuments(config, flags = {}) {
157
179
  }
158
180
  fs.mkdirSync(outputDir, { recursive: true });
159
181
 
160
- // Step 1: Navigate to Dokumenter
161
- log(`Navigating to Dokumenter: ${dokumenterUrl}`);
162
- let code = await runPlaywrightCli(["goto", dokumenterUrl]);
163
- if (code !== 0) {
164
- logIssue("documents", `Failed to navigate to Dokumenter (exit ${code})`, { url: dokumenterUrl });
165
- console.error("Failed to navigate to Dokumenter.");
166
- return code;
167
- }
168
- await sleep(2000);
169
-
170
- // Step 2: Check if person selection is needed — parse snapshot for person buttons
171
- const checkSnapshot = path.join(outputDir, "dokumenter_check.yml");
172
- await runPlaywrightCli(["snapshot", "--filename", checkSnapshot]);
173
- if (fs.existsSync(checkSnapshot)) {
174
- const checkText = fs.readFileSync(checkSnapshot, "utf8");
175
- if (checkText.includes("Hvem vil du bruke Helsenorge") || checkText.includes("personliste")) {
176
- log("Person selection detected on Dokumenter page.");
177
- const personName = config.lastPerson || config.person || RECOMMENDED_PERSON;
178
- // Find the button ref directly from the snapshot text
179
- let clickRef = null;
180
- const lines = checkText.split(/\r?\n/);
181
- for (const line of lines) {
182
- const btnMatch = line.match(/button "([^"]*)" \[ref=(e\d+)\]/);
183
- if (btnMatch && btnMatch[1].includes(personName)) {
184
- clickRef = btnMatch[2];
185
- log(`Found person button: "${btnMatch[1]}" (ref=${clickRef})`);
186
- break;
187
- }
182
+ const personName = config.lastPerson || config.person || RECOMMENDED_PERSON;
183
+ const maxSteps = 8; // safety limit
184
+
185
+ for (let step = 0; step < maxSteps; step++) {
186
+ // Take snapshot to detect current page state
187
+ const snapPath = path.join(outputDir, `doc_step_${step}.yml`);
188
+ await runPlaywrightCli(["snapshot", "--filename", snapPath]);
189
+ const snapText = fs.existsSync(snapPath) ? fs.readFileSync(snapPath, "utf8") : "";
190
+ const page = detectPage(snapText);
191
+ log(`Step ${step + 1}: detected page = ${page}`);
192
+
193
+ if (page === "cookies") {
194
+ log("Dismissing cookies...");
195
+ await handleCookies();
196
+ await sleep(1000);
197
+ continue;
198
+ }
199
+
200
+ if (page === "person-picker") {
201
+ log("Selecting person...");
202
+ const ref = findPersonRefInSnapshot(snapText, personName);
203
+ if (ref) {
204
+ await runPlaywrightCli(["click", ref]);
205
+ log(`Clicked person button ref=${ref}`);
206
+ } else {
207
+ log("No person button found in snapshot.");
208
+ logIssue("person-selection", "No person button found in Dokumenter snapshot");
188
209
  }
189
- // Fallback: find first button inside the person list region
190
- if (!clickRef) {
191
- let inRegion = false;
192
- for (const line of lines) {
193
- if (line.includes('region "Hvem vil du bruke Helsenorge')) inRegion = true;
194
- if (inRegion) {
195
- const btnMatch = line.match(/button "[^"]*" \[ref=(e\d+)\]/);
196
- if (btnMatch) {
197
- clickRef = btnMatch[1];
198
- log(`Fallback: clicking first person button (ref=${clickRef})`);
199
- break;
200
- }
201
- }
202
- }
210
+ await sleep(3000);
211
+ continue;
212
+ }
213
+
214
+ if (page === "document-list") {
215
+ // Save the document list
216
+ fs.copyFileSync(snapPath, path.join(outputDir, "dokumenter.yml"));
217
+ await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "dokumenter.png"), "--full-page"]);
218
+ log("Saved: dokumenter.yml + dokumenter.png");
219
+
220
+ // Click "Se detaljer" on first document — find it in the snapshot
221
+ const lines = snapText.split(/\r?\n/);
222
+ let detailRef = null;
223
+ for (const line of lines) {
224
+ const match = line.match(/button "Se detaljer" \[ref=(e\d+)\]/);
225
+ if (match) { detailRef = match[1]; break; }
203
226
  }
204
- if (clickRef) {
205
- await runPlaywrightCli(["click", clickRef]);
206
- log("Person selected on Dokumenter page.");
227
+ if (detailRef) {
228
+ await runPlaywrightCli(["click", detailRef]);
229
+ log(`Clicked 'Se detaljer' (ref=${detailRef})`);
207
230
  } else {
208
- log("Could not find person button in snapshot. Trying select-person...");
209
- await handleSelectPerson(config, personName);
231
+ log("Could not find 'Se detaljer' in snapshot.");
232
+ logIssue("documents", "No 'Se detaljer' button found in document list");
210
233
  }
211
234
  await sleep(2000);
235
+ continue;
212
236
  }
213
- try { fs.unlinkSync(checkSnapshot); } catch (e) {}
214
- }
215
-
216
- // Step 3: Take snapshot of document list
217
- const docListSnapshot = path.join(outputDir, "dokumenter.yml");
218
- await runPlaywrightCli(["snapshot", "--filename", docListSnapshot]);
219
- await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "dokumenter.png"), "--full-page"]);
220
- log("Saved: dokumenter.yml + dokumenter.png");
221
-
222
- // Step 4: Click first document's "Se detaljer"
223
- log("Looking for first document...");
224
- const clickResult = await runPlaywrightCliCapture(["eval", '() => { const link = document.querySelector("a[href*=\\"detaljer\\"], button:has-text(\\"Se detaljer\\"), a:has-text(\\"Se detaljer\\")"); if (link) { link.click(); return "clicked"; } return "not-found"; }']);
225
- if (clickResult.stdout.includes("not-found")) {
226
- logIssue("documents", "Could not find 'Se detaljer' link on Dokumenter page", { url: dokumenterUrl });
227
- log("Could not find 'Se detaljer'. Check dokumenter.yml for the page structure.");
228
- // Still continue — agent can handle manually
229
- } else {
230
- log("Clicked 'Se detaljer' on first document.");
231
- await sleep(2000);
232
- }
233
237
 
234
- // Step 4: Click "Åpne dokumentet"
235
- const openResult = await runPlaywrightCliCapture(["eval", '() => { const link = document.querySelector("a:has-text(\\"Åpne dokumentet\\"), button:has-text(\\"Åpne dokumentet\\")"); if (link) { link.click(); return "clicked"; } return "not-found"; }']);
236
- if (openResult.stdout.includes("not-found")) {
237
- logIssue("documents", "Could not find 'Åpne dokumentet' link", { url: dokumenterUrl });
238
- log("Could not find 'Åpne dokumentet'. Take a snapshot to inspect the page.");
239
- } else {
240
- log("Clicked 'Åpne dokumentet'.");
241
- await sleep(3000);
242
- }
243
-
244
- // Step 5: Detect format (PDF vs HTML)
245
- const docSnapshot = path.join(outputDir, "document.yml");
246
- await runPlaywrightCli(["snapshot", "--filename", docSnapshot]);
247
-
248
- let format = "unknown";
249
- if (fs.existsSync(docSnapshot)) {
250
- const snapshotText = fs.readFileSync(docSnapshot, "utf8");
251
- if (/href.*\/pdf\//i.test(snapshotText) || /blob:/i.test(snapshotText) || /\.pdf/i.test(snapshotText)) {
252
- format = "pdf";
253
- } else if (snapshotText.length > 500) {
254
- // Has substantial content — likely HTML
255
- format = "html";
238
+ if (page === "document-detail") {
239
+ // Click "Åpne dokumentet"
240
+ const lines = snapText.split(/\r?\n/);
241
+ let openRef = null;
242
+ for (const line of lines) {
243
+ const match = line.match(/(?:button|link) "([^"]*Åpne dokumentet[^"]*)" \[ref=(e\d+)\]/);
244
+ if (match) { openRef = match[2]; break; }
245
+ }
246
+ if (openRef) {
247
+ await runPlaywrightCli(["click", openRef]);
248
+ log(`Clicked 'Åpne dokumentet' (ref=${openRef})`);
249
+ } else {
250
+ log("Could not find 'Åpne dokumentet' in snapshot.");
251
+ logIssue("documents", "No 'Åpne dokumentet' button found in document detail");
252
+ }
253
+ await sleep(3000);
254
+ continue;
256
255
  }
257
- }
258
- log(`Detected document format: ${format}`);
259
256
 
260
- // Step 6: Capture based on format
261
- if (format === "pdf") {
262
- log("Attempting PDF download...");
263
- // Try direct PDF link first
264
- const dlCode = await runPlaywrightCli(["run-code", `async page => { const link = page.locator('a[href*="/pdf/"]').first(); const count = await link.count(); if (count > 0) { const [download] = await Promise.all([ page.waitForEvent('download'), link.click() ]); await download.saveAs('${outputDir.replace(/\\/g, "/")}/document.pdf'); return; } const lastNed = page.getByRole('link', { name: 'Last ned' }); const lastNedCount = await lastNed.count(); if (lastNedCount > 0) { const [download] = await Promise.all([ page.waitForEvent('download'), lastNed.click() ]); await download.saveAs('${outputDir.replace(/\\/g, "/")}/document.pdf'); return; } throw new Error('No PDF link found'); }`]);
265
- if (dlCode !== 0) {
266
- logIssue("pdf-download", "PDF download failed", { url: dokumenterUrl, outputDir });
267
- log("PDF download failed. Check document.yml for available links.");
268
- } else {
269
- const pdfPath = path.join(outputDir, "document.pdf");
270
- if (fs.existsSync(pdfPath)) {
271
- log(`PDF saved: ${pdfPath}`);
257
+ if (page === "document-pdf") {
258
+ log("PDF document detected. Downloading...");
259
+ fs.copyFileSync(snapPath, path.join(outputDir, "document.yml"));
260
+ const dlCode = await runPlaywrightCli(["run-code", `async page => { const link = page.locator('a[href*="/pdf/"]').first(); const count = await link.count(); if (count > 0) { const [download] = await Promise.all([ page.waitForEvent('download'), link.click() ]); await download.saveAs('${outputDir.replace(/\\/g, "/")}/document.pdf'); return; } const lastNed = page.getByRole('link', { name: 'Last ned' }); const lastNedCount = await lastNed.count(); if (lastNedCount > 0) { const [download] = await Promise.all([ page.waitForEvent('download'), lastNed.click() ]); await download.saveAs('${outputDir.replace(/\\/g, "/")}/document.pdf'); return; } throw new Error('No PDF link found'); }`]);
261
+ if (dlCode === 0 && fs.existsSync(path.join(outputDir, "document.pdf"))) {
262
+ log(`PDF saved: ${path.join(outputDir, "document.pdf")}`);
272
263
  } else {
273
- logIssue("pdf-download", "PDF download command succeeded but file not found", { outputDir });
274
- log("PDF download command ran but file not found.");
264
+ logIssue("pdf-download", "PDF download failed", { outputDir });
265
+ log("PDF download failed.");
275
266
  }
267
+ log(`\nDocument verification complete. Format: pdf`);
268
+ log(`Output: ${outputDir}`);
269
+ return 0;
276
270
  }
277
- } else if (format === "html") {
278
- log("Capturing HTML document...");
279
- // Screenshot
280
- const ssCode = await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "document_screenshot.png"), "--full-page"]);
281
- if (ssCode !== 0) {
282
- logIssue("html-capture", "Full-page screenshot failed (possible PDF misdetected as HTML)", { outputDir });
283
- log("Screenshot failed — document might be PDF. Trying download...");
284
- format = "pdf";
285
- } else {
271
+
272
+ if (page === "document-html") {
273
+ log("HTML document detected. Capturing...");
274
+ fs.copyFileSync(snapPath, path.join(outputDir, "document.yml"));
275
+ await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "document_screenshot.png"), "--full-page"]);
286
276
  log("Saved: document_screenshot.png (full-page)");
277
+ const htmlResult = await runPlaywrightCliCapture(["eval", "document.documentElement.outerHTML"]);
278
+ if (htmlResult.code === 0 && htmlResult.stdout) {
279
+ fs.writeFileSync(path.join(outputDir, "document.html"), htmlResult.stdout.replace(/^### Result\s*/i, ""));
280
+ log("Saved: document.html");
281
+ }
282
+ log(`\nDocument verification complete. Format: html`);
283
+ log(`Output: ${outputDir}`);
284
+ return 0;
287
285
  }
288
- // Save raw HTML
289
- const htmlResult = await runPlaywrightCliCapture(["eval", "document.documentElement.outerHTML"]);
290
- if (htmlResult.code === 0 && htmlResult.stdout) {
291
- const htmlContent = htmlResult.stdout.replace(/^### Result\s*/i, "");
292
- fs.writeFileSync(path.join(outputDir, "document.html"), htmlContent);
293
- log("Saved: document.html");
286
+
287
+ // Unknown page if this is step 0, try navigating to Dokumenter
288
+ if (step === 0) {
289
+ const dokumenterUrl = resolveDokumenterUrl(config);
290
+ if (dokumenterUrl) {
291
+ log(`Unknown page state. Navigating to Dokumenter: ${dokumenterUrl}`);
292
+ await runPlaywrightCli(["goto", dokumenterUrl]);
293
+ await sleep(2000);
294
+ continue;
295
+ }
294
296
  }
295
- } else {
296
- logIssue("documents", `Unknown document formatcould not detect PDF or HTML`, { outputDir });
297
- log("Could not determine format. Taking snapshot and screenshot for manual review.");
297
+
298
+ // Still unknown after navigation give up
299
+ log(`Could not determine page state. Taking screenshot for manual review.`);
298
300
  await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "document_screenshot.png"), "--full-page"]);
301
+ logIssue("documents", `Stuck on unknown page at step ${step + 1}`, { outputDir });
302
+ break;
299
303
  }
300
304
 
301
- log(`\nDocument verification complete. Format: ${format}`);
302
305
  log(`Output: ${outputDir}`);
303
- return 0;
306
+ return 1;
304
307
  }
305
308
 
306
309
  // --- Standardized commands: cookies, select-person, validate ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-tester",
3
- "version": "0.11.4",
3
+ "version": "0.12.0",
4
4
  "description": "AI-powered form testing skill for /skjemautfyller forms using Playwright CLI. Works with Claude Code and GitHub Copilot.",
5
5
  "main": "form-tester.js",
6
6
  "bin": {