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.
|
|
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
|
-
##
|
|
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
|
|
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