form-tester 0.3.4 → 0.4.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/.claude/skills/form-tester/SKILL.md +7 -1
- package/form-tester.js +116 -14
- package/package.json +1 -1
|
@@ -10,9 +10,15 @@ allowed-tools: Bash(powershell:*), Bash(playwright-cli:*), Bash(npx form-tester:
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npx form-tester install # one-time setup
|
|
13
|
-
npx form-tester #
|
|
13
|
+
npx form-tester # interactive CLI
|
|
14
|
+
|
|
15
|
+
# Non-interactive mode (no prompts, best for AI agents):
|
|
16
|
+
npx form-tester test <url> --auto
|
|
17
|
+
npx form-tester test <url> --auto --pnr 12345 --persona ung-mann --scenario "test validation"
|
|
14
18
|
```
|
|
15
19
|
|
|
20
|
+
Persona IDs: `ung-mann`, `gravid-kvinne`, `eldre-kvinne`, `kronisk-syk-mann`. Defaults to "noen" (neutral answers) if omitted.
|
|
21
|
+
|
|
16
22
|
## Commands
|
|
17
23
|
|
|
18
24
|
```bash
|
package/form-tester.js
CHANGED
|
@@ -6,7 +6,7 @@ const { spawn, execSync } = require("child_process");
|
|
|
6
6
|
|
|
7
7
|
const CONFIG_PATH = path.join(__dirname, "form-tester.config.json");
|
|
8
8
|
const OUTPUT_BASE = path.resolve(__dirname, "output");
|
|
9
|
-
const LOCAL_VERSION = "0.
|
|
9
|
+
const LOCAL_VERSION = "0.4.0";
|
|
10
10
|
const RECOMMENDED_PERSON = "Uromantisk Direktør";
|
|
11
11
|
|
|
12
12
|
const PERSONAS = [
|
|
@@ -272,7 +272,11 @@ function isPlaywrightCliAvailable() {
|
|
|
272
272
|
|
|
273
273
|
function runCommand(command, args, options = {}) {
|
|
274
274
|
return new Promise((resolve) => {
|
|
275
|
-
const child = spawn(command, args, {
|
|
275
|
+
const child = spawn(command, args, {
|
|
276
|
+
stdio: "inherit",
|
|
277
|
+
shell: process.platform === "win32",
|
|
278
|
+
...options,
|
|
279
|
+
});
|
|
276
280
|
child.on("error", (err) => {
|
|
277
281
|
console.error(`Failed to launch ${command}: ${err.message}`);
|
|
278
282
|
resolve(1);
|
|
@@ -284,9 +288,9 @@ function runCommand(command, args, options = {}) {
|
|
|
284
288
|
function runPlaywrightCli(args) {
|
|
285
289
|
return new Promise((resolve) => {
|
|
286
290
|
const spec = getPlaywrightCommandSpec();
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
291
|
+
const spawnOpts = { stdio: "inherit" };
|
|
292
|
+
if (spec.shell) spawnOpts.shell = true;
|
|
293
|
+
const child = spawn(spec.command, [...spec.args, ...args], spawnOpts);
|
|
290
294
|
child.on("error", (err) => {
|
|
291
295
|
console.error(`Failed to launch ${spec.command}: ${err.message}`);
|
|
292
296
|
resolve(1);
|
|
@@ -298,10 +302,12 @@ function runPlaywrightCli(args) {
|
|
|
298
302
|
function runPlaywrightCliCapture(args) {
|
|
299
303
|
return new Promise((resolve) => {
|
|
300
304
|
const spec = getPlaywrightCommandSpec();
|
|
305
|
+
const captureOpts = { stdio: ["ignore", "pipe", "pipe"] };
|
|
306
|
+
if (spec.shell) captureOpts.shell = true;
|
|
301
307
|
const child = spawn(
|
|
302
308
|
spec.command,
|
|
303
309
|
[...spec.args, ...args],
|
|
304
|
-
|
|
310
|
+
captureOpts,
|
|
305
311
|
);
|
|
306
312
|
let stdout = "";
|
|
307
313
|
let stderr = "";
|
|
@@ -328,6 +334,7 @@ function getPlaywrightCommandSpec() {
|
|
|
328
334
|
const command = getPlaywrightCommand();
|
|
329
335
|
const lower = command.toLowerCase();
|
|
330
336
|
if (process.platform === "win32" && (lower.endsWith(".cmd") || lower.endsWith(".ps1"))) {
|
|
337
|
+
// Try to resolve the actual .js entry point to avoid shell/spawn issues
|
|
331
338
|
const nodeDir = path.dirname(command);
|
|
332
339
|
const cliPath = path.join(
|
|
333
340
|
nodeDir,
|
|
@@ -337,16 +344,12 @@ function getPlaywrightCommandSpec() {
|
|
|
337
344
|
"playwright-cli.js",
|
|
338
345
|
);
|
|
339
346
|
if (fs.existsSync(cliPath)) {
|
|
340
|
-
return { command: process.execPath, args: [cliPath] };
|
|
347
|
+
return { command: process.execPath, args: [cliPath], shell: false };
|
|
341
348
|
}
|
|
349
|
+
// Fallback: run via node process.execPath with the .cmd/.ps1 content
|
|
350
|
+
return { command, args: [], shell: true };
|
|
342
351
|
}
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
command: "powershell",
|
|
346
|
-
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command],
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
return { command, args: [] };
|
|
352
|
+
return { command, args: [], shell: false };
|
|
350
353
|
}
|
|
351
354
|
|
|
352
355
|
function findRepoRoot(startDir) {
|
|
@@ -875,6 +878,91 @@ async function handleTest(url, config) {
|
|
|
875
878
|
);
|
|
876
879
|
}
|
|
877
880
|
|
|
881
|
+
async function handleTestAuto(url, config, flags) {
|
|
882
|
+
// Resolve PNR
|
|
883
|
+
const pnr = flags.pnr || config.pnr;
|
|
884
|
+
if (!pnr) {
|
|
885
|
+
console.error("No PNR available. Pass --pnr <value> or set it in form-tester.config.json");
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
const fullUrl = extractPnrFromUrl(url) ? url : setPnrOnUrl(url, pnr);
|
|
889
|
+
config.pnr = pnr;
|
|
890
|
+
saveConfig(config);
|
|
891
|
+
|
|
892
|
+
// Resolve persona
|
|
893
|
+
let personaChoice;
|
|
894
|
+
const personaId = flags.persona;
|
|
895
|
+
if (personaId) {
|
|
896
|
+
const found = getPersonaById(personaId);
|
|
897
|
+
if (found) {
|
|
898
|
+
console.log(`Persona: ${found.name} — ${found.description}`);
|
|
899
|
+
personaChoice = { type: "preset", persona: found };
|
|
900
|
+
} else {
|
|
901
|
+
console.log(`Unknown persona "${personaId}", using Noen.`);
|
|
902
|
+
personaChoice = { type: "noen", persona: null };
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
console.log("Persona: Noen — nøytrale svar (auto)");
|
|
906
|
+
personaChoice = { type: "noen", persona: null };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Resolve scenario
|
|
910
|
+
const scenarioChoice = flags.scenario
|
|
911
|
+
? { type: "custom", description: flags.scenario }
|
|
912
|
+
: { type: "default", description: "Standard test" };
|
|
913
|
+
console.log(`Scenario: ${scenarioChoice.description}`);
|
|
914
|
+
|
|
915
|
+
// Create output directory
|
|
916
|
+
const formId = sanitizeSegment(extractFormId(fullUrl));
|
|
917
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
918
|
+
const outputDir = path.join(OUTPUT_BASE, formId, timestamp);
|
|
919
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
920
|
+
|
|
921
|
+
config.lastTestUrl = fullUrl;
|
|
922
|
+
config.lastRunDir = outputDir;
|
|
923
|
+
saveConfig(config);
|
|
924
|
+
|
|
925
|
+
// Save persona and scenario
|
|
926
|
+
if (personaChoice.type === "preset") {
|
|
927
|
+
fs.writeFileSync(path.join(outputDir, "persona.json"), JSON.stringify(personaChoice.persona, null, 2));
|
|
928
|
+
} else {
|
|
929
|
+
fs.writeFileSync(path.join(outputDir, "persona.json"), JSON.stringify(
|
|
930
|
+
{ id: "noen", name: "Noen", description: "Nøytrale svar", traits: {} }, null, 2,
|
|
931
|
+
));
|
|
932
|
+
}
|
|
933
|
+
fs.writeFileSync(path.join(outputDir, "scenario.json"), JSON.stringify(scenarioChoice, null, 2));
|
|
934
|
+
|
|
935
|
+
// Open and snapshot
|
|
936
|
+
console.log("Opening form with Playwright CLI...");
|
|
937
|
+
await runPlaywrightCli(["open", fullUrl]);
|
|
938
|
+
await runPlaywrightCli(["snapshot", "--filename", path.join(outputDir, "page_open.yml")]);
|
|
939
|
+
await runPlaywrightCli(["screenshot", "--filename", path.join(outputDir, "page_open.png")]);
|
|
940
|
+
|
|
941
|
+
// Auto-select person (try recommended, then first available)
|
|
942
|
+
let options = extractPersonsFromSnapshotFile(path.join(outputDir, "page_open.yml"));
|
|
943
|
+
if (!options.length) {
|
|
944
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
945
|
+
await sleep(1500);
|
|
946
|
+
options = await fetchPersonOptions();
|
|
947
|
+
if (options.length) break;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (options.length) {
|
|
951
|
+
options = prioritizeRecommended(options, RECOMMENDED_PERSON);
|
|
952
|
+
const chosen = options[0];
|
|
953
|
+
console.log(`Auto-selected person: ${chosen}`);
|
|
954
|
+
config.lastPerson = chosen;
|
|
955
|
+
saveConfig(config);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const dokumenterUrl = resolveDokumenterUrl(config);
|
|
959
|
+
console.log(`Output folder: ${outputDir}`);
|
|
960
|
+
if (dokumenterUrl) {
|
|
961
|
+
console.log(`Dokumenter URL: ${dokumenterUrl}`);
|
|
962
|
+
}
|
|
963
|
+
printNextSteps(outputDir, dokumenterUrl || "/dokumenter?pnr={PNR}");
|
|
964
|
+
}
|
|
965
|
+
|
|
878
966
|
async function handleCommand(line, config) {
|
|
879
967
|
const trimmed = line.trim();
|
|
880
968
|
if (!trimmed) return;
|
|
@@ -1028,6 +1116,20 @@ async function main() {
|
|
|
1028
1116
|
process.exit(0);
|
|
1029
1117
|
}
|
|
1030
1118
|
|
|
1119
|
+
if (args[0] === "test" && args.includes("--auto")) {
|
|
1120
|
+
const config = loadConfig();
|
|
1121
|
+
const url = args.find((a) => a.startsWith("http"));
|
|
1122
|
+
if (!url) {
|
|
1123
|
+
console.error("Usage: form-tester test <url> --auto [--pnr <pnr>] [--persona <id>] [--scenario <text>]");
|
|
1124
|
+
process.exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
const pnrFlag = args[args.indexOf("--pnr") + 1];
|
|
1127
|
+
const personaFlag = args[args.indexOf("--persona") + 1];
|
|
1128
|
+
const scenarioFlag = args[args.indexOf("--scenario") + 1];
|
|
1129
|
+
await handleTestAuto(url, config, { pnr: pnrFlag, persona: personaFlag, scenario: scenarioFlag });
|
|
1130
|
+
process.exit(0);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1031
1133
|
const config = loadConfig();
|
|
1032
1134
|
await handleVersionMismatch(config);
|
|
1033
1135
|
|
package/package.json
CHANGED