form-tester 0.5.1 → 0.7.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 +26 -1
- package/form-tester.js +163 -1
- package/package.json +1 -1
|
@@ -26,6 +26,23 @@ form-tester
|
|
|
26
26
|
|
|
27
27
|
Persona IDs: `ung-mann`, `gravid-kvinne`, `eldre-kvinne`, `kronisk-syk-mann`. Defaults to "noen" (neutral answers) if omitted.
|
|
28
28
|
|
|
29
|
+
## Recording & Replay
|
|
30
|
+
|
|
31
|
+
Every test run records all commands to `recording.json` in the output folder. To ensure ALL commands are recorded (including form filling, clicking, etc.), always use `form-tester exec` instead of `playwright-cli` directly:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# ALWAYS use this instead of playwright-cli directly:
|
|
35
|
+
form-tester exec fill e1 "value"
|
|
36
|
+
form-tester exec click e3
|
|
37
|
+
form-tester exec screenshot --filename "path.png" --full-page
|
|
38
|
+
form-tester exec close # finalizes and saves the recording
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Replay a previous run:
|
|
42
|
+
```bash
|
|
43
|
+
form-tester replay output/form-id/timestamp/recording.json
|
|
44
|
+
```
|
|
45
|
+
|
|
29
46
|
## Commands
|
|
30
47
|
|
|
31
48
|
```bash
|
|
@@ -49,4 +66,12 @@ Use `/people` to rescan the visible person list and get a numbered selection pro
|
|
|
49
66
|
|
|
50
67
|
## Playwright CLI
|
|
51
68
|
|
|
52
|
-
|
|
69
|
+
IMPORTANT: Always use `form-tester exec` instead of `playwright-cli` directly. This ensures all commands are recorded for replay. The syntax is the same — just prefix with `form-tester exec`:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
form-tester exec snapshot
|
|
73
|
+
form-tester exec fill e1 "value"
|
|
74
|
+
form-tester exec click e3
|
|
75
|
+
form-tester exec screenshot --filename "page.png" --full-page
|
|
76
|
+
form-tester exec close
|
|
77
|
+
```
|
package/form-tester.js
CHANGED
|
@@ -6,9 +6,80 @@ 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.
|
|
9
|
+
const LOCAL_VERSION = "0.7.0";
|
|
10
10
|
const RECOMMENDED_PERSON = "Uromantisk Direktør";
|
|
11
11
|
|
|
12
|
+
// Recording — persisted to disk so `form-tester exec` can append across processes
|
|
13
|
+
let activeRecordingPath = null;
|
|
14
|
+
|
|
15
|
+
function startRecording(outputDir) {
|
|
16
|
+
const filePath = path.join(outputDir, "recording.json");
|
|
17
|
+
const data = {
|
|
18
|
+
version: LOCAL_VERSION,
|
|
19
|
+
startedAt: new Date().toISOString(),
|
|
20
|
+
completedAt: null,
|
|
21
|
+
commandCount: 0,
|
|
22
|
+
commands: [],
|
|
23
|
+
};
|
|
24
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
25
|
+
activeRecordingPath = filePath;
|
|
26
|
+
return filePath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function recordCommand(args) {
|
|
30
|
+
// In-process recording
|
|
31
|
+
if (activeRecordingPath && fs.existsSync(activeRecordingPath)) {
|
|
32
|
+
appendToRecording(activeRecordingPath, args);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Check config for active recording (cross-process via exec)
|
|
36
|
+
try {
|
|
37
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
38
|
+
if (config.activeRecording && fs.existsSync(config.activeRecording)) {
|
|
39
|
+
appendToRecording(config.activeRecording, args);
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// no config or no active recording, skip
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function appendToRecording(filePath, args) {
|
|
47
|
+
try {
|
|
48
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
49
|
+
data.commands.push({ args, timestamp: new Date().toISOString() });
|
|
50
|
+
data.commandCount = data.commands.length;
|
|
51
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// recording file corrupted or gone, skip
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function finalizeRecording(filePath) {
|
|
58
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
59
|
+
try {
|
|
60
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
61
|
+
data.completedAt = new Date().toISOString();
|
|
62
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
63
|
+
return filePath;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function saveRecording() {
|
|
70
|
+
const result = finalizeRecording(activeRecordingPath);
|
|
71
|
+
activeRecordingPath = null;
|
|
72
|
+
// Clear active recording from config
|
|
73
|
+
try {
|
|
74
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
75
|
+
delete config.activeRecording;
|
|
76
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// config not found, skip
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
12
83
|
const PERSONAS = [
|
|
13
84
|
{
|
|
14
85
|
id: "ung-mann",
|
|
@@ -286,6 +357,7 @@ function runCommand(command, args, options = {}) {
|
|
|
286
357
|
}
|
|
287
358
|
|
|
288
359
|
function runPlaywrightCli(args) {
|
|
360
|
+
recordCommand(args);
|
|
289
361
|
return new Promise((resolve) => {
|
|
290
362
|
const spec = getPlaywrightCommandSpec();
|
|
291
363
|
const spawnOpts = { stdio: "inherit" };
|
|
@@ -821,6 +893,11 @@ async function handleTest(url, config) {
|
|
|
821
893
|
config.lastRunDir = outputDir;
|
|
822
894
|
saveConfig(config);
|
|
823
895
|
|
|
896
|
+
// Start recording and persist path to config for `exec`
|
|
897
|
+
const recordingFile = startRecording(outputDir);
|
|
898
|
+
config.activeRecording = recordingFile;
|
|
899
|
+
saveConfig(config);
|
|
900
|
+
|
|
824
901
|
if (personaChoice.type === "preset") {
|
|
825
902
|
fs.writeFileSync(
|
|
826
903
|
path.join(outputDir, "persona.json"),
|
|
@@ -877,6 +954,13 @@ async function handleTest(url, config) {
|
|
|
877
954
|
dokumenterUrl ||
|
|
878
955
|
"/dokumenter?pnr={PNR}",
|
|
879
956
|
);
|
|
957
|
+
|
|
958
|
+
// Save recording
|
|
959
|
+
const recordingPath = saveRecording();
|
|
960
|
+
if (recordingPath) {
|
|
961
|
+
console.log(`Recording saved: ${recordingPath}`);
|
|
962
|
+
console.log(`Replay with: form-tester replay "${recordingPath}"`);
|
|
963
|
+
}
|
|
880
964
|
}
|
|
881
965
|
|
|
882
966
|
async function handleTestAuto(url, config, flags) {
|
|
@@ -928,6 +1012,11 @@ async function handleTestAuto(url, config, flags) {
|
|
|
928
1012
|
config.lastRunDir = outputDir;
|
|
929
1013
|
saveConfig(config);
|
|
930
1014
|
|
|
1015
|
+
// Start recording and persist path to config for `exec`
|
|
1016
|
+
const recordingFile = startRecording(outputDir);
|
|
1017
|
+
config.activeRecording = recordingFile;
|
|
1018
|
+
saveConfig(config);
|
|
1019
|
+
|
|
931
1020
|
// Save persona and scenario
|
|
932
1021
|
if (personaChoice.type === "preset") {
|
|
933
1022
|
fs.writeFileSync(path.join(outputDir, "persona.json"), JSON.stringify(personaChoice.persona, null, 2));
|
|
@@ -983,6 +1072,35 @@ async function handleTestAuto(url, config, flags) {
|
|
|
983
1072
|
console.log("");
|
|
984
1073
|
printNextSteps(outputDir, dokumenterUrl || "/dokumenter?pnr={PNR}");
|
|
985
1074
|
}
|
|
1075
|
+
|
|
1076
|
+
// Save recording
|
|
1077
|
+
const recordingPath = saveRecording();
|
|
1078
|
+
if (recordingPath) {
|
|
1079
|
+
log(`Recording saved: ${recordingPath}`);
|
|
1080
|
+
log(`Replay with: form-tester replay "${recordingPath}"`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async function handleReplay(filePath) {
|
|
1085
|
+
if (!fs.existsSync(filePath)) {
|
|
1086
|
+
console.error(`Recording not found: ${filePath}`);
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
}
|
|
1089
|
+
const recording = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1090
|
+
console.log(`Replaying ${recording.commandCount} commands from ${recording.startedAt}`);
|
|
1091
|
+
console.log("");
|
|
1092
|
+
|
|
1093
|
+
for (let i = 0; i < recording.commands.length; i++) {
|
|
1094
|
+
const cmd = recording.commands[i];
|
|
1095
|
+
console.log(`[${i + 1}/${recording.commandCount}] playwright-cli ${cmd.args.join(" ")}`);
|
|
1096
|
+
const code = await runPlaywrightCli(cmd.args);
|
|
1097
|
+
if (code !== 0) {
|
|
1098
|
+
console.error(`Command failed with exit code ${code}. Stopping replay.`);
|
|
1099
|
+
process.exit(1);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
console.log("\nReplay complete.");
|
|
986
1104
|
}
|
|
987
1105
|
|
|
988
1106
|
async function handleCommand(line, config) {
|
|
@@ -1138,6 +1256,50 @@ async function main() {
|
|
|
1138
1256
|
process.exit(0);
|
|
1139
1257
|
}
|
|
1140
1258
|
|
|
1259
|
+
if (args[0] === "exec") {
|
|
1260
|
+
const pwArgs = args.slice(1);
|
|
1261
|
+
if (!pwArgs.length) {
|
|
1262
|
+
console.error("Usage: form-tester exec <playwright-cli command and args>");
|
|
1263
|
+
process.exit(1);
|
|
1264
|
+
}
|
|
1265
|
+
// Record (reads config.activeRecording) and run playwright-cli directly
|
|
1266
|
+
recordCommand(pwArgs);
|
|
1267
|
+
// Run without going through runPlaywrightCli to avoid double-recording
|
|
1268
|
+
const spec = getPlaywrightCommandSpec();
|
|
1269
|
+
const spawnOpts = { stdio: "inherit" };
|
|
1270
|
+
if (spec.shell) spawnOpts.shell = true;
|
|
1271
|
+
const code = await new Promise((resolve) => {
|
|
1272
|
+
const child = spawn(spec.command, [...spec.args, ...pwArgs], spawnOpts);
|
|
1273
|
+
child.on("error", (err) => { console.error(err.message); resolve(1); });
|
|
1274
|
+
child.on("exit", (c) => resolve(c ?? 0));
|
|
1275
|
+
});
|
|
1276
|
+
// If the command was "close", finalize the recording
|
|
1277
|
+
if (pwArgs[0] === "close") {
|
|
1278
|
+
try {
|
|
1279
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
1280
|
+
if (config.activeRecording) {
|
|
1281
|
+
const rPath = finalizeRecording(config.activeRecording);
|
|
1282
|
+
delete config.activeRecording;
|
|
1283
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
1284
|
+
if (rPath) console.log(`Recording saved: ${rPath}`);
|
|
1285
|
+
}
|
|
1286
|
+
} catch (e) {
|
|
1287
|
+
// no config, skip
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
process.exit(code);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (args[0] === "replay") {
|
|
1294
|
+
const filePath = args[1];
|
|
1295
|
+
if (!filePath) {
|
|
1296
|
+
console.error("Usage: form-tester replay <recording.json>");
|
|
1297
|
+
process.exit(1);
|
|
1298
|
+
}
|
|
1299
|
+
await handleReplay(path.resolve(filePath));
|
|
1300
|
+
process.exit(0);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1141
1303
|
if (args[0] === "test" && args.includes("--auto")) {
|
|
1142
1304
|
const config = loadConfig();
|
|
1143
1305
|
const url = args.find((a) => a.startsWith("http"));
|
package/package.json
CHANGED