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.
- package/form-tester.js +137 -185
- 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.
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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 (
|
|
203
|
-
await runPlaywrightCli(["click",
|
|
204
|
-
log(
|
|
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
|
|
260
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
325
|
-
log("PDF download
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
log(
|
|
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
|
|
306
|
+
return 1;
|
|
355
307
|
}
|
|
356
308
|
|
|
357
309
|
// --- Standardized commands: cookies, select-person, validate ---
|
package/package.json
CHANGED