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.
@@ -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
- You can also run `playwright-cli` commands directly when needed.
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.5.1";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form-tester",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "AI-powered form testing skill for /skjemautfyller forms using Playwright CLI. Works with Claude Code and GitHub Copilot.",
5
5
  "main": "form-tester.js",
6
6
  "bin": {