form-tester 0.2.3
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/.claude/skills/playwright-cli/SKILL.md +278 -0
- package/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
- package/.claude/skills/playwright-cli/references/running-code.md +232 -0
- package/.claude/skills/playwright-cli/references/session-management.md +169 -0
- package/.claude/skills/playwright-cli/references/storage-state.md +275 -0
- package/.claude/skills/playwright-cli/references/test-generation.md +88 -0
- package/.claude/skills/playwright-cli/references/tracing.md +139 -0
- package/.claude/skills/playwright-cli/references/video-recording.md +43 -0
- package/README.md +75 -0
- package/form-tester.config.example.json +3 -0
- package/form-tester.js +984 -0
- package/package.json +26 -0
- package/tests/form-tester.test.js +120 -0
package/form-tester.js
ADDED
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const readline = require("readline");
|
|
5
|
+
const { spawn, execSync } = require("child_process");
|
|
6
|
+
|
|
7
|
+
const CONFIG_PATH = path.join(__dirname, "form-tester.config.json");
|
|
8
|
+
const OUTPUT_BASE = path.resolve(__dirname, "output");
|
|
9
|
+
const LOCAL_VERSION = "0.2.3";
|
|
10
|
+
const RECOMMENDED_PERSON = "Uromantisk Direktør";
|
|
11
|
+
|
|
12
|
+
const PERSONAS = [
|
|
13
|
+
{
|
|
14
|
+
id: "ung-mann",
|
|
15
|
+
name: "Ung mann",
|
|
16
|
+
description: "25 år, frisk, ingen medisiner, aktiv livsstil",
|
|
17
|
+
traits: {
|
|
18
|
+
gender: "Mann",
|
|
19
|
+
age: 25,
|
|
20
|
+
smoker: false,
|
|
21
|
+
alcohol: "0-5",
|
|
22
|
+
medications: false,
|
|
23
|
+
allergies: false,
|
|
24
|
+
previousConditions: false,
|
|
25
|
+
exercise: "3-4",
|
|
26
|
+
pregnant: false,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "gravid-kvinne",
|
|
31
|
+
name: "Gravid kvinne",
|
|
32
|
+
description: "32 år, frisk, førstegangsfødende, samboer, yrkesaktiv",
|
|
33
|
+
traits: {
|
|
34
|
+
gender: "Kvinne",
|
|
35
|
+
age: 32,
|
|
36
|
+
smoker: false,
|
|
37
|
+
alcohol: "none",
|
|
38
|
+
medications: false,
|
|
39
|
+
allergies: false,
|
|
40
|
+
previousConditions: false,
|
|
41
|
+
exercise: "1-2",
|
|
42
|
+
pregnant: true,
|
|
43
|
+
firstPregnancy: true,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "eldre-kvinne",
|
|
48
|
+
name: "Eldre kvinne",
|
|
49
|
+
description: "68 år, overgangsalder, tar D-vitamin, lett vektnedgang",
|
|
50
|
+
traits: {
|
|
51
|
+
gender: "Kvinne",
|
|
52
|
+
age: 68,
|
|
53
|
+
smoker: false,
|
|
54
|
+
alcohol: "0-5",
|
|
55
|
+
medications: true,
|
|
56
|
+
allergies: false,
|
|
57
|
+
previousConditions: false,
|
|
58
|
+
exercise: "1-2",
|
|
59
|
+
pregnant: false,
|
|
60
|
+
menopause: true,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "kronisk-syk",
|
|
65
|
+
name: "Kronisk syk mann",
|
|
66
|
+
description: "45 år, diabetes type 2, faste medisiner, røykt tidligere",
|
|
67
|
+
traits: {
|
|
68
|
+
gender: "Mann",
|
|
69
|
+
age: 45,
|
|
70
|
+
smoker: "previously",
|
|
71
|
+
alcohol: "5-10",
|
|
72
|
+
medications: true,
|
|
73
|
+
allergies: true,
|
|
74
|
+
previousConditions: true,
|
|
75
|
+
exercise: "0",
|
|
76
|
+
pregnant: false,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
function getPersonas() {
|
|
82
|
+
return PERSONAS;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getPersonaById(id) {
|
|
86
|
+
return PERSONAS.find((p) => p.id === id) || null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatPersonaList() {
|
|
90
|
+
const lines = PERSONAS.map(
|
|
91
|
+
(p, i) => ` ${i + 1}. ${p.name} — ${p.description}`,
|
|
92
|
+
);
|
|
93
|
+
lines.push(` ${PERSONAS.length + 1}. Noen — tilfeldig / nøytrale svar`);
|
|
94
|
+
lines.push(` ${PERSONAS.length + 2}. Lag egen — beskriv persona selv`);
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function promptScenario() {
|
|
99
|
+
console.log("\nTestscenario (beskriv hva du vil teste, eller trykk Enter for standard test):");
|
|
100
|
+
const answer = await ask("Scenario: ");
|
|
101
|
+
const trimmed = (answer || "").trim();
|
|
102
|
+
if (!trimmed) {
|
|
103
|
+
console.log("Scenario: Standard test — ren utfylling og innsending.");
|
|
104
|
+
return { type: "default", description: "Standard test" };
|
|
105
|
+
}
|
|
106
|
+
console.log(`Scenario: ${trimmed}`);
|
|
107
|
+
return { type: "custom", description: trimmed };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function promptPersona(config) {
|
|
111
|
+
console.log("\nVelg persona for utfylling:");
|
|
112
|
+
console.log(formatPersonaList());
|
|
113
|
+
const answer = await ask(
|
|
114
|
+
`Velg (1-${PERSONAS.length + 2}): `,
|
|
115
|
+
);
|
|
116
|
+
const idx = Number.parseInt(answer, 10);
|
|
117
|
+
if (idx >= 1 && idx <= PERSONAS.length) {
|
|
118
|
+
const persona = PERSONAS[idx - 1];
|
|
119
|
+
console.log(`Persona: ${persona.name} — ${persona.description}`);
|
|
120
|
+
config.lastPersona = persona.id;
|
|
121
|
+
saveConfig(config);
|
|
122
|
+
return { type: "preset", persona };
|
|
123
|
+
}
|
|
124
|
+
if (idx === PERSONAS.length + 1) {
|
|
125
|
+
console.log("Persona: Noen — nøytrale svar");
|
|
126
|
+
config.lastPersona = "noen";
|
|
127
|
+
saveConfig(config);
|
|
128
|
+
return { type: "noen", persona: null };
|
|
129
|
+
}
|
|
130
|
+
if (idx === PERSONAS.length + 2) {
|
|
131
|
+
const description = await ask("Beskriv persona: ");
|
|
132
|
+
console.log(`Persona: Egendefinert — ${description}`);
|
|
133
|
+
config.lastPersona = "custom";
|
|
134
|
+
saveConfig(config);
|
|
135
|
+
return { type: "custom", description };
|
|
136
|
+
}
|
|
137
|
+
console.log("Ugyldig valg, bruker 'Noen'.");
|
|
138
|
+
config.lastPersona = "noen";
|
|
139
|
+
saveConfig(config);
|
|
140
|
+
return { type: "noen", persona: null };
|
|
141
|
+
}
|
|
142
|
+
const DEFAULT_CONFIG = {
|
|
143
|
+
pnr: "",
|
|
144
|
+
dokumenterUrlTemplate: "/dokumenter?pnr={PNR}",
|
|
145
|
+
lastTestUrl: "",
|
|
146
|
+
lastRunDir: "",
|
|
147
|
+
lastSeenVersion: "",
|
|
148
|
+
lastPerson: "",
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function loadConfig() {
|
|
152
|
+
try {
|
|
153
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf8");
|
|
154
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
155
|
+
} catch (err) {
|
|
156
|
+
return { ...DEFAULT_CONFIG };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function saveConfig(config) {
|
|
161
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveDokumenterUrl(config) {
|
|
165
|
+
if (!config.dokumenterUrlTemplate) return "";
|
|
166
|
+
if (config.dokumenterUrlTemplate.includes("{PNR}")) {
|
|
167
|
+
if (!config.pnr) return "";
|
|
168
|
+
return config.dokumenterUrlTemplate.replace("{PNR}", config.pnr);
|
|
169
|
+
}
|
|
170
|
+
return config.dokumenterUrlTemplate;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractPnrFromUrl(url) {
|
|
174
|
+
try {
|
|
175
|
+
const parsed = new URL(url);
|
|
176
|
+
return parsed.searchParams.get("pnr") || "";
|
|
177
|
+
} catch (err) {
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function setPnrOnUrl(url, pnr) {
|
|
183
|
+
try {
|
|
184
|
+
const parsed = new URL(url);
|
|
185
|
+
parsed.searchParams.set("pnr", pnr);
|
|
186
|
+
return parsed.toString();
|
|
187
|
+
} catch (err) {
|
|
188
|
+
const joiner = url.includes("?") ? "&" : "?";
|
|
189
|
+
return `${url}${joiner}pnr=${encodeURIComponent(pnr)}`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function ensurePnrInUrl(url, config, ask) {
|
|
194
|
+
const existing = extractPnrFromUrl(url);
|
|
195
|
+
if (existing) {
|
|
196
|
+
config.pnr = existing;
|
|
197
|
+
saveConfig(config);
|
|
198
|
+
return { url, pnr: existing };
|
|
199
|
+
}
|
|
200
|
+
const answer = await ask("PNR (required because URL lacks pnr=): ");
|
|
201
|
+
const pnr = answer || config.pnr;
|
|
202
|
+
if (!pnr) return { url: "", pnr: "" };
|
|
203
|
+
const updatedUrl = setPnrOnUrl(url, pnr);
|
|
204
|
+
config.pnr = pnr;
|
|
205
|
+
saveConfig(config);
|
|
206
|
+
return { url: updatedUrl, pnr };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function extractFormId(url) {
|
|
210
|
+
try {
|
|
211
|
+
const parsed = new URL(url);
|
|
212
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
213
|
+
const idx = parts.indexOf("skjemautfyller");
|
|
214
|
+
if (idx >= 0 && parts[idx + 1]) return parts[idx + 1];
|
|
215
|
+
return parts[parts.length - 1] || "FORM";
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return "FORM";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sanitizeSegment(value) {
|
|
222
|
+
return value.replace(/[<>:"/\\|?*]+/g, "").trim() || "FORM";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getPlaywrightCommand() {
|
|
226
|
+
if (process.platform === "win32") {
|
|
227
|
+
try {
|
|
228
|
+
const output = execSync(
|
|
229
|
+
"Get-Command playwright-cli -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Path",
|
|
230
|
+
{ stdio: ["ignore", "pipe", "ignore"], shell: "powershell" },
|
|
231
|
+
)
|
|
232
|
+
.toString()
|
|
233
|
+
.trim();
|
|
234
|
+
if (output) {
|
|
235
|
+
if (output.toLowerCase().endsWith(".ps1")) {
|
|
236
|
+
const cmdPath = output.replace(/\.ps1$/i, ".cmd");
|
|
237
|
+
if (fs.existsSync(cmdPath)) return cmdPath;
|
|
238
|
+
}
|
|
239
|
+
return output;
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
// fall through to default
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return "playwright-cli";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isPlaywrightCliAvailable() {
|
|
249
|
+
if (process.platform === "win32") {
|
|
250
|
+
try {
|
|
251
|
+
const output = execSync(
|
|
252
|
+
"Get-Command playwright-cli -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Path",
|
|
253
|
+
{ stdio: ["ignore", "pipe", "ignore"], shell: "powershell" },
|
|
254
|
+
)
|
|
255
|
+
.toString()
|
|
256
|
+
.trim();
|
|
257
|
+
return Boolean(output);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
execSync("command -v playwright-cli", {
|
|
264
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
265
|
+
shell: true,
|
|
266
|
+
});
|
|
267
|
+
return true;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function runCommand(command, args, options = {}) {
|
|
274
|
+
return new Promise((resolve) => {
|
|
275
|
+
const child = spawn(command, args, { stdio: "inherit", ...options });
|
|
276
|
+
child.on("error", (err) => {
|
|
277
|
+
console.error(`Failed to launch ${command}: ${err.message}`);
|
|
278
|
+
resolve(1);
|
|
279
|
+
});
|
|
280
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function runPlaywrightCli(args) {
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
const spec = getPlaywrightCommandSpec();
|
|
287
|
+
const child = spawn(spec.command, [...spec.args, ...args], {
|
|
288
|
+
stdio: "inherit",
|
|
289
|
+
});
|
|
290
|
+
child.on("error", (err) => {
|
|
291
|
+
console.error(`Failed to launch ${spec.command}: ${err.message}`);
|
|
292
|
+
resolve(1);
|
|
293
|
+
});
|
|
294
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function runPlaywrightCliCapture(args) {
|
|
299
|
+
return new Promise((resolve) => {
|
|
300
|
+
const spec = getPlaywrightCommandSpec();
|
|
301
|
+
const child = spawn(
|
|
302
|
+
spec.command,
|
|
303
|
+
[...spec.args, ...args],
|
|
304
|
+
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
305
|
+
);
|
|
306
|
+
let stdout = "";
|
|
307
|
+
let stderr = "";
|
|
308
|
+
child.stdout.on("data", (data) => {
|
|
309
|
+
stdout += data.toString();
|
|
310
|
+
});
|
|
311
|
+
child.stderr.on("data", (data) => {
|
|
312
|
+
stderr += data.toString();
|
|
313
|
+
});
|
|
314
|
+
child.on("error", (err) => {
|
|
315
|
+
resolve({ code: 1, stdout: "", stderr: err.message });
|
|
316
|
+
});
|
|
317
|
+
child.on("exit", (code) =>
|
|
318
|
+
resolve({
|
|
319
|
+
code: code ?? 0,
|
|
320
|
+
stdout: stdout.trim(),
|
|
321
|
+
stderr: stderr.trim(),
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function getPlaywrightCommandSpec() {
|
|
328
|
+
const command = getPlaywrightCommand();
|
|
329
|
+
const lower = command.toLowerCase();
|
|
330
|
+
if (process.platform === "win32" && (lower.endsWith(".cmd") || lower.endsWith(".ps1"))) {
|
|
331
|
+
const nodeDir = path.dirname(command);
|
|
332
|
+
const cliPath = path.join(
|
|
333
|
+
nodeDir,
|
|
334
|
+
"node_modules",
|
|
335
|
+
"@playwright",
|
|
336
|
+
"cli",
|
|
337
|
+
"playwright-cli.js",
|
|
338
|
+
);
|
|
339
|
+
if (fs.existsSync(cliPath)) {
|
|
340
|
+
return { command: process.execPath, args: [cliPath] };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (lower.endsWith(".ps1")) {
|
|
344
|
+
return {
|
|
345
|
+
command: "powershell",
|
|
346
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return { command, args: [] };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function findRepoRoot(startDir) {
|
|
353
|
+
let current = startDir;
|
|
354
|
+
while (true) {
|
|
355
|
+
const gitPath = path.join(current, ".git");
|
|
356
|
+
if (fs.existsSync(gitPath)) return current;
|
|
357
|
+
const parent = path.dirname(current);
|
|
358
|
+
if (parent === current) return null;
|
|
359
|
+
current = parent;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let rl = null;
|
|
364
|
+
|
|
365
|
+
function ensureReadline() {
|
|
366
|
+
if (!rl) {
|
|
367
|
+
rl = readline.createInterface({
|
|
368
|
+
input: process.stdin,
|
|
369
|
+
output: process.stdout,
|
|
370
|
+
prompt: "form-tester> ",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return rl;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function ask(question) {
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
const currentRl = ensureReadline();
|
|
379
|
+
currentRl.question(question, (answer) => resolve(answer.trim()));
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function clearConsole() {
|
|
384
|
+
if (typeof console.clear === "function") {
|
|
385
|
+
console.clear();
|
|
386
|
+
} else {
|
|
387
|
+
process.stdout.write("\x1Bc");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function printHelp() {
|
|
392
|
+
console.log(
|
|
393
|
+
[
|
|
394
|
+
"Commands:",
|
|
395
|
+
" /setup Install Playwright CLI + skills if missing",
|
|
396
|
+
" /update Update repo (if git), Playwright CLI, and skills",
|
|
397
|
+
" /version Show local skill version",
|
|
398
|
+
" /people Scan visible person list and prompt selection",
|
|
399
|
+
" /persona List available personas",
|
|
400
|
+
" /test {url} Open form URL with Playwright CLI and save initial artifacts",
|
|
401
|
+
" /save {label} Save snapshot + screenshot to last output folder",
|
|
402
|
+
" /clear Clear the console",
|
|
403
|
+
" /help Show this help",
|
|
404
|
+
" /exit Exit the app",
|
|
405
|
+
" /quit Exit the app",
|
|
406
|
+
].join("\n"),
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function printVersion(config) {
|
|
411
|
+
const saved = config.lastSeenVersion || "unknown";
|
|
412
|
+
console.log(`Local version: ${LOCAL_VERSION}`);
|
|
413
|
+
console.log(`Saved version: ${saved}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function printNextSteps(outputDir, dokumenterUrl) {
|
|
417
|
+
console.log("");
|
|
418
|
+
console.log("Next steps (manual Playwright CLI):");
|
|
419
|
+
console.log('- Accept cookies if prompted.');
|
|
420
|
+
console.log('- Use the person selection prompt to choose the correct person.');
|
|
421
|
+
console.log('- Fill required fields with realistic Norwegian data.');
|
|
422
|
+
console.log(
|
|
423
|
+
"- If validation errors remain after submit, click the label/fieldset inside error blocks to trigger validation.",
|
|
424
|
+
);
|
|
425
|
+
console.log(
|
|
426
|
+
`- Save often: playwright-cli snapshot --filename "${path.join(outputDir, "step.yml")}"`,
|
|
427
|
+
);
|
|
428
|
+
console.log(`- Or use: /save step`);
|
|
429
|
+
console.log(
|
|
430
|
+
`- Screenshot before submit: playwright-cli screenshot --filename "${path.join(outputDir, "before_submit.png")}"`,
|
|
431
|
+
);
|
|
432
|
+
console.log("- Submit only when validation errors are cleared.");
|
|
433
|
+
console.log(
|
|
434
|
+
'- If you see the modal "Det skjedde en feil under innsending av skjema. Prøv igjen senere." on save or submit, open DevTools -> Network before retrying. Then try resubmitting once. If it persists, record the Correlation ID header in test_results.txt.',
|
|
435
|
+
);
|
|
436
|
+
console.log(`- Open Dokumenter: playwright-cli goto "${dokumenterUrl}"`);
|
|
437
|
+
console.log("- Select the same person if prompted.");
|
|
438
|
+
console.log("- Wait for the Dokumenter list to load; the first item is the latest.");
|
|
439
|
+
console.log("- Document name should match the form page h1#sidetittel.");
|
|
440
|
+
console.log(
|
|
441
|
+
`- Save Dokumenter list: playwright-cli snapshot --filename "${path.join(outputDir, "dokumenter.yml")}"`,
|
|
442
|
+
);
|
|
443
|
+
console.log("- Click the first document link to open it.");
|
|
444
|
+
console.log("- Wait for the document HTML to render fully.");
|
|
445
|
+
console.log(
|
|
446
|
+
`- Save full-page document screenshot: playwright-cli screenshot --filename "${path.join(outputDir, "document_screenshot.png")}" --full-page`,
|
|
447
|
+
);
|
|
448
|
+
console.log(
|
|
449
|
+
`- Save document snapshot: playwright-cli snapshot --filename "${path.join(outputDir, "document.yml")}"`,
|
|
450
|
+
);
|
|
451
|
+
console.log(
|
|
452
|
+
`- Save HTML: playwright-cli eval "document.documentElement.outerHTML" > "${path.join(outputDir, "document.html")}"`,
|
|
453
|
+
);
|
|
454
|
+
console.log(
|
|
455
|
+
`- Record results: write test_results.txt in "${outputDir}" with status and notes.`,
|
|
456
|
+
);
|
|
457
|
+
console.log("- Close browser when done: playwright-cli close");
|
|
458
|
+
console.log("");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function normalizeLabel(value) {
|
|
462
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function sleep(ms) {
|
|
466
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function extractJsonArray(text) {
|
|
470
|
+
const match = text.match(/\[[\s\S]*\]/);
|
|
471
|
+
if (!match) return null;
|
|
472
|
+
try {
|
|
473
|
+
const parsed = JSON.parse(match[0]);
|
|
474
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function extractResultBlock(output) {
|
|
481
|
+
const lines = output.split(/\r?\n/);
|
|
482
|
+
const start = lines.findIndex((line) => line.trim() === "### Result");
|
|
483
|
+
if (start < 0) return null;
|
|
484
|
+
const collected = [];
|
|
485
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
486
|
+
const line = lines[i];
|
|
487
|
+
if (line.trim().startsWith("###")) break;
|
|
488
|
+
if (line.trim()) collected.push(line);
|
|
489
|
+
}
|
|
490
|
+
return collected.join("\n").trim();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function sanitizePersonOptions(list) {
|
|
494
|
+
const normalized = list
|
|
495
|
+
.map(normalizeLabel)
|
|
496
|
+
.filter(Boolean)
|
|
497
|
+
.filter((item) => item.length <= 60);
|
|
498
|
+
const seen = new Set();
|
|
499
|
+
return normalized.filter((item) => {
|
|
500
|
+
const key = item.toLowerCase();
|
|
501
|
+
if (seen.has(key)) return false;
|
|
502
|
+
seen.add(key);
|
|
503
|
+
return true;
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function prioritizeRecommended(list, recommendedLabel) {
|
|
508
|
+
const recommended = normalizeLabel(recommendedLabel).toLowerCase();
|
|
509
|
+
const preferred = [];
|
|
510
|
+
const rest = [];
|
|
511
|
+
for (const item of list) {
|
|
512
|
+
if (item.toLowerCase() === recommended) {
|
|
513
|
+
preferred.push(item);
|
|
514
|
+
} else {
|
|
515
|
+
rest.push(item);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return [...preferred, ...rest];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function parsePersonList(output) {
|
|
522
|
+
const resultBlock = extractResultBlock(output);
|
|
523
|
+
const source = resultBlock || output;
|
|
524
|
+
const json = extractJsonArray(source);
|
|
525
|
+
if (json) return sanitizePersonOptions(json);
|
|
526
|
+
return sanitizePersonOptions(source.split(/\r?\n/));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function extractPersonsFromSnapshotText(text) {
|
|
530
|
+
const lines = text.split(/\r?\n/);
|
|
531
|
+
const header = 'region "Hvem vil du bruke Helsenorge på vegne av?"';
|
|
532
|
+
let regionIndent = null;
|
|
533
|
+
let inRegion = false;
|
|
534
|
+
const names = [];
|
|
535
|
+
const buttons = [];
|
|
536
|
+
for (const line of lines) {
|
|
537
|
+
const indentMatch = line.match(/^(\s*)/);
|
|
538
|
+
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
539
|
+
if (!inRegion) {
|
|
540
|
+
if (line.includes(header)) {
|
|
541
|
+
inRegion = true;
|
|
542
|
+
regionIndent = indent;
|
|
543
|
+
}
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (indent <= regionIndent && line.trim().startsWith("-")) {
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
const strongMatch = line.match(/- strong .*?:\s*(.+)$/);
|
|
550
|
+
if (strongMatch) {
|
|
551
|
+
names.push(strongMatch[1].trim());
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const buttonMatch = line.match(/- button "([^"]+)"/);
|
|
555
|
+
if (buttonMatch) {
|
|
556
|
+
buttons.push(buttonMatch[1].trim());
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (names.length) return sanitizePersonOptions(names);
|
|
560
|
+
return sanitizePersonOptions(buttons);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function extractPersonsFromSnapshotFile(snapshotPath) {
|
|
564
|
+
if (!snapshotPath || !fs.existsSync(snapshotPath)) return [];
|
|
565
|
+
const text = fs.readFileSync(snapshotPath, "utf8");
|
|
566
|
+
return extractPersonsFromSnapshotText(text);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function fetchPersonOptions() {
|
|
570
|
+
const script = [
|
|
571
|
+
"() => {",
|
|
572
|
+
'const normalize = (value) => String(value || "").replace(/\\s+/g, " ").trim();',
|
|
573
|
+
"const isVisible = (el) => {",
|
|
574
|
+
"if (!el) return false;",
|
|
575
|
+
"const style = window.getComputedStyle(el);",
|
|
576
|
+
'if (!style || style.display === "none" || style.visibility === "hidden") {',
|
|
577
|
+
"return false;",
|
|
578
|
+
"}",
|
|
579
|
+
"const rect = el.getBoundingClientRect();",
|
|
580
|
+
"return rect.width >= 4 && rect.height >= 4;",
|
|
581
|
+
"};",
|
|
582
|
+
'const textOf = (el) => normalize(el.innerText || el.textContent || "");',
|
|
583
|
+
'const itemSelectors = ["[role=\\"option\\"]","[role=\\"listitem\\"]","li","button","a","div","span"];',
|
|
584
|
+
"const containerSelectors = [",
|
|
585
|
+
'"[role=\\"listbox\\"]",',
|
|
586
|
+
'"[role=\\"list\\"]",',
|
|
587
|
+
'"ul",',
|
|
588
|
+
'"ol",',
|
|
589
|
+
'"[data-testid*=\\"person\\" i]",',
|
|
590
|
+
'"[data-testid*=\\"people\\" i]",',
|
|
591
|
+
'"[aria-label*=\\"person\\" i]",',
|
|
592
|
+
'"[aria-label*=\\"personer\\" i]"',
|
|
593
|
+
"];",
|
|
594
|
+
"const containers = containerSelectors",
|
|
595
|
+
".flatMap((sel) => Array.from(document.querySelectorAll(sel)))",
|
|
596
|
+
".filter(isVisible);",
|
|
597
|
+
"const lists = containers",
|
|
598
|
+
".map((container) => {",
|
|
599
|
+
"const items = Array.from(container.querySelectorAll(itemSelectors.join(\",\")))",
|
|
600
|
+
".filter(isVisible)",
|
|
601
|
+
".map(textOf)",
|
|
602
|
+
".filter(Boolean)",
|
|
603
|
+
".filter((text) => text.length <= 60);",
|
|
604
|
+
"return Array.from(new Set(items));",
|
|
605
|
+
"})",
|
|
606
|
+
".filter((list) => list.length >= 2 && list.length <= 30);",
|
|
607
|
+
"if (lists.length) { return lists.sort((a, b) => b.length - a.length)[0]; }",
|
|
608
|
+
"const labeled = Array.from(document.querySelectorAll(",
|
|
609
|
+
"\"[data-testid*=\\\\\"person\\\\\" i],",
|
|
610
|
+
"[data-testid*=\\\\\"personer\\\\\" i],",
|
|
611
|
+
"[aria-label*=\\\\\"person\\\\\" i],",
|
|
612
|
+
"[aria-label*=\\\\\"personer\\\\\" i]\"",
|
|
613
|
+
"))",
|
|
614
|
+
".map(textOf)",
|
|
615
|
+
".filter(Boolean)",
|
|
616
|
+
".filter((text) => text.length <= 60);",
|
|
617
|
+
"if (labeled.length) { return Array.from(new Set(labeled)); }",
|
|
618
|
+
"const scope = document.querySelector(\"main\") || document.body;",
|
|
619
|
+
"const fallback = Array.from(scope.querySelectorAll(",
|
|
620
|
+
"\"button,[role=\\\\\"button\\\\\"],a,[role=\\\\\"link\\\\\"],li,[role=\\\\\"listitem\\\\\"],",
|
|
621
|
+
"[role=\\\\\"option\\\\\"]\"",
|
|
622
|
+
"))",
|
|
623
|
+
".filter(isVisible)",
|
|
624
|
+
".map(textOf)",
|
|
625
|
+
".filter(Boolean)",
|
|
626
|
+
".filter((text) => text.length <= 60);",
|
|
627
|
+
"return Array.from(new Set(fallback));",
|
|
628
|
+
"}",
|
|
629
|
+
].join(" ");
|
|
630
|
+
|
|
631
|
+
const result = await runPlaywrightCliCapture(["eval", script]);
|
|
632
|
+
const combinedOutput = `${result.stdout}\n${result.stderr}`.trim();
|
|
633
|
+
if (result.code !== 0 || combinedOutput.startsWith("### Error")) {
|
|
634
|
+
console.log(
|
|
635
|
+
`Failed to read person list from browser (${combinedOutput || "unknown error"}).`,
|
|
636
|
+
);
|
|
637
|
+
return [];
|
|
638
|
+
}
|
|
639
|
+
return parsePersonList(result.stdout);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async function promptPersonSelection(config) {
|
|
643
|
+
const answer = await ask(
|
|
644
|
+
"Open the person picker, then press Enter to scan (or type skip): ",
|
|
645
|
+
);
|
|
646
|
+
if (/^skip$/i.test(answer)) return;
|
|
647
|
+
let options = extractPersonsFromSnapshotFile(
|
|
648
|
+
config.lastRunDir
|
|
649
|
+
? path.join(config.lastRunDir, "page_open.yml")
|
|
650
|
+
: "",
|
|
651
|
+
);
|
|
652
|
+
if (!options.length) {
|
|
653
|
+
options = [];
|
|
654
|
+
}
|
|
655
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
656
|
+
if (attempt > 0) {
|
|
657
|
+
await sleep(1500);
|
|
658
|
+
}
|
|
659
|
+
if (!options.length) {
|
|
660
|
+
options = await fetchPersonOptions();
|
|
661
|
+
}
|
|
662
|
+
if (options.length) break;
|
|
663
|
+
}
|
|
664
|
+
options = prioritizeRecommended(options, RECOMMENDED_PERSON);
|
|
665
|
+
if (!options.length) {
|
|
666
|
+
console.log(
|
|
667
|
+
"No person options detected. Open the picker and run /people to retry.",
|
|
668
|
+
);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
console.log("Select a person:");
|
|
672
|
+
options.forEach((option, index) => {
|
|
673
|
+
const recommended =
|
|
674
|
+
option.toLowerCase() === RECOMMENDED_PERSON.toLowerCase()
|
|
675
|
+
? " (Recommended)"
|
|
676
|
+
: "";
|
|
677
|
+
console.log(`${index + 1}: ${option}${recommended}`);
|
|
678
|
+
});
|
|
679
|
+
while (true) {
|
|
680
|
+
const selection = await ask(`Select person by number (1-${options.length}): `);
|
|
681
|
+
const idx = Number.parseInt(selection, 10);
|
|
682
|
+
if (!Number.isNaN(idx) && idx >= 1 && idx <= options.length) {
|
|
683
|
+
const chosen = options[idx - 1];
|
|
684
|
+
console.log(`Select this person in the UI: ${chosen}`);
|
|
685
|
+
config.lastPerson = chosen;
|
|
686
|
+
saveConfig(config);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
console.log("Invalid selection. Try again or type /clear then /people.");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function saveArtifacts(config, label) {
|
|
694
|
+
if (!config.lastRunDir) {
|
|
695
|
+
console.log("No output folder yet. Run /test first.");
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const safeLabel = sanitizeSegment(label || "save");
|
|
699
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
700
|
+
const base = path.join(config.lastRunDir, `${safeLabel}_${timestamp}`);
|
|
701
|
+
await runPlaywrightCli(["snapshot", "--filename", `${base}.yml`]);
|
|
702
|
+
await runPlaywrightCli(["screenshot", "--filename", `${base}.png`]);
|
|
703
|
+
console.log(`Saved: ${base}.yml and ${base}.png`);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function handleSetup() {
|
|
707
|
+
if (!isPlaywrightCliAvailable()) {
|
|
708
|
+
const answer = await ask(
|
|
709
|
+
"Playwright CLI is not installed. Install globally now? (y/n): ",
|
|
710
|
+
);
|
|
711
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
712
|
+
console.log("Skipped installation.");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const installCode = await runCommand("npm", [
|
|
716
|
+
"install",
|
|
717
|
+
"-g",
|
|
718
|
+
"@playwright/cli@latest",
|
|
719
|
+
]);
|
|
720
|
+
if (installCode !== 0) {
|
|
721
|
+
console.log("Install failed. Fix npm and re-run /setup.");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const skillsCode = await runPlaywrightCli(["install", "--skills"]);
|
|
727
|
+
if (skillsCode === 0) {
|
|
728
|
+
console.log("Playwright CLI skills installed.");
|
|
729
|
+
} else {
|
|
730
|
+
console.log("Failed to install Playwright CLI skills.");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function handleUpdate(config) {
|
|
735
|
+
const repoRoot = findRepoRoot(__dirname);
|
|
736
|
+
if (repoRoot) {
|
|
737
|
+
console.log(`Updating repository in ${repoRoot}...`);
|
|
738
|
+
const gitCode = await runCommand("git", [
|
|
739
|
+
"-C",
|
|
740
|
+
repoRoot,
|
|
741
|
+
"pull",
|
|
742
|
+
"--ff-only",
|
|
743
|
+
]);
|
|
744
|
+
if (gitCode !== 0) {
|
|
745
|
+
console.log("Git update failed. Fix git and re-run /update.");
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
console.log("No git repo found. Skipping code update.");
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const updateAnswer = await ask(
|
|
753
|
+
"Update Playwright CLI globally now? (y/n): ",
|
|
754
|
+
);
|
|
755
|
+
if (/^y(es)?$/i.test(updateAnswer)) {
|
|
756
|
+
const installCode = await runCommand("npm", [
|
|
757
|
+
"install",
|
|
758
|
+
"-g",
|
|
759
|
+
"@playwright/cli@latest",
|
|
760
|
+
]);
|
|
761
|
+
if (installCode !== 0) {
|
|
762
|
+
console.log("Playwright CLI update failed. Fix npm and re-run /update.");
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
console.log("Skipped Playwright CLI update.");
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const skillsCode = await runPlaywrightCli(["install", "--skills"]);
|
|
770
|
+
if (skillsCode === 0) {
|
|
771
|
+
console.log("Playwright CLI skills installed.");
|
|
772
|
+
config.lastSeenVersion = LOCAL_VERSION;
|
|
773
|
+
saveConfig(config);
|
|
774
|
+
} else {
|
|
775
|
+
console.log("Failed to install Playwright CLI skills.");
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
console.log(
|
|
780
|
+
"Reload the skill in Copilot CLI with /skills (reload) or /restart.",
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async function handleVersionMismatch(config) {
|
|
785
|
+
if (!config.lastSeenVersion) {
|
|
786
|
+
config.lastSeenVersion = LOCAL_VERSION;
|
|
787
|
+
saveConfig(config);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (config.lastSeenVersion === LOCAL_VERSION) return;
|
|
791
|
+
console.log(
|
|
792
|
+
`Version change detected: saved ${config.lastSeenVersion} -> local ${LOCAL_VERSION}.`,
|
|
793
|
+
);
|
|
794
|
+
const answer = await ask("Run /update now? (y/n): ");
|
|
795
|
+
if (/^y(es)?$/i.test(answer)) {
|
|
796
|
+
await handleUpdate(config);
|
|
797
|
+
} else {
|
|
798
|
+
console.log("You can run /update later to refresh dependencies.");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function handleTest(url, config) {
|
|
803
|
+
const updated = await ensurePnrInUrl(url, config, ask);
|
|
804
|
+
if (!updated.pnr) {
|
|
805
|
+
console.log("PNR is required when not included in the URL.");
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const personaChoice = await promptPersona(config);
|
|
810
|
+
const scenarioChoice = await promptScenario();
|
|
811
|
+
|
|
812
|
+
const formId = sanitizeSegment(extractFormId(updated.url));
|
|
813
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
814
|
+
const outputDir = path.join(OUTPUT_BASE, formId, timestamp);
|
|
815
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
816
|
+
|
|
817
|
+
config.lastTestUrl = updated.url;
|
|
818
|
+
config.lastRunDir = outputDir;
|
|
819
|
+
saveConfig(config);
|
|
820
|
+
|
|
821
|
+
if (personaChoice.type === "preset") {
|
|
822
|
+
fs.writeFileSync(
|
|
823
|
+
path.join(outputDir, "persona.json"),
|
|
824
|
+
JSON.stringify(personaChoice.persona, null, 2),
|
|
825
|
+
);
|
|
826
|
+
} else if (personaChoice.type === "custom") {
|
|
827
|
+
fs.writeFileSync(
|
|
828
|
+
path.join(outputDir, "persona.json"),
|
|
829
|
+
JSON.stringify(
|
|
830
|
+
{ id: "custom", name: "Egendefinert", description: personaChoice.description, traits: {} },
|
|
831
|
+
null,
|
|
832
|
+
2,
|
|
833
|
+
),
|
|
834
|
+
);
|
|
835
|
+
} else {
|
|
836
|
+
fs.writeFileSync(
|
|
837
|
+
path.join(outputDir, "persona.json"),
|
|
838
|
+
JSON.stringify(
|
|
839
|
+
{ id: "noen", name: "Noen", description: "Nøytrale svar", traits: {} },
|
|
840
|
+
null,
|
|
841
|
+
2,
|
|
842
|
+
),
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
fs.writeFileSync(
|
|
847
|
+
path.join(outputDir, "scenario.json"),
|
|
848
|
+
JSON.stringify(scenarioChoice, null, 2),
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
console.log("Opening form with Playwright CLI...");
|
|
852
|
+
await runPlaywrightCli(["open", updated.url]);
|
|
853
|
+
await runPlaywrightCli([
|
|
854
|
+
"snapshot",
|
|
855
|
+
"--filename",
|
|
856
|
+
path.join(outputDir, "page_open.yml"),
|
|
857
|
+
]);
|
|
858
|
+
await runPlaywrightCli([
|
|
859
|
+
"screenshot",
|
|
860
|
+
"--filename",
|
|
861
|
+
path.join(outputDir, "page_open.png"),
|
|
862
|
+
]);
|
|
863
|
+
|
|
864
|
+
await promptPersonSelection(config);
|
|
865
|
+
|
|
866
|
+
const dokumenterUrl = resolveDokumenterUrl(config);
|
|
867
|
+
console.log(`Output folder: ${outputDir}`);
|
|
868
|
+
if (dokumenterUrl) {
|
|
869
|
+
console.log(`Dokumenter URL: ${dokumenterUrl}`);
|
|
870
|
+
}
|
|
871
|
+
printNextSteps(
|
|
872
|
+
outputDir,
|
|
873
|
+
dokumenterUrl ||
|
|
874
|
+
"/dokumenter?pnr={PNR}",
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function handleCommand(line, config) {
|
|
879
|
+
const trimmed = line.trim();
|
|
880
|
+
if (!trimmed) return;
|
|
881
|
+
|
|
882
|
+
let input = trimmed;
|
|
883
|
+
if (!input.startsWith("/")) {
|
|
884
|
+
input = `/test ${input}`;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const [command, ...rest] = input.split(" ");
|
|
888
|
+
const arg = rest.join(" ").trim();
|
|
889
|
+
|
|
890
|
+
switch (command) {
|
|
891
|
+
case "/help":
|
|
892
|
+
printHelp();
|
|
893
|
+
break;
|
|
894
|
+
case "/setup":
|
|
895
|
+
await handleSetup();
|
|
896
|
+
break;
|
|
897
|
+
case "/update":
|
|
898
|
+
await handleUpdate(config);
|
|
899
|
+
break;
|
|
900
|
+
case "/version":
|
|
901
|
+
printVersion(config);
|
|
902
|
+
break;
|
|
903
|
+
case "/people":
|
|
904
|
+
await promptPersonSelection(config);
|
|
905
|
+
break;
|
|
906
|
+
case "/persona":
|
|
907
|
+
console.log("\nTilgjengelige personas:");
|
|
908
|
+
console.log(formatPersonaList());
|
|
909
|
+
break;
|
|
910
|
+
case "/save": {
|
|
911
|
+
const label = arg || (await ask("Label: "));
|
|
912
|
+
await saveArtifacts(config, label);
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
case "/clear":
|
|
916
|
+
case "/cls":
|
|
917
|
+
clearConsole();
|
|
918
|
+
break;
|
|
919
|
+
case "/test": {
|
|
920
|
+
const url = arg || (await ask("Form URL: "));
|
|
921
|
+
if (!url) {
|
|
922
|
+
console.log("Form URL is required.");
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
await handleTest(url, config);
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
case "/exit":
|
|
929
|
+
case "/quit":
|
|
930
|
+
ensureReadline().close();
|
|
931
|
+
break;
|
|
932
|
+
default:
|
|
933
|
+
console.log("Unknown command. Use /help.");
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async function main() {
|
|
938
|
+
const config = loadConfig();
|
|
939
|
+
await handleVersionMismatch(config);
|
|
940
|
+
|
|
941
|
+
const args = process.argv.slice(2);
|
|
942
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
943
|
+
printHelp();
|
|
944
|
+
process.exit(0);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
console.log("Form tester CLI (Playwright CLI wrapper)");
|
|
948
|
+
console.log("Type /help for commands.");
|
|
949
|
+
const currentRl = ensureReadline();
|
|
950
|
+
currentRl.prompt();
|
|
951
|
+
|
|
952
|
+
currentRl.on("line", async (line) => {
|
|
953
|
+
try {
|
|
954
|
+
await handleCommand(line, config);
|
|
955
|
+
} catch (err) {
|
|
956
|
+
console.error(`Error: ${err.message}`);
|
|
957
|
+
}
|
|
958
|
+
currentRl.prompt();
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
currentRl.on("close", () => {
|
|
962
|
+
process.exit(0);
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (require.main === module) {
|
|
967
|
+
main();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
module.exports = {
|
|
971
|
+
extractFormId,
|
|
972
|
+
extractPnrFromUrl,
|
|
973
|
+
setPnrOnUrl,
|
|
974
|
+
ensurePnrInUrl,
|
|
975
|
+
sanitizeSegment,
|
|
976
|
+
resolveDokumenterUrl,
|
|
977
|
+
prioritizeRecommended,
|
|
978
|
+
parsePersonList,
|
|
979
|
+
getPersonas,
|
|
980
|
+
getPersonaById,
|
|
981
|
+
formatPersonaList,
|
|
982
|
+
promptScenario,
|
|
983
|
+
};
|
|
984
|
+
|