form-tester 0.11.5 → 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 +137 -185
  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.5";
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,201 +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 INSIDE the person list region (not the nav header)
179
- let clickRef = null;
180
- const lines = checkText.split(/\r?\n/);
181
- let inRegion = false;
182
- for (const line of lines) {
183
- if (line.includes('region "Hvem vil du bruke Helsenorge')) inRegion = true;
184
- if (!inRegion) continue;
185
- const btnMatch = line.match(/button "([^"]*)" \[ref=(e\d+)\]/);
186
- if (btnMatch) {
187
- // First try to match person name
188
- if (btnMatch[1].includes(personName)) {
189
- clickRef = btnMatch[2];
190
- log(`Found person button: "${btnMatch[1]}" (ref=${clickRef})`);
191
- break;
192
- }
193
- // Or take first button in region as fallback
194
- if (!clickRef) {
195
- clickRef = btnMatch[2];
196
- }
197
- }
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");
198
209
  }
199
- if (clickRef && !lines.some((l) => l.includes(personName) && l.includes(clickRef))) {
200
- log(`Fallback: clicking first person button in region (ref=${clickRef})`);
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; }
201
226
  }
202
- if (clickRef) {
203
- await runPlaywrightCli(["click", clickRef]);
204
- log("Person clicked on Dokumenter page. Waiting for navigation...");
205
- await sleep(3000);
206
- // Verify we left the person picker — take a new snapshot
207
- const verifySnapshot = path.join(outputDir, "dokumenter_verify.yml");
208
- await runPlaywrightCli(["snapshot", "--filename", verifySnapshot]);
209
- if (fs.existsSync(verifySnapshot)) {
210
- const verifyText = fs.readFileSync(verifySnapshot, "utf8");
211
- if (verifyText.includes("Hvem vil du bruke Helsenorge")) {
212
- // Still on person picker — the click might have opened the nav dropdown instead
213
- // Try clicking the person button inside the full-page list (region), not the nav
214
- log("Still on person picker. Looking for person in full-page list...");
215
- const regionLines = verifyText.split(/\r?\n/);
216
- let inFullPageRegion = false;
217
- let retryRef = null;
218
- for (const line of regionLines) {
219
- if (line.includes('region "Hvem vil du bruke Helsenorge')) inFullPageRegion = true;
220
- if (line.includes('listitem') && inFullPageRegion) {
221
- const btnMatch = line.match(/button "[^"]*" \[ref=(e\d+)\]/);
222
- // Also check next lines for button
223
- if (btnMatch) {
224
- retryRef = btnMatch[1];
225
- break;
226
- }
227
- }
228
- if (inFullPageRegion) {
229
- const btnMatch = line.match(/button "([^"]*)" \[ref=(e\d+)\]/);
230
- if (btnMatch && btnMatch[1].includes(personName)) {
231
- retryRef = btnMatch[2];
232
- break;
233
- }
234
- }
235
- }
236
- // If region-based search didn't find it, just click first listitem button in region
237
- if (!retryRef && inFullPageRegion) {
238
- let inList = false;
239
- for (const line of regionLines) {
240
- if (line.includes('region "Hvem vil du bruke Helsenorge')) inList = true;
241
- if (inList) {
242
- const btnMatch = line.match(/button "[^"]*" \[ref=(e\d+)\].*\[cursor=pointer\]/);
243
- if (btnMatch) {
244
- retryRef = btnMatch[1];
245
- break;
246
- }
247
- }
248
- }
249
- }
250
- if (retryRef) {
251
- log(`Retrying with ref=${retryRef}...`);
252
- await runPlaywrightCli(["click", retryRef]);
253
- await sleep(3000);
254
- }
255
- }
256
- try { fs.unlinkSync(verifySnapshot); } catch (e) {}
257
- }
227
+ if (detailRef) {
228
+ await runPlaywrightCli(["click", detailRef]);
229
+ log(`Clicked 'Se detaljer' (ref=${detailRef})`);
258
230
  } else {
259
- log("Could not find person button in snapshot. Trying select-person...");
260
- await handleSelectPerson(config, personName);
261
- await sleep(3000);
231
+ log("Could not find 'Se detaljer' in snapshot.");
232
+ logIssue("documents", "No 'Se detaljer' button found in document list");
262
233
  }
234
+ await sleep(2000);
235
+ continue;
263
236
  }
264
- try { fs.unlinkSync(checkSnapshot); } catch (e) {}
265
- }
266
-
267
- // Step 3: Take snapshot of document list
268
- const docListSnapshot = path.join(outputDir, "dokumenter.yml");
269
- await runPlaywrightCli(["snapshot", "--filename", docListSnapshot]);
270
- await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "dokumenter.png"), "--full-page"]);
271
- log("Saved: dokumenter.yml + dokumenter.png");
272
-
273
- // Step 4: Click first document's "Se detaljer"
274
- log("Looking for first document...");
275
- 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"; }']);
276
- if (clickResult.stdout.includes("not-found")) {
277
- logIssue("documents", "Could not find 'Se detaljer' link on Dokumenter page", { url: dokumenterUrl });
278
- log("Could not find 'Se detaljer'. Check dokumenter.yml for the page structure.");
279
- // Still continue — agent can handle manually
280
- } else {
281
- log("Clicked 'Se detaljer' on first document.");
282
- await sleep(2000);
283
- }
284
237
 
285
- // Step 4: Click "Åpne dokumentet"
286
- 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"; }']);
287
- if (openResult.stdout.includes("not-found")) {
288
- logIssue("documents", "Could not find 'Åpne dokumentet' link", { url: dokumenterUrl });
289
- log("Could not find 'Åpne dokumentet'. Take a snapshot to inspect the page.");
290
- } else {
291
- log("Clicked 'Åpne dokumentet'.");
292
- await sleep(3000);
293
- }
294
-
295
- // Step 5: Detect format (PDF vs HTML)
296
- const docSnapshot = path.join(outputDir, "document.yml");
297
- await runPlaywrightCli(["snapshot", "--filename", docSnapshot]);
298
-
299
- let format = "unknown";
300
- if (fs.existsSync(docSnapshot)) {
301
- const snapshotText = fs.readFileSync(docSnapshot, "utf8");
302
- if (/href.*\/pdf\//i.test(snapshotText) || /blob:/i.test(snapshotText) || /\.pdf/i.test(snapshotText)) {
303
- format = "pdf";
304
- } else if (snapshotText.length > 500) {
305
- // Has substantial content — likely HTML
306
- 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;
307
255
  }
308
- }
309
- log(`Detected document format: ${format}`);
310
256
 
311
- // Step 6: Capture based on format
312
- if (format === "pdf") {
313
- log("Attempting PDF download...");
314
- // Try direct PDF link first
315
- 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'); }`]);
316
- if (dlCode !== 0) {
317
- logIssue("pdf-download", "PDF download failed", { url: dokumenterUrl, outputDir });
318
- log("PDF download failed. Check document.yml for available links.");
319
- } else {
320
- const pdfPath = path.join(outputDir, "document.pdf");
321
- if (fs.existsSync(pdfPath)) {
322
- 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")}`);
323
263
  } else {
324
- logIssue("pdf-download", "PDF download command succeeded but file not found", { outputDir });
325
- log("PDF download command ran but file not found.");
264
+ logIssue("pdf-download", "PDF download failed", { outputDir });
265
+ log("PDF download failed.");
326
266
  }
267
+ log(`\nDocument verification complete. Format: pdf`);
268
+ log(`Output: ${outputDir}`);
269
+ return 0;
327
270
  }
328
- } else if (format === "html") {
329
- log("Capturing HTML document...");
330
- // Screenshot
331
- const ssCode = await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "document_screenshot.png"), "--full-page"]);
332
- if (ssCode !== 0) {
333
- logIssue("html-capture", "Full-page screenshot failed (possible PDF misdetected as HTML)", { outputDir });
334
- log("Screenshot failed — document might be PDF. Trying download...");
335
- format = "pdf";
336
- } 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"]);
337
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;
338
285
  }
339
- // Save raw HTML
340
- const htmlResult = await runPlaywrightCliCapture(["eval", "document.documentElement.outerHTML"]);
341
- if (htmlResult.code === 0 && htmlResult.stdout) {
342
- const htmlContent = htmlResult.stdout.replace(/^### Result\s*/i, "");
343
- fs.writeFileSync(path.join(outputDir, "document.html"), htmlContent);
344
- 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
+ }
345
296
  }
346
- } else {
347
- logIssue("documents", `Unknown document formatcould not detect PDF or HTML`, { outputDir });
348
- 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.`);
349
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;
350
303
  }
351
304
 
352
- log(`\nDocument verification complete. Format: ${format}`);
353
305
  log(`Output: ${outputDir}`);
354
- return 0;
306
+ return 1;
355
307
  }
356
308
 
357
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.5",
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": {