form-tester 0.9.1 → 0.10.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.
@@ -52,113 +52,123 @@ Notes:
52
52
  - The CLI opens the form with Playwright CLI, saves an initial snapshot + screenshot, and prints next-step commands.
53
53
  - Use `/save {label}` to capture additional snapshots + screenshots into the same output folder.
54
54
  - Use `/update` after changing the skill to update Playwright CLI + skills, then reload the skill in Copilot CLI with `/skills` (reload) or `/restart`.
55
- - Use `/people` to rescan the visible person list and get a numbered selection prompt.
56
- - Use the next-step checklist printed by the CLI (cookies, person selection, validation fix, Dokumenter verification, save HTML/PDF).
57
- - If the error modal appears on save or submit ("Det skjedde en feil under innsending av skjema. Prøv igjen senere."), open DevTools -> Network before retrying. Then try resubmitting once. If it persists, find the failed request and capture the Correlation ID header in test_results.txt.
58
55
  - Use `--help` or `-h` to print the command list without starting the prompt.
59
56
  - IMPORTANT: Always use `form-tester exec` instead of `playwright-cli` directly. This records all commands for replay. Same syntax: `form-tester exec fill e1 "value"`, `form-tester exec click e3`, `form-tester exec close` (finalizes recording).
60
57
  - Replay a previous run: `form-tester replay output/form-id/timestamp/recording.json`
61
58
  - IMPORTANT: When something unexpected happens during a test — wrong page state, unexpected modal, failed command, element not found, timeout, wrong document format — ALWAYS log an issue:
62
59
  `form-tester issue <category> "<description of what happened>"`
63
60
  Categories: person-selection, navigation, form-fill, submission, documents, pdf-download, html-capture, screenshot, snapshot, validation, modal, timeout, other
64
- Example: `form-tester issue modal "Submit showed error modal instead of success: Det skjedde en feil"`
65
- Example: `form-tester issue person-selection "Person list showed 0 options, had to retry manually"`
66
- These logs help us improve the skill to handle more scenarios automatically.
67
61
  - View logged issues: `form-tester issues`
68
- - IMPORTANT: All screenshots taken during a test run MUST use `--full-page` to capture the entire page, not just the viewport. This applies to every screenshot command: `form-tester exec screenshot --filename "..." --full-page`
69
- - IMPORTANT: Take a full-page screenshot EVERY TIME the page changes. This includes: after clicking any action button (Neste, Forrige, Send inn, etc.), after a step/page transition, after form validation errors appear, after modals open, and after submission. Name screenshots descriptively (e.g., step1_filled.png, step2_before_submit.png, submit_result.png).
62
+ - IMPORTANT: All screenshots MUST use `--full-page`. Take a full-page screenshot EVERY TIME the page changes.
63
+
64
+ Test flow (step by step):
70
65
 
71
- Test flow (when /test is triggered):
72
66
  IMPORTANT: Each prompt below MUST be asked as a separate message to the user. Wait for the user's response before proceeding to the next step. Do NOT combine multiple prompts into one message.
73
67
 
74
68
  1. Prompt for PNR (if not in URL). Wait for response.
75
69
  2. Prompt for persona (1-6 selection). Show the numbered list and wait for the user's response.
76
- 3. Prompt for test scenario in a NEW separate message. Ask: "Any specific test scenario? (describe what to test, or Enter for standard clean test)". Wait for the user's response. If the user says nothing specific or "default" or just presses Enter, use standard test. The scenario is saved to scenario.json in the output directory.
77
- 4. Only after receiving answers to all prompts: open browser, fill form, submit, verify.
70
+ 3. Prompt for test scenario in a NEW separate message. Wait for the user's response.
71
+ 4. Only after receiving answers to all prompts, proceed with the steps below.
72
+
73
+ Step 1 — Open and setup:
74
+ ```
75
+ form-tester test <url> --auto --pnr <pnr> --persona <id> --scenario "<text>"
76
+ ```
77
+
78
+ Step 2 — Dismiss cookies:
79
+ ```
80
+ form-tester cookies
81
+ ```
82
+ This automatically finds and clicks the cookie banner. No-op if no banner is present.
78
83
 
79
- Form filling strategy:
80
- Before filling any fields, take a snapshot and study the FULL form structure:
84
+ Step 3 — Select person:
85
+ ```
86
+ form-tester select-person
87
+ ```
88
+ This scans for available persons, selects the recommended one ("Uromantisk Direktør"), clicks it, and takes a screenshot. To select a specific person:
89
+ ```
90
+ form-tester select-person "Navn Navnesen"
91
+ ```
92
+
93
+ Step 4 — Study the form structure:
94
+ Take a snapshot and study the FULL form before filling anything:
81
95
  1. Identify ALL sections, including collapsed/accordion sections (buttons with arrow icons).
82
- 2. Expand ALL collapsed sections FIRST by clicking their header buttons. Take a new snapshot after expanding.
83
- 3. Identify ALL required fields across all sections before starting to fill.
84
- 4. Fill fields section by section, top to bottom.
96
+ 2. Expand ALL collapsed sections FIRST by clicking their header buttons.
97
+ 3. Take a new snapshot after expanding.
98
+ 4. Identify ALL required fields across all sections.
99
+
100
+ Step 5 — Fill the form:
101
+ Fill fields section by section, top to bottom. Use `form-tester exec` for all commands.
85
102
 
86
103
  Autosuggest / search fields (e.g. "Søk opp et legemiddel"):
87
- These fields show a dropdown with suggestions as you type. Do NOT use `fill` + `Enter` — the value won't commit.
88
- Instead:
104
+ Do NOT use `fill` + `Enter` — the value won't commit. Instead:
89
105
  ```
90
106
  form-tester exec fill <ref> "search text"
91
107
  ```
92
- Then wait for the dropdown to appear and take a snapshot to find the suggestion element:
108
+ Wait for the dropdown, take a snapshot to find the suggestion, then click it:
93
109
  ```
94
110
  form-tester exec snapshot
111
+ form-tester exec click <suggestion-ref>
95
112
  ```
96
- Then click the correct suggestion from the dropdown list. If no dropdown appears, try:
113
+ Or use run-code:
97
114
  ```
98
115
  form-tester exec run-code "async page => { const input = page.locator('#fieldId'); await input.fill('search text'); await page.waitForTimeout(1000); const option = page.locator('[role=\"option\"]').first(); await option.click(); }"
99
116
  ```
100
117
 
101
- Handling validation errors after submit:
102
- CRITICAL RULES follow these exactly:
103
- 1. MAXIMUM 3 submit attempts. If validation errors persist after 3 attempts, STOP. Log the remaining errors with `form-tester issue validation "..."` and note them in test_results.txt. Do NOT keep retrying.
104
- 2. After each failed submit, take a snapshot and READ the validation error list carefully.
105
- 3. Each validation error is a clickable link with an href like `#fieldId`. Click the error link to scroll to and focus the unfilled field. This is the ONLY reliable way to find the field.
106
- 4. After clicking the error link, take a snapshot to see the field in context and fill it.
107
- 5. Do NOT re-fill fields that are already filled. Only fix the fields listed in the validation errors.
108
- 6. Do NOT use JavaScript `dispatchEvent` hacks or `element.evaluate()` to set values — these bypass React's state and the form won't register the value. Always use Playwright's `fill`, `click`, `select` commands.
109
- 7. Before resubmitting, verify that the number of validation errors has decreased. If the same errors persist after you tried to fix them, the approach isn't working — try a different strategy (e.g., expand a collapsed section, use a different selector).
110
- 8. Some forms have accordion/collapsible sections. Validation errors inside collapsed sections cannot be filled until the section is expanded. Look for buttons near the error's field ID in the snapshot and click to expand.
118
+ Step 6 Submit:
119
+ Take a screenshot before submitting, then click the submit button.
111
120
 
112
- Post-submit verification:
121
+ Step 7 — Handle validation errors:
122
+ IMPORTANT: If validation errors appear after clicking submit, the form DID NOT SUBMIT. The errors are blocking submission. You must fix them first.
123
+
124
+ Run the validate command to find and scroll to each error:
125
+ ```
126
+ form-tester validate
127
+ ```
128
+ This will:
129
+ 1. Take a snapshot and parse all validation errors
130
+ 2. Click each error link to scroll to the unfilled field
131
+ 3. Take a snapshot at each field so you can see what needs to be filled
132
+ 4. Print structured output with field IDs and error messages
133
+
134
+ After `form-tester validate`, fix each field it found, then run `form-tester validate` again to confirm errors are resolved. Only then resubmit.
135
+
136
+ CRITICAL RULES:
137
+ - MAXIMUM 3 submit attempts. After 3, STOP. Log remaining errors with `form-tester issue validation "..."` and note them in test_results.txt.
138
+ - Do NOT re-fill fields that are already filled. Only fix fields listed in validation errors.
139
+ - Do NOT use JavaScript `dispatchEvent` hacks or `element.evaluate()` to set values — these bypass React's state. Always use Playwright's `fill`, `click`, `select` commands.
140
+ - If the same errors persist after fixing, the field might be inside a collapsed accordion section. Expand it first.
141
+
142
+ Step 8 — Post-submit verification:
113
143
  After a successful submission, read the modal text carefully:
114
- - If it says the form is stored in Dokumenter (e.g. "En kopi er også lagret i Dokumenter" or "Skjemaet er fullført og lagret i Dokumenter"), proceed with Dokumenter verification below.
115
- - If the modal does NOT mention Dokumenter, or says the form will not be stored/you will not get a response, skip Dokumenter verification entirely. Record this in test_results.txt.
144
+ - If it says the form is stored in Dokumenter (e.g. "lagret i Dokumenter"), proceed with Dokumenter verification.
145
+ - If the modal does NOT mention Dokumenter, skip Dokumenter verification. Record this in test_results.txt.
116
146
 
117
- Dokumenter verification (only when modal confirms storage):
118
- Use the standardized documents command — it handles navigation, format detection, PDF download, and HTML capture automatically:
147
+ Step 9 — Dokumenter verification (only when modal confirms storage):
119
148
  ```
120
149
  form-tester documents
121
150
  ```
122
- This will:
123
- 1. Navigate to `/dokumenter?pnr={PNR}`
124
- 2. Click "Se detaljer" on the first document
125
- 3. Click "Åpne dokumentet"
126
- 4. Auto-detect PDF vs HTML format
127
- 5. Download PDF or capture HTML screenshot + raw HTML
128
- 6. Log issues automatically if any step fails
129
-
130
- If `form-tester documents` doesn't find the right elements (logged as issues), fall back to manual steps:
131
- 1. Navigate to `/dokumenter?pnr={PNR}` and select the same person used during form fill.
132
- 2. The document list loads sorted newest first. The first entry should match the form title.
133
- 3. Click "Se detaljer" on the first document, then click "Åpne dokumentet".
134
- 4. After clicking "Åpne dokumentet", determine the document format:
135
-
136
- How to detect format:
137
- - Take a snapshot: `form-tester exec snapshot`
138
- - If snapshot shows a link with href containing `/pdf/` or `blob:` → PDF
139
- - If `--full-page` screenshot times out → PDF (do NOT retry, switch to download)
140
- - If snapshot shows rendered HTML content (headings, paragraphs, form data) → HTML
141
-
142
- PDF documents — DOWNLOAD, do NOT screenshot:
143
- IMPORTANT: `run-code` does NOT have access to `require` or `fs`. Do NOT use `require('fs')`. Use Playwright's download API instead.
144
-
145
- Find the PDF download link in the snapshot (look for `a[href*="/pdf/"]` or "Last ned" link), then download using the Playwright download event:
151
+ This handles the full flow: navigate to Dokumenter, find latest doc, detect format, download PDF or screenshot HTML.
152
+
153
+ If `form-tester documents` fails (logged as issues), fall back to manual steps:
154
+ 1. Navigate to `/dokumenter?pnr={PNR}` and select the same person.
155
+ 2. Click "Se detaljer" on the first document, then "Åpne dokumentet".
156
+ 3. Detect format from snapshot:
157
+ - `a[href*="/pdf/"]` or `blob:` PDF
158
+ - `--full-page` screenshot times out → PDF
159
+ - Rendered HTML content HTML
160
+
161
+ PDF download:
146
162
  ```
147
163
  form-tester exec run-code "async page => { const link = page.locator('a[href*=\"/pdf/\"]').first(); const [download] = await Promise.all([ page.waitForEvent('download'), link.click() ]); await download.saveAs('$OUTPUT_DIR/document.pdf'); }"
148
164
  ```
149
- If there is no direct PDF link but a "Last ned" button:
150
- ```
151
- form-tester exec run-code "async page => { const [download] = await Promise.all([ page.waitForEvent('download'), page.getByRole('link', { name: 'Last ned' }).click() ]); await download.saveAs('$OUTPUT_DIR/document.pdf'); }"
152
- ```
153
- Verify the download: check that document.pdf exists in the output folder.
154
165
 
155
- HTML documents SCREENSHOT full page:
166
+ HTML — screenshot + save:
156
167
  ```
157
168
  form-tester exec screenshot --filename "$OUTPUT_DIR/document_screenshot.png" --full-page
169
+ form-tester exec eval "document.documentElement.outerHTML"
158
170
  ```
159
- Also save raw HTML: `form-tester exec eval "document.documentElement.outerHTML"` → save to document.html.
160
-
161
- XML/other: Note type in test_results.txt, skip capture.
162
171
 
163
- 5. Log any issues encountered: `form-tester issue documents "description of what went wrong"`
164
- 6. Include the document verification results in test_results.txt (document title, whether it matched the form h1, document type: HTML/PDF/XML).
172
+ Step 10 Finalize:
173
+ - Write test_results.txt with status, data used, and notes.
174
+ - Close browser: `form-tester exec close`
@@ -47,21 +47,31 @@ Replay a previous run:
47
47
  form-tester replay output/form-id/timestamp/recording.json
48
48
  ```
49
49
 
50
- ## Document verification
50
+ ## Standardized commands
51
+
52
+ Use these instead of manual steps — they handle retries, edge cases, and error logging automatically:
51
53
 
52
- After form submission, use the standardized documents command:
53
54
  ```bash
54
- form-tester documents # auto-navigates, detects PDF/HTML, captures
55
+ form-tester cookies # dismiss cookie banner (tries known selectors)
56
+ form-tester select-person # select recommended person ("Uromantisk Direktør")
57
+ form-tester select-person "Name" # select specific person by name
58
+ form-tester validate # parse validation errors, scroll to each field, show context
59
+ form-tester documents # navigate to Dokumenter, detect PDF/HTML, capture
60
+ form-tester issue <category> "<text>" # log an issue for skill improvement
61
+ form-tester issues # view recent issues
55
62
  ```
56
- This handles the full flow: navigate to Dokumenter, find latest doc, detect format, download PDF or screenshot HTML.
63
+
64
+ ### Typical test flow:
65
+ 1. `form-tester test <url> --auto` — open form
66
+ 2. `form-tester cookies` — dismiss cookies
67
+ 3. `form-tester select-person` — select person
68
+ 4. Fill the form with `form-tester exec fill/click/select`
69
+ 5. Submit, then `form-tester validate` — find and fix any validation errors
70
+ 6. `form-tester documents` — verify document after successful submission
71
+ 7. `form-tester exec close` — finalize recording
57
72
 
58
73
  ## Issue logging
59
74
 
60
- When something unexpected happens during a test, log it for skill improvement:
61
- ```bash
62
- form-tester issue <category> "<description>"
63
- form-tester issues # view recent issues
64
- ```
65
75
  Categories: `person-selection`, `navigation`, `form-fill`, `submission`, `documents`, `pdf-download`, `html-capture`, `screenshot`, `snapshot`, `validation`, `modal`, `timeout`, `other`
66
76
 
67
77
  ## Interactive commands
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.9.1";
10
+ const LOCAL_VERSION = "0.10.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
@@ -257,6 +257,268 @@ async function handleDocuments(config, flags = {}) {
257
257
  return 0;
258
258
  }
259
259
 
260
+ // --- Standardized commands: cookies, select-person, validate ---
261
+
262
+ async function handleCookies() {
263
+ // Try known cookie banner selectors
264
+ const script = `() => {
265
+ const selectors = [
266
+ '[data-testid="reject-all-cookies"]',
267
+ '[data-testid="accept-all-cookies"]',
268
+ 'button[id*="cookie" i]',
269
+ 'button[class*="cookie" i]',
270
+ '[data-testid*="cookie" i]',
271
+ 'button:has-text("Avvis alle")',
272
+ 'button:has-text("Reject")',
273
+ 'button:has-text("Aksepter")',
274
+ ];
275
+ for (const sel of selectors) {
276
+ const el = document.querySelector(sel);
277
+ if (el && el.offsetParent !== null) {
278
+ el.click();
279
+ return { found: true, selector: sel, text: (el.textContent || "").trim().substring(0, 60) };
280
+ }
281
+ }
282
+ return { found: false };
283
+ }`;
284
+ const result = await runPlaywrightCliCapture(["eval", script]);
285
+ const output = result.stdout.replace(/^### Result\s*/i, "").trim();
286
+ try {
287
+ const parsed = JSON.parse(output);
288
+ if (parsed.found) {
289
+ console.log(`Cookie banner dismissed: "${parsed.text}" (${parsed.selector})`);
290
+ return 0;
291
+ }
292
+ } catch (e) {
293
+ // parse failed, check raw output
294
+ }
295
+ console.log("No cookie banner found.");
296
+ return 0;
297
+ }
298
+
299
+ async function handleSelectPerson(config, targetName) {
300
+ const v = "normal";
301
+ const log = (msg) => console.log(msg);
302
+
303
+ // Step 1: Try to find person list from current page snapshot
304
+ const tmpSnapshot = path.join(
305
+ config.lastRunDir || OUTPUT_BASE,
306
+ `_person_scan_${Date.now()}.yml`,
307
+ );
308
+ await runPlaywrightCli(["snapshot", "--filename", tmpSnapshot]);
309
+
310
+ let options = extractPersonsFromSnapshotFile(tmpSnapshot);
311
+
312
+ // Step 2: If no options from snapshot, try JS-based detection
313
+ if (!options.length) {
314
+ for (let attempt = 0; attempt < 3; attempt++) {
315
+ log(`Scanning for person options (attempt ${attempt + 1})...`);
316
+ await sleep(1500);
317
+ options = await fetchPersonOptions();
318
+ if (options.length) break;
319
+ }
320
+ }
321
+
322
+ if (!options.length) {
323
+ console.error("No person options found on page.");
324
+ logIssue("person-selection", "No person options found by select-person command");
325
+ // Clean up temp file
326
+ try { fs.unlinkSync(tmpSnapshot); } catch (e) {}
327
+ return 1;
328
+ }
329
+
330
+ options = prioritizeRecommended(options, RECOMMENDED_PERSON);
331
+
332
+ // Step 3: Determine which person to select
333
+ let chosen;
334
+ if (targetName) {
335
+ const target = targetName.toLowerCase();
336
+ chosen = options.find((o) => o.toLowerCase().includes(target));
337
+ if (!chosen) {
338
+ log(`Person "${targetName}" not found. Available: ${options.join(", ")}`);
339
+ chosen = options[0];
340
+ log(`Falling back to: ${chosen}`);
341
+ }
342
+ } else {
343
+ chosen = options[0]; // recommended or first
344
+ }
345
+
346
+ // Step 4: Click the person button
347
+ log(`Selecting person: ${chosen}`);
348
+ const clickScript = `() => {
349
+ const buttons = Array.from(document.querySelectorAll('button, [role="button"], [role="option"], a'));
350
+ const normalize = (s) => (s || "").replace(/\\s+/g, " ").trim();
351
+ const target = ${JSON.stringify(chosen)};
352
+ for (const btn of buttons) {
353
+ const text = normalize(btn.textContent);
354
+ if (text.includes(target) || text === target) {
355
+ btn.click();
356
+ return { clicked: true, text: text.substring(0, 80) };
357
+ }
358
+ }
359
+ return { clicked: false, available: buttons.slice(0, 10).map(b => normalize(b.textContent).substring(0, 60)).filter(Boolean) };
360
+ }`;
361
+ const clickResult = await runPlaywrightCliCapture(["eval", clickScript]);
362
+ const clickOutput = clickResult.stdout.replace(/^### Result\s*/i, "").trim();
363
+ try {
364
+ const parsed = JSON.parse(clickOutput);
365
+ if (parsed.clicked) {
366
+ log(`Person selected: ${parsed.text}`);
367
+ config.lastPerson = chosen;
368
+ saveConfig(config);
369
+ await sleep(1000);
370
+ // Take screenshot after selection
371
+ if (config.lastRunDir) {
372
+ await runPlaywrightCli(["screenshot", "--filename", path.join(config.lastRunDir, "person_selected.png"), "--full-page"]);
373
+ }
374
+ } else {
375
+ log("Could not click person button. Available buttons:");
376
+ (parsed.available || []).forEach((b) => log(` - ${b}`));
377
+ logIssue("person-selection", `Could not click "${chosen}". Buttons found but none matched.`);
378
+ }
379
+ } catch (e) {
380
+ log(`Person selection result: ${clickOutput}`);
381
+ }
382
+
383
+ // Clean up temp file
384
+ try { fs.unlinkSync(tmpSnapshot); } catch (e) {}
385
+ return 0;
386
+ }
387
+
388
+ function parseValidationErrors(snapshotText) {
389
+ const lines = snapshotText.split(/\r?\n/);
390
+ const errors = [];
391
+ // Look for the validation status block: status "Sjekk at følgende er riktig utfylt:"
392
+ let inValidation = false;
393
+ let validationIndent = null;
394
+
395
+ for (const line of lines) {
396
+ const indentMatch = line.match(/^(\s*)/);
397
+ const indent = indentMatch ? indentMatch[1].length : 0;
398
+
399
+ if (!inValidation) {
400
+ if (line.includes('status "Sjekk at følgende er riktig utfylt:"') || line.includes('status "Sjekk at f')) {
401
+ inValidation = true;
402
+ validationIndent = indent;
403
+ }
404
+ continue;
405
+ }
406
+
407
+ // Exit validation block when we hit same or lower indent that's not part of the list
408
+ if (indent <= validationIndent && !line.includes("list") && !line.includes("listitem") && !line.includes("link") && line.trim().startsWith("-")) {
409
+ break;
410
+ }
411
+
412
+ // Parse error links: link "error text" [ref=eXXX] ... /url: "#fieldId"
413
+ const linkMatch = line.match(/link "([^"]+)" \[ref=(e\d+)\]/);
414
+ if (linkMatch) {
415
+ const errorText = linkMatch[1];
416
+ const ref = linkMatch[2];
417
+ // Look for the URL on the next line or in the same block
418
+ errors.push({ text: errorText, ref, fieldId: null });
419
+ }
420
+
421
+ // Parse URL for the most recent error
422
+ const urlMatch = line.match(/\/url:\s*"#([^"]+)"/);
423
+ if (urlMatch && errors.length > 0 && !errors[errors.length - 1].fieldId) {
424
+ errors[errors.length - 1].fieldId = urlMatch[1];
425
+ }
426
+ }
427
+
428
+ return errors;
429
+ }
430
+
431
+ async function handleValidate(config) {
432
+ const outputDir = config.lastRunDir;
433
+ if (!outputDir) {
434
+ console.error("No output folder. Run a test first.");
435
+ return 1;
436
+ }
437
+ fs.mkdirSync(outputDir, { recursive: true });
438
+
439
+ // Step 1: Take a fresh snapshot
440
+ const snapshotPath = path.join(outputDir, `validate_${Date.now()}.yml`);
441
+ await runPlaywrightCli(["snapshot", "--filename", snapshotPath]);
442
+
443
+ if (!fs.existsSync(snapshotPath)) {
444
+ console.error("Failed to take snapshot.");
445
+ return 1;
446
+ }
447
+
448
+ const snapshotText = fs.readFileSync(snapshotPath, "utf8");
449
+ const errors = parseValidationErrors(snapshotText);
450
+
451
+ if (!errors.length) {
452
+ console.log("No validation errors found. Form is ready to submit.");
453
+ return 0;
454
+ }
455
+
456
+ console.log(`\n${errors.length} validation error(s) found:\n`);
457
+
458
+ for (let i = 0; i < errors.length; i++) {
459
+ const err = errors[i];
460
+ const fieldId = err.fieldId ? decodeURIComponent(err.fieldId) : "unknown";
461
+ console.log(` ${i + 1}. [${fieldId}] "${err.text}" (ref=${err.ref})`);
462
+ }
463
+
464
+ // Step 2: Click each error link to scroll to it and take a snapshot
465
+ console.log("\nScrolling to each error and taking snapshots...\n");
466
+
467
+ for (let i = 0; i < errors.length; i++) {
468
+ const err = errors[i];
469
+ const fieldId = err.fieldId ? decodeURIComponent(err.fieldId) : null;
470
+
471
+ // Click the error link to scroll to the field
472
+ const clickCode = await runPlaywrightCli(["click", err.ref]);
473
+ if (clickCode !== 0) {
474
+ console.log(` ${i + 1}. Could not click error link ${err.ref}`);
475
+ continue;
476
+ }
477
+
478
+ await sleep(500);
479
+
480
+ // Take a snapshot focused on the field area
481
+ const fieldSnapshotPath = path.join(outputDir, `validation_error_${i + 1}.yml`);
482
+ await runPlaywrightCli(["snapshot", "--filename", fieldSnapshotPath]);
483
+
484
+ // Read the snapshot to find the field context
485
+ if (fs.existsSync(fieldSnapshotPath)) {
486
+ const fieldSnapshot = fs.readFileSync(fieldSnapshotPath, "utf8");
487
+
488
+ // Find the field in the snapshot by its ID
489
+ let fieldContext = "";
490
+ if (fieldId) {
491
+ const fieldLines = fieldSnapshot.split(/\r?\n/);
492
+ for (let j = 0; j < fieldLines.length; j++) {
493
+ // Look for elements with aria-invalid or the field ID
494
+ if (fieldLines[j].includes(`[aria-invalid`) || fieldLines[j].includes(fieldId.replace(/\./g, "%2E"))) {
495
+ // Grab surrounding context (5 lines before, 10 after)
496
+ const start = Math.max(0, j - 5);
497
+ const end = Math.min(fieldLines.length, j + 10);
498
+ fieldContext = fieldLines.slice(start, end).join("\n");
499
+ break;
500
+ }
501
+ }
502
+ }
503
+
504
+ if (fieldContext) {
505
+ console.log(` ${i + 1}. [${fieldId}] "${err.text}"`);
506
+ console.log(` Field context from snapshot:`);
507
+ console.log(fieldContext.split("\n").map((l) => ` ${l}`).join("\n"));
508
+ console.log("");
509
+ } else {
510
+ console.log(` ${i + 1}. [${fieldId}] "${err.text}" — scrolled to field, see ${fieldSnapshotPath}`);
511
+ }
512
+ }
513
+ }
514
+
515
+ // Step 3: Scroll back to top
516
+ await runPlaywrightCliCapture(["eval", "window.scrollTo(0, 0)"]);
517
+
518
+ console.log(`\nFix these ${errors.length} field(s), then run 'form-tester validate' again before submitting.`);
519
+ return errors.length;
520
+ }
521
+
260
522
  const PERSONAS = [
261
523
  {
262
524
  id: "ung-mann",
@@ -650,9 +912,12 @@ function printHelp() {
650
912
  " form-tester test <url> --human Interactive test with prompts",
651
913
  " form-tester exec <command> [args] Run playwright-cli command (recorded)",
652
914
  " form-tester replay <recording.json> Replay a recorded test run",
653
- " form-tester documents Verify document in Dokumenter (auto PDF/HTML)",
654
- " form-tester issue <category> <message> Log an issue for skill improvement",
655
- " form-tester issues [limit] Show recent logged issues",
915
+ " form-tester cookies Dismiss cookie banner",
916
+ " form-tester select-person [name] Select person (recommended or by name)",
917
+ " form-tester validate Parse validation errors, scroll to each field",
918
+ " form-tester documents Verify document in Dokumenter (auto PDF/HTML)",
919
+ " form-tester issue <category> <message> Log an issue for skill improvement",
920
+ " form-tester issues [limit] Show recent logged issues",
656
921
  "",
657
922
  "Interactive commands:",
658
923
  " /test {url} Open form URL and start test",
@@ -1543,6 +1808,24 @@ async function main() {
1543
1808
  process.exit(code);
1544
1809
  }
1545
1810
 
1811
+ if (args[0] === "cookies") {
1812
+ const code = await handleCookies();
1813
+ process.exit(code);
1814
+ }
1815
+
1816
+ if (args[0] === "select-person") {
1817
+ const config = loadConfig();
1818
+ const targetName = args.slice(1).join(" ") || null;
1819
+ const code = await handleSelectPerson(config, targetName);
1820
+ process.exit(code);
1821
+ }
1822
+
1823
+ if (args[0] === "validate") {
1824
+ const config = loadConfig();
1825
+ const code = await handleValidate(config);
1826
+ process.exit(code);
1827
+ }
1828
+
1546
1829
  if (args[0] === "test" && args.includes("--human")) {
1547
1830
  const config = loadConfig();
1548
1831
  const url = args.find((a) => a.startsWith("http"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-tester",
3
- "version": "0.9.1",
3
+ "version": "0.10.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": {