form-tester 0.8.3 → 0.9.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.
@@ -58,6 +58,13 @@ Notes:
58
58
  - Use `--help` or `-h` to print the command list without starting the prompt.
59
59
  - 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
60
  - Replay a previous run: `form-tester replay output/form-id/timestamp/recording.json`
61
+ - 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
+ `form-tester issue <category> "<description of what happened>"`
63
+ 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
+ - View logged issues: `form-tester issues`
61
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`
62
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).
63
70
 
@@ -75,6 +82,19 @@ After a successful submission, read the modal text carefully:
75
82
  - 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.
76
83
 
77
84
  Dokumenter verification (only when modal confirms storage):
85
+ Use the standardized documents command — it handles navigation, format detection, PDF download, and HTML capture automatically:
86
+ ```
87
+ form-tester documents
88
+ ```
89
+ This will:
90
+ 1. Navigate to `/dokumenter?pnr={PNR}`
91
+ 2. Click "Se detaljer" on the first document
92
+ 3. Click "Åpne dokumentet"
93
+ 4. Auto-detect PDF vs HTML format
94
+ 5. Download PDF or capture HTML screenshot + raw HTML
95
+ 6. Log issues automatically if any step fails
96
+
97
+ If `form-tester documents` doesn't find the right elements (logged as issues), fall back to manual steps:
78
98
  1. Navigate to `/dokumenter?pnr={PNR}` and select the same person used during form fill.
79
99
  2. The document list loads sorted newest first. The first entry should match the form title.
80
100
  3. Click "Se detaljer" on the first document, then click "Åpne dokumentet".
@@ -107,4 +127,5 @@ Dokumenter verification (only when modal confirms storage):
107
127
 
108
128
  XML/other: Note type in test_results.txt, skip capture.
109
129
 
110
- 5. Include the document verification results in test_results.txt (document title, whether it matched the form h1, document type: HTML/PDF/XML).
130
+ 5. Log any issues encountered: `form-tester issue documents "description of what went wrong"`
131
+ 6. Include the document verification results in test_results.txt (document title, whether it matched the form h1, document type: HTML/PDF/XML).
@@ -47,7 +47,24 @@ Replay a previous run:
47
47
  form-tester replay output/form-id/timestamp/recording.json
48
48
  ```
49
49
 
50
- ## Commands
50
+ ## Document verification
51
+
52
+ After form submission, use the standardized documents command:
53
+ ```bash
54
+ form-tester documents # auto-navigates, detects PDF/HTML, captures
55
+ ```
56
+ This handles the full flow: navigate to Dokumenter, find latest doc, detect format, download PDF or screenshot HTML.
57
+
58
+ ## Issue logging
59
+
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
+ Categories: `person-selection`, `navigation`, `form-fill`, `submission`, `documents`, `pdf-download`, `html-capture`, `screenshot`, `snapshot`, `validation`, `modal`, `timeout`, `other`
66
+
67
+ ## Interactive commands
51
68
 
52
69
  ```bash
53
70
  /setup
package/form-tester.js CHANGED
@@ -6,7 +6,8 @@ const { spawn, execSync } = require("child_process");
6
6
 
7
7
  const CONFIG_PATH = path.join(process.cwd(), "form-tester.config.json");
8
8
  const OUTPUT_BASE = path.resolve(process.cwd(), "output");
9
- const LOCAL_VERSION = "0.8.3";
9
+ const ISSUES_PATH = path.join(OUTPUT_BASE, "issues.jsonl");
10
+ const LOCAL_VERSION = "0.9.0";
10
11
  const RECOMMENDED_PERSON = "Uromantisk Direktør";
11
12
 
12
13
  // Recording — persisted to disk so `form-tester exec` can append across processes
@@ -80,6 +81,182 @@ function saveRecording() {
80
81
  return result;
81
82
  }
82
83
 
84
+ // Issue logging — captures problems during test runs for skill improvement
85
+ const ISSUE_CATEGORIES = [
86
+ "person-selection",
87
+ "navigation",
88
+ "form-fill",
89
+ "submission",
90
+ "documents",
91
+ "pdf-download",
92
+ "html-capture",
93
+ "screenshot",
94
+ "snapshot",
95
+ "validation",
96
+ "modal",
97
+ "timeout",
98
+ "other",
99
+ ];
100
+
101
+ function logIssue(category, message, context = {}) {
102
+ const entry = {
103
+ timestamp: new Date().toISOString(),
104
+ version: LOCAL_VERSION,
105
+ category,
106
+ message,
107
+ ...context,
108
+ };
109
+
110
+ // Append to global issues log
111
+ fs.mkdirSync(path.dirname(ISSUES_PATH), { recursive: true });
112
+ fs.appendFileSync(ISSUES_PATH, JSON.stringify(entry) + "\n");
113
+
114
+ // Also append to current run dir if available
115
+ try {
116
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
117
+ if (config.lastRunDir && fs.existsSync(config.lastRunDir)) {
118
+ const runIssuesPath = path.join(config.lastRunDir, "issues.jsonl");
119
+ fs.appendFileSync(runIssuesPath, JSON.stringify(entry) + "\n");
120
+ }
121
+ } catch (e) {
122
+ // no config or run dir, global log is enough
123
+ }
124
+
125
+ return entry;
126
+ }
127
+
128
+ function listIssues(limit = 20) {
129
+ if (!fs.existsSync(ISSUES_PATH)) return [];
130
+ const lines = fs.readFileSync(ISSUES_PATH, "utf8").trim().split("\n").filter(Boolean);
131
+ const issues = lines.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
132
+ return issues.slice(-limit);
133
+ }
134
+
135
+ function formatIssue(issue) {
136
+ const time = issue.timestamp ? issue.timestamp.replace("T", " ").replace(/\.\d+Z$/, "") : "?";
137
+ const url = issue.url ? ` | ${issue.url}` : "";
138
+ const formId = issue.formId ? ` | ${issue.formId}` : "";
139
+ return `[${time}] [${issue.category}]${formId}${url}\n ${issue.message}`;
140
+ }
141
+
142
+ async function handleDocuments(config, flags = {}) {
143
+ const v = flags.verbosity || "normal";
144
+ const log = (msg) => { if (v !== "silent") console.log(msg); };
145
+
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
+ const outputDir = config.lastRunDir;
154
+ if (!outputDir) {
155
+ console.error("No output folder. Run a test first.");
156
+ return 1;
157
+ }
158
+ fs.mkdirSync(outputDir, { recursive: true });
159
+
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: Take snapshot of document list
171
+ const docListSnapshot = path.join(outputDir, "dokumenter.yml");
172
+ await runPlaywrightCli(["snapshot", "--filename", docListSnapshot]);
173
+ await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "dokumenter.png"), "--full-page"]);
174
+ log("Saved: dokumenter.yml + dokumenter.png");
175
+
176
+ // Step 3: Click first document's "Se detaljer"
177
+ log("Looking for first document...");
178
+ 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"; }']);
179
+ if (clickResult.stdout.includes("not-found")) {
180
+ logIssue("documents", "Could not find 'Se detaljer' link on Dokumenter page", { url: dokumenterUrl });
181
+ log("Could not find 'Se detaljer'. Check dokumenter.yml for the page structure.");
182
+ // Still continue — agent can handle manually
183
+ } else {
184
+ log("Clicked 'Se detaljer' on first document.");
185
+ await sleep(2000);
186
+ }
187
+
188
+ // Step 4: Click "Åpne dokumentet"
189
+ 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"; }']);
190
+ if (openResult.stdout.includes("not-found")) {
191
+ logIssue("documents", "Could not find 'Åpne dokumentet' link", { url: dokumenterUrl });
192
+ log("Could not find 'Åpne dokumentet'. Take a snapshot to inspect the page.");
193
+ } else {
194
+ log("Clicked 'Åpne dokumentet'.");
195
+ await sleep(3000);
196
+ }
197
+
198
+ // Step 5: Detect format (PDF vs HTML)
199
+ const docSnapshot = path.join(outputDir, "document.yml");
200
+ await runPlaywrightCli(["snapshot", "--filename", docSnapshot]);
201
+
202
+ let format = "unknown";
203
+ if (fs.existsSync(docSnapshot)) {
204
+ const snapshotText = fs.readFileSync(docSnapshot, "utf8");
205
+ if (/href.*\/pdf\//i.test(snapshotText) || /blob:/i.test(snapshotText) || /\.pdf/i.test(snapshotText)) {
206
+ format = "pdf";
207
+ } else if (snapshotText.length > 500) {
208
+ // Has substantial content — likely HTML
209
+ format = "html";
210
+ }
211
+ }
212
+ log(`Detected document format: ${format}`);
213
+
214
+ // Step 6: Capture based on format
215
+ if (format === "pdf") {
216
+ log("Attempting PDF download...");
217
+ // Try direct PDF link first
218
+ 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'); }`]);
219
+ if (dlCode !== 0) {
220
+ logIssue("pdf-download", "PDF download failed", { url: dokumenterUrl, outputDir });
221
+ log("PDF download failed. Check document.yml for available links.");
222
+ } else {
223
+ const pdfPath = path.join(outputDir, "document.pdf");
224
+ if (fs.existsSync(pdfPath)) {
225
+ log(`PDF saved: ${pdfPath}`);
226
+ } else {
227
+ logIssue("pdf-download", "PDF download command succeeded but file not found", { outputDir });
228
+ log("PDF download command ran but file not found.");
229
+ }
230
+ }
231
+ } else if (format === "html") {
232
+ log("Capturing HTML document...");
233
+ // Screenshot
234
+ const ssCode = await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "document_screenshot.png"), "--full-page"]);
235
+ if (ssCode !== 0) {
236
+ logIssue("html-capture", "Full-page screenshot failed (possible PDF misdetected as HTML)", { outputDir });
237
+ log("Screenshot failed — document might be PDF. Trying download...");
238
+ format = "pdf";
239
+ } else {
240
+ log("Saved: document_screenshot.png (full-page)");
241
+ }
242
+ // Save raw HTML
243
+ const htmlResult = await runPlaywrightCliCapture(["eval", "document.documentElement.outerHTML"]);
244
+ if (htmlResult.code === 0 && htmlResult.stdout) {
245
+ const htmlContent = htmlResult.stdout.replace(/^### Result\s*/i, "");
246
+ fs.writeFileSync(path.join(outputDir, "document.html"), htmlContent);
247
+ log("Saved: document.html");
248
+ }
249
+ } else {
250
+ logIssue("documents", `Unknown document format — could not detect PDF or HTML`, { outputDir });
251
+ log("Could not determine format. Taking snapshot and screenshot for manual review.");
252
+ await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "document_screenshot.png"), "--full-page"]);
253
+ }
254
+
255
+ log(`\nDocument verification complete. Format: ${format}`);
256
+ log(`Output: ${outputDir}`);
257
+ return 0;
258
+ }
259
+
83
260
  const PERSONAS = [
84
261
  {
85
262
  id: "ung-mann",
@@ -473,6 +650,9 @@ function printHelp() {
473
650
  " form-tester test <url> --human Interactive test with prompts",
474
651
  " form-tester exec <command> [args] Run playwright-cli command (recorded)",
475
652
  " 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",
476
656
  "",
477
657
  "Interactive commands:",
478
658
  " /test {url} Open form URL and start test",
@@ -717,6 +897,7 @@ async function fetchPersonOptions() {
717
897
  console.log(
718
898
  `Failed to read person list from browser (${combinedOutput || "unknown error"}).`,
719
899
  );
900
+ logIssue("person-selection", `Failed to read person list: ${combinedOutput || "unknown error"}`);
720
901
  return [];
721
902
  }
722
903
  return parsePersonList(result.stdout);
@@ -749,6 +930,7 @@ async function promptPersonSelection(config) {
749
930
  console.log(
750
931
  "No person options detected. Open the picker and run /people to retry.",
751
932
  );
933
+ logIssue("person-selection", "No person options detected after 3 attempts");
752
934
  return;
753
935
  }
754
936
  console.log("Select a person:");
@@ -1100,6 +1282,7 @@ async function handleReplay(filePath) {
1100
1282
  console.log(`[${i + 1}/${recording.commandCount}] playwright-cli ${cmd.args.join(" ")}`);
1101
1283
  const code = await runPlaywrightCli(cmd.args);
1102
1284
  if (code !== 0) {
1285
+ logIssue("other", `Replay command failed: playwright-cli ${cmd.args.join(" ")} (exit ${code})`);
1103
1286
  console.error(`Command failed with exit code ${code}. Stopping replay.`);
1104
1287
  process.exit(1);
1105
1288
  }
@@ -1317,6 +1500,49 @@ async function main() {
1317
1500
  process.exit(0);
1318
1501
  }
1319
1502
 
1503
+ if (args[0] === "issue") {
1504
+ const category = args[1];
1505
+ const message = args.slice(2).join(" ");
1506
+ if (!category || !message) {
1507
+ console.error("Usage: form-tester issue <category> <message>");
1508
+ console.error(`\nCategories: ${ISSUE_CATEGORIES.join(", ")}`);
1509
+ process.exit(1);
1510
+ }
1511
+ if (!ISSUE_CATEGORIES.includes(category)) {
1512
+ console.error(`Unknown category "${category}". Valid: ${ISSUE_CATEGORIES.join(", ")}`);
1513
+ process.exit(1);
1514
+ }
1515
+ const config = loadConfig();
1516
+ const entry = logIssue(category, message, {
1517
+ url: config.lastTestUrl || undefined,
1518
+ formId: config.lastTestUrl ? extractFormId(config.lastTestUrl) : undefined,
1519
+ outputDir: config.lastRunDir || undefined,
1520
+ });
1521
+ console.log(`Issue logged: [${entry.category}] ${entry.message}`);
1522
+ process.exit(0);
1523
+ }
1524
+
1525
+ if (args[0] === "issues") {
1526
+ const limit = Number.parseInt(args[1], 10) || 20;
1527
+ const issues = listIssues(limit);
1528
+ if (!issues.length) {
1529
+ console.log("No issues logged yet.");
1530
+ process.exit(0);
1531
+ }
1532
+ console.log(`Last ${issues.length} issue(s):\n`);
1533
+ for (const issue of issues) {
1534
+ console.log(formatIssue(issue));
1535
+ }
1536
+ process.exit(0);
1537
+ }
1538
+
1539
+ if (args[0] === "documents") {
1540
+ const config = loadConfig();
1541
+ const verbosity = args.includes("--silent") ? "silent" : args.includes("--verbose") ? "verbose" : "normal";
1542
+ const code = await handleDocuments(config, { verbosity });
1543
+ process.exit(code);
1544
+ }
1545
+
1320
1546
  if (args[0] === "test" && args.includes("--human")) {
1321
1547
  const config = loadConfig();
1322
1548
  const url = args.find((a) => a.startsWith("http"));
@@ -1414,5 +1640,8 @@ module.exports = {
1414
1640
  startRecording,
1415
1641
  appendToRecording,
1416
1642
  finalizeRecording,
1643
+ logIssue,
1644
+ listIssues,
1645
+ ISSUE_CATEGORIES,
1417
1646
  };
1418
1647
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-tester",
3
- "version": "0.8.3",
3
+ "version": "0.9.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": {