testdriverai 6.0.32 → 6.1.1-canary.613bfa3.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/agent/events.js CHANGED
@@ -1,5 +1,4 @@
1
1
  const { EventEmitter2 } = require("eventemitter2");
2
- const { censorSensitiveDataDeep } = require("./lib/censorship");
3
2
 
4
3
  // Factory function to create a new emitter instance with censoring middleware
5
4
  const createEmitter = () => {
@@ -13,14 +12,6 @@ const createEmitter = () => {
13
12
  ignoreErrors: false,
14
13
  });
15
14
 
16
- // Override emit to censor sensitive data before emitting
17
- const originalEmit = emitter.emit.bind(emitter);
18
- emitter.emit = function (event, ...args) {
19
- // Censor all arguments passed to emit
20
- const censoredArgs = args.map(censorSensitiveDataDeep);
21
- return originalEmit(event, ...censoredArgs);
22
- };
23
-
24
15
  return emitter;
25
16
  };
26
17
 
@@ -46,7 +37,7 @@ const events = {
46
37
  status: "status",
47
38
  log: {
48
39
  markdown: {
49
- static: "log:markdown:static",
40
+ static: "log:markdown",
50
41
  start: "log:markdown:start",
51
42
  chunk: "log:markdown:chunk",
52
43
  end: "log:markdown:end",
package/agent/index.js CHANGED
@@ -63,7 +63,11 @@ class TestDriverAgent extends EventEmitter2 {
63
63
  // Derive properties from cliArgs
64
64
  const flags = cliArgs.options || {};
65
65
  const firstArg = cliArgs.args && cliArgs.args[0];
66
+
67
+ // All commands (run, edit, generate) use the same pattern:
68
+ // first argument is the main file to work with
66
69
  this.thisFile = firstArg || this.config.TD_DEFAULT_TEST_FILE;
70
+
67
71
  this.resultFile = flags.resultFile || null;
68
72
  this.newSandbox = flags.newSandbox || false;
69
73
  this.healMode = flags.healMode || flags.heal || false;
@@ -427,6 +431,7 @@ class TestDriverAgent extends EventEmitter2 {
427
431
 
428
432
  // Log current execution position for debugging
429
433
  if (this.sourceMapper.currentFileSourceMap) {
434
+ this.emitter.emit(events.log.log, "");
430
435
  this.emitter.emit(
431
436
  events.log.log,
432
437
  theme.dim(`${this.sourceMapper.getCurrentPositionDescription()}`),
@@ -886,30 +891,30 @@ commands:
886
891
  // based on the current state of the system (primarily the current screenshot)
887
892
  // it will generate files that contain only "prompts"
888
893
  // @todo revit the generate command
889
- async generate(type, count, baseYaml, skipYaml = false) {
890
- this.emitter.emit(events.log.debug, "generate called, %s", type);
894
+ async generate(count = 1) {
895
+ this.emitter.emit(events.log.debug, `generate called with count: ${count}`);
891
896
 
892
- this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
897
+ await this.runLifecycle("prerun");
893
898
 
894
- if (baseYaml && !skipYaml) {
895
- await this.runLifecycle("prerun");
896
- await this.run(baseYaml, false, false);
897
- await this.runLifecycle("postrun");
898
- }
899
+ this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
899
900
 
900
901
  let image = await this.system.captureScreenBase64();
901
902
 
902
903
  const streamId = `generate-${Date.now()}`;
903
904
  this.emitter.emit(events.log.markdown.start, streamId);
904
905
 
906
+ let mouse = await this.system.getMousePosition();
907
+ let activeWindow = await this.system.activeWin();
908
+
905
909
  let message = await this.sdk.req(
906
910
  "generate",
907
911
  {
908
- type,
912
+ prompt: "make sure to do a spellcheck",
909
913
  image,
910
- mousePosition: await this.system.getMousePosition(),
911
- activeWindow: await this.system.activeWin(),
914
+ mousePosition: mouse,
915
+ activeWindow: activeWindow,
912
916
  count,
917
+ stream: false,
913
918
  },
914
919
  (chunk) => {
915
920
  if (chunk.type === "data") {
@@ -932,35 +937,36 @@ commands:
932
937
  .replace(/['"`]/g, "")
933
938
  .replace(/[^a-zA-Z0-9-]/g, "") // remove any non-alphanumeric chars except hyphens
934
939
  .toLowerCase() + ".yaml";
940
+
935
941
  let path1 = path.join(
936
942
  this.workingDir,
937
943
  "testdriver",
938
944
  "generate",
939
945
  fileName,
940
946
  );
941
-
942
947
  // create generate directory if it doesn't exist
943
- if (!fs.existsSync(path.join(this.workingDir, "generate"))) {
944
- fs.mkdirSync(path.join(this.workingDir, "generate"));
948
+ const generateDir = path.join(this.workingDir, "testdriver", "generate");
949
+ if (!fs.existsSync(generateDir)) {
950
+ fs.mkdirSync(generateDir);
951
+ console.log("Created generate directory:", generateDir);
952
+ } else {
953
+ console.log("Generate directory already exists:", generateDir);
945
954
  }
946
955
 
947
956
  let list = testPrompt.steps;
948
957
 
949
- if (baseYaml && fs.existsSync(baseYaml)) {
950
- list.unshift({
951
- step: {
952
- command: "run",
953
- file: baseYaml,
954
- },
955
- });
956
- }
957
958
  let contents = yaml.dump({
958
959
  version: packageJson.version,
959
960
  steps: list,
960
961
  });
962
+
963
+ this.emitter.emit(events.log.debug, `writing file ${path1} ${contents}`);
964
+
961
965
  fs.writeFileSync(path1, contents);
962
966
  }
963
967
 
968
+ await this.runLifecycle("postrun");
969
+
964
970
  this.exit(false);
965
971
  }
966
972
 
@@ -1511,6 +1517,8 @@ ${regression}
1511
1517
  }
1512
1518
 
1513
1519
  async embed(file, depth, pushToHistory) {
1520
+ let inputFile = JSON.parse(JSON.stringify(file));
1521
+
1514
1522
  this.analytics.track("embed", { file });
1515
1523
 
1516
1524
  this.emitter.emit(
@@ -1520,7 +1528,7 @@ ${regression}
1520
1528
 
1521
1529
  depth = depth + 1;
1522
1530
 
1523
- this.emitter.emit(events.log.log, `${file} (start)`);
1531
+ this.emitter.emit(events.log.log, `${inputFile} (start)`);
1524
1532
 
1525
1533
  // Use the new helper method to resolve file paths relative to testdriver directory
1526
1534
  const currentFilePath = this.sourceMapper.currentFilePath || this.thisFile;
@@ -1573,7 +1581,7 @@ ${regression}
1573
1581
  this.sourceMapper.restoreContext(previousContext);
1574
1582
  }
1575
1583
 
1576
- this.emitter.emit(events.log.log, `${file} (end)`);
1584
+ this.emitter.emit(events.log.log, `${inputFile} (end)`);
1577
1585
  }
1578
1586
 
1579
1587
  // Returns sandboxId to use (either from file if recent, or null)
@@ -1780,10 +1788,6 @@ ${regression}
1780
1788
  // Start the debugger server as early as possible to ensure event listeners are attached
1781
1789
  if (!debuggerStarted) {
1782
1790
  debuggerStarted = true; // Prevent multiple starts, especially when running test in parallel
1783
- this.emitter.emit(
1784
- events.log.narration,
1785
- theme.green(`Starting debugger server...`),
1786
- );
1787
1791
  debuggerProcess = await createDebuggerProcess(
1788
1792
  this.config,
1789
1793
  this.emitter,
@@ -1791,6 +1795,7 @@ ${regression}
1791
1795
  }
1792
1796
  this.debuggerUrl = debuggerProcess.url || null; // Store the debugger URL
1793
1797
  this.emitter.emit(events.log.log, `This is beta software!`);
1798
+ this.emitter.emit(events.log.log, ``);
1794
1799
  this.emitter.emit(
1795
1800
  events.log.log,
1796
1801
  theme.yellow(`Join our Discord for help`),
@@ -1799,6 +1804,7 @@ ${regression}
1799
1804
  events.log.log,
1800
1805
  `https://discord.com/invite/cWDFW8DzPm`,
1801
1806
  );
1807
+ this.emitter.emit(events.log.log, ``);
1802
1808
 
1803
1809
  // make testdriver directory if it doesn't exist
1804
1810
  let testdriverFolder = path.join(this.workingDir);
@@ -1812,7 +1818,10 @@ ${regression}
1812
1818
  }
1813
1819
 
1814
1820
  // if the directory for thisFile doesn't exist, create it
1815
- if (this.cliArgs.command !== "sandbox") {
1821
+ if (
1822
+ this.cliArgs.command !== "sandbox" &&
1823
+ this.cliArgs.command !== "generate"
1824
+ ) {
1816
1825
  const dir = path.dirname(this.thisFile);
1817
1826
  if (!fs.existsSync(dir)) {
1818
1827
  fs.mkdirSync(dir, { recursive: true });
@@ -1837,7 +1846,10 @@ ${regression}
1837
1846
  await this.sdk.auth();
1838
1847
  }
1839
1848
 
1840
- if (this.cliArgs.command !== "sandbox") {
1849
+ if (
1850
+ this.cliArgs.command !== "sandbox" &&
1851
+ this.cliArgs.command !== "generate"
1852
+ ) {
1841
1853
  this.emitter.emit(
1842
1854
  events.log.log,
1843
1855
  theme.dim(`Working on ${this.thisFile}`),
@@ -2034,6 +2046,20 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2034
2046
  // Use the current file path from sourceMapper to find the lifecycle directory
2035
2047
  // If sourceMapper doesn't have a current file, use thisFile which should be the file being run
2036
2048
  let currentFilePath = this.sourceMapper.currentFilePath || this.thisFile;
2049
+
2050
+ this.emitter.emit(events.log.log, ``);
2051
+ this.emitter.emit(events.log.log, "Running lifecycle: " + lifecycleName);
2052
+
2053
+ // If we still don't have a currentFilePath, fall back to the default testdriver directory
2054
+ if (!currentFilePath) {
2055
+ currentFilePath = path.join(
2056
+ this.workingDir,
2057
+ "testdriver",
2058
+ "testdriver.yaml",
2059
+ );
2060
+ console.log("No currentFilePath found, using fallback:", currentFilePath);
2061
+ }
2062
+
2037
2063
  // Ensure we have an absolute path
2038
2064
  if (currentFilePath && !path.isAbsolute(currentFilePath)) {
2039
2065
  currentFilePath = path.resolve(this.workingDir, currentFilePath);
@@ -2070,6 +2096,9 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2070
2096
  }
2071
2097
  }
2072
2098
  }
2099
+
2100
+ this.emitter.emit(events.log.log, lifecycleFile);
2101
+
2073
2102
  if (lifecycleFile) {
2074
2103
  // Store current source mapping state before running lifecycle file
2075
2104
  const previousContext = this.sourceMapper.saveContext();
@@ -2139,7 +2168,7 @@ Please check your network connection, TD_API_KEY, or the service status.`,
2139
2168
  }
2140
2169
 
2141
2170
  // Move environment setup and special handling here
2142
- if (["edit", "run"].includes(commandName)) {
2171
+ if (["edit", "run", "generate"].includes(commandName)) {
2143
2172
  await this.buildEnv(options);
2144
2173
  }
2145
2174
 
@@ -196,6 +196,42 @@ function createCommandDefinitions(agent) {
196
196
  console.log(`TestDriver.ai v${packageJson.version}`);
197
197
  },
198
198
  },
199
+
200
+ generate: {
201
+ description: "Generate test files based on current screen state",
202
+ args: {
203
+ file: Args.string({
204
+ description: "Base test file to run before generating (optional)",
205
+ required: false,
206
+ }),
207
+ },
208
+ flags: {
209
+ count: Flags.integer({
210
+ description: "Number of test files to generate",
211
+ default: 3,
212
+ }),
213
+ headless: Flags.boolean({
214
+ description: "Run in headless mode (no GUI)",
215
+ default: false,
216
+ }),
217
+ new: Flags.boolean({
218
+ description:
219
+ "Create a new sandbox instead of reconnecting to an existing one",
220
+ default: false,
221
+ }),
222
+ "sandbox-ami": Flags.string({
223
+ description: "Specify AMI ID for sandbox instance (e.g., ami-1234)",
224
+ }),
225
+ "sandbox-instance": Flags.string({
226
+ description: "Specify EC2 instance type for sandbox (e.g., i3.metal)",
227
+ }),
228
+ },
229
+ handler: async (args, flags) => {
230
+ // The file argument is already handled by thisFile in the agent constructor
231
+ // Just call generate with the count
232
+ await agent.generate(flags.count || 3);
233
+ },
234
+ },
199
235
  };
200
236
  }
201
237
 
@@ -38,18 +38,23 @@ const censorSensitiveData = (message) => {
38
38
 
39
39
  // Function to censor sensitive data in any value (recursive for objects/arrays)
40
40
  const censorSensitiveDataDeep = (value) => {
41
- if (typeof value === "string") {
42
- return censorSensitiveData(value);
43
- } else if (Array.isArray(value)) {
44
- return value.map(censorSensitiveDataDeep);
45
- } else if (value && typeof value === "object") {
46
- const result = {};
47
- for (const [key, val] of Object.entries(value)) {
48
- result[key] = censorSensitiveDataDeep(val);
41
+ try {
42
+ if (typeof value === "string") {
43
+ return censorSensitiveData(value);
44
+ } else if (Array.isArray(value)) {
45
+ return value.map(censorSensitiveDataDeep);
46
+ } else if (value && typeof value === "object") {
47
+ const result = {};
48
+ for (const [key, val] of Object.entries(value)) {
49
+ result[key] = censorSensitiveDataDeep(val);
50
+ }
51
+ return result;
49
52
  }
50
- return result;
53
+ return value;
54
+ } catch {
55
+ // If we hit any error (like circular reference), just return a safe placeholder
56
+ return "[Object]";
51
57
  }
52
- return value;
53
58
  };
54
59
 
55
60
  // Function to update interpolation variables (for runtime updates)
@@ -80,21 +80,21 @@ commands:
80
80
  // this will actually interpret the command and execute it
81
81
  switch (object.command) {
82
82
  case "type":
83
- emitter.emit(events.log.narration, `typing ${object.text}`);
84
83
  emitter.emit(events.log.log, generator.jsonToManual(object));
84
+ emitter.emit(events.log.narration, `typing ${object.text}`);
85
85
  response = await commands.type(object.text, object.delay);
86
86
  break;
87
87
  case "press-keys":
88
+ emitter.emit(events.log.log, generator.jsonToManual(object));
88
89
  emitter.emit(
89
90
  events.log.narration,
90
- `pressing keys ${object.keys.join(",")}`,
91
+ `pressing keys: ${Array.isArray(object.keys) ? object.keys.join(", ") : object.keys}`,
91
92
  );
92
- emitter.emit(events.log.log, generator.jsonToManual(object));
93
93
  response = await commands["press-keys"](object.keys);
94
94
  break;
95
95
  case "scroll":
96
- emitter.emit(events.log.narration, `scrolling ${object.direction}`);
97
96
  emitter.emit(events.log.log, generator.jsonToManual(object));
97
+ emitter.emit(events.log.narration, `scrolling ${object.direction}`);
98
98
  response = await commands.scroll(
99
99
  object.direction,
100
100
  object.amount,
@@ -102,21 +102,21 @@ commands:
102
102
  );
103
103
  break;
104
104
  case "wait":
105
+ emitter.emit(events.log.log, generator.jsonToManual(object));
105
106
  emitter.emit(
106
107
  events.log.narration,
107
108
  `waiting ${object.timeout} seconds`,
108
109
  );
109
- emitter.emit(events.log.log, generator.jsonToManual(object));
110
110
  response = await commands.wait(object.timeout);
111
111
  break;
112
112
  case "click":
113
- emitter.emit(events.log.narration, `${object.action}`);
114
113
  emitter.emit(events.log.log, generator.jsonToManual(object));
114
+ emitter.emit(events.log.narration, `${object.action}`);
115
115
  response = await commands["click"](object.x, object.y, object.action);
116
116
  break;
117
117
  case "hover":
118
- emitter.emit(events.log.narration, `moving mouse`);
119
118
  emitter.emit(events.log.log, generator.jsonToManual(object));
119
+ emitter.emit(events.log.narration, `moving mouse`);
120
120
  response = await commands["hover"](object.x, object.y);
121
121
  break;
122
122
  case "drag":
@@ -124,11 +124,11 @@ commands:
124
124
  response = await commands["drag"](object.x, object.y);
125
125
  break;
126
126
  case "hover-text":
127
+ emitter.emit(events.log.log, generator.jsonToManual(object));
127
128
  emitter.emit(
128
129
  events.log.narration,
129
130
  `searching for ${object.description}`,
130
131
  );
131
- emitter.emit(events.log.log, generator.jsonToManual(object));
132
132
  response = await commands["hover-text"](
133
133
  object.text,
134
134
  object.description,
@@ -138,11 +138,11 @@ commands:
138
138
  );
139
139
  break;
140
140
  case "hover-image":
141
+ emitter.emit(events.log.log, generator.jsonToManual(object));
141
142
  emitter.emit(
142
143
  events.log.narration,
143
144
  `searching for image of ${object.description}`,
144
145
  );
145
- emitter.emit(events.log.log, generator.jsonToManual(object));
146
146
  response = await commands["hover-image"](
147
147
  object.description,
148
148
  object.action,
@@ -157,19 +157,19 @@ commands:
157
157
  response = await commands["match-image"](object.path, object.action);
158
158
  break;
159
159
  case "wait-for-image":
160
+ emitter.emit(events.log.log, generator.jsonToManual(object));
160
161
  emitter.emit(
161
162
  events.log.narration,
162
163
  `waiting for ${object.description}`,
163
164
  );
164
- emitter.emit(events.log.log, generator.jsonToManual(object));
165
165
  response = await commands["wait-for-image"](
166
166
  object.description,
167
167
  object.timeout,
168
168
  );
169
169
  break;
170
170
  case "wait-for-text":
171
- emitter.emit(events.log.narration, `waiting for ${object.text}`);
172
171
  emitter.emit(events.log.log, generator.jsonToManual(object));
172
+ emitter.emit(events.log.narration, `waiting for ${object.text}`);
173
173
  copy.text = "*****";
174
174
  response = await commands["wait-for-text"](
175
175
  object.text,
@@ -178,8 +178,8 @@ commands:
178
178
  );
179
179
  break;
180
180
  case "scroll-until-text":
181
- emitter.emit(events.log.narration, `scrolling until ${object.text}`);
182
181
  emitter.emit(events.log.log, generator.jsonToManual(object));
182
+ emitter.emit(events.log.narration, `scrolling until ${object.text}`);
183
183
  copy.text = "*****";
184
184
  response = await commands["scroll-until-text"](
185
185
  object.text,
@@ -191,8 +191,8 @@ commands:
191
191
  break;
192
192
  case "scroll-until-image": {
193
193
  const needle = object.description || object.path;
194
- emitter.emit(events.log.narration, `scrolling until ${needle}`);
195
194
  emitter.emit(events.log.log, generator.jsonToManual(object));
195
+ emitter.emit(events.log.narration, `scrolling until ${needle}`);
196
196
  response = await commands["scroll-until-image"](
197
197
  object.description,
198
198
  object.direction,
@@ -203,8 +203,8 @@ commands:
203
203
  break;
204
204
  }
205
205
  case "focus-application":
206
- emitter.emit(events.log.narration, `focusing ${object.name}`);
207
206
  emitter.emit(events.log.log, generator.jsonToManual(object));
207
+ emitter.emit(events.log.narration, `focusing ${object.name}`);
208
208
  response = await commands["focus-application"](object.name);
209
209
  break;
210
210
  case "remember": {
@@ -215,12 +215,12 @@ commands:
215
215
  break;
216
216
  }
217
217
  case "assert":
218
- emitter.emit(events.log.narration, `asserting ${object.expect}`);
219
218
  emitter.emit(events.log.log, generator.jsonToManual(object));
219
+ emitter.emit(events.log.narration, `asserting ${object.expect}`);
220
220
  response = await commands.assert(object.expect, object.async);
221
+
221
222
  break;
222
223
  case "exec":
223
- emitter.emit(events.log.narration, `exec`);
224
224
  emitter.emit(
225
225
  events.log.log,
226
226
  generator.jsonToManual({
@@ -176,7 +176,7 @@ const createCommands = (
176
176
  }
177
177
 
178
178
  const handleAssertResponse = (response) => {
179
- emitter.emit(events.log.markdown.static, response);
179
+ emitter.emit(events.log.log, response);
180
180
 
181
181
  if (response.indexOf("The task passed") > -1) {
182
182
  return true;
@@ -727,14 +727,14 @@ const createCommands = (
727
727
  `Command failed with exit code ${result.out.returncode}: ${result.out.stderr}`,
728
728
  );
729
729
  } else {
730
- if (!silent) {
731
- emitter.emit(events.log.log, theme.dim(`Command stdout:`), true);
730
+ if (!silent && result.out?.stdout) {
731
+ emitter.emit(events.log.log, theme.dim(`stdout:`), true);
732
732
  emitter.emit(events.log.log, `${result.out.stdout}`, true);
733
+ }
733
734
 
734
- if (result.out.stderr) {
735
- emitter.emit(events.log.log, theme.dim(`Command stderr:`), true);
736
- emitter.emit(events.log.log, `${result.out.stderr}`, true);
737
- }
735
+ if (!silent && result.out.stderr) {
736
+ emitter.emit(events.log.log, theme.dim(`stderr:`), true);
737
+ emitter.emit(events.log.log, `${result.out.stderr}`, true);
738
738
  }
739
739
 
740
740
  return result.out?.stdout?.trim();
@@ -65,11 +65,6 @@ function createDebuggerServer(config = {}) {
65
65
 
66
66
  ws.on("close", () => {
67
67
  clients.delete(ws);
68
-
69
- // If no clients connected, we can optionally shut down
70
- if (clients.size === 0) {
71
- console.log("No clients connected, keeping server alive");
72
- }
73
68
  });
74
69
 
75
70
  ws.on("error", (error) => {
@@ -1,6 +1,6 @@
1
1
  // parses markdown content to find code blocks, and then extracts yaml from those code blocks
2
2
  const yaml = require("js-yaml");
3
- const package = require("../../package.json");
3
+ const pkg = require("../../package.json");
4
4
  const session = require("./session");
5
5
  const theme = require("./theme");
6
6
  // do the actual parsing
@@ -56,7 +56,7 @@ const jsonToManual = function (json, colors = true) {
56
56
  const dumpToYML = async function (inputArray, sessionInstance = null) {
57
57
  // use yml dump to convert json to yml
58
58
  let yml = await yaml.dump({
59
- version: package.version,
59
+ version: pkg.version,
60
60
  session: sessionInstance ? sessionInstance.get() : session.get(),
61
61
  steps: inputArray,
62
62
  });
package/agent/lib/sdk.js CHANGED
@@ -95,7 +95,7 @@ const createSDK = (emitter, config, sessionInstance) => {
95
95
  return token;
96
96
  } catch (error) {
97
97
  outputError(error);
98
- return;
98
+ throw error; // Re-throw the error so calling code can handle it properly
99
99
  }
100
100
  }
101
101
  };
@@ -194,6 +194,7 @@ const createSDK = (emitter, config, sessionInstance) => {
194
194
  return value;
195
195
  } catch (error) {
196
196
  outputError(error);
197
+ throw error; // Re-throw the error so calling code can handle it properly
197
198
  }
198
199
  };
199
200
 
@@ -263,7 +263,7 @@ class SourceMapper {
263
263
  let description = `${fileName}:${(sourcePosition.step.startLine || 0) + 1}`;
264
264
 
265
265
  if (sourcePosition.command) {
266
- description += `:${(sourcePosition.command.startLine || 0) + 1} (${sourcePosition.command.command || "unknown command"})`;
266
+ description += `:${(sourcePosition.command.startLine || 0) + 1}`;
267
267
  } else {
268
268
  description += ` (step ${sourcePosition.step.stepIndex + 1})`;
269
269
  }
@@ -0,0 +1,413 @@
1
+ ---
2
+ title: "Tauri Apps"
3
+ sidebarTitle: "Tauri Apps"
4
+ description: "Testing Tauri Apps with TestDriver"
5
+ icon: "https://tauri.app/favicon.svg"
6
+ ---
7
+
8
+ > [Tauri](https://tauri.app/) is a framework for building tiny, fast binaries for all major desktop and mobile platforms. Developers can integrate any frontend framework that compiles to HTML, JavaScript, and CSS for building their user experience while leveraging languages such as Rust, Swift, and Kotlin for backend logic when needed.
9
+ >
10
+ > <br />– [https://tauri.app/start](https://tauri.app/start/)
11
+
12
+ ## Testing Tauri apps with TestDriver
13
+
14
+ In this guide, we'll leverage [Playwright](https://playwright.dev/) and the
15
+ [TestDriver Playwright SDK](/getting-started/playwright) to convert the [Tauri Quick Start](https://tauri.app/start/create-project/)
16
+ to TestDriver's selectorless, Vision AI.
17
+
18
+ <Callout icon="code">
19
+ View Source:
20
+ [https://github.com/testdriverai/demo-tauri-app](https://github.com/testdriverai/demo-tauri-app)
21
+ </Callout>
22
+
23
+ ### Requirements
24
+
25
+ To start testing your Tauri app with TestDriver, you need the following:
26
+
27
+ <AccordionGroup>
28
+ <Accordion title="Create a TestDriver account">
29
+ <Steps>
30
+ <Step title="Create a TestDriver Account">
31
+ You will need a [TestDriver Pro](https://app.testdriver.ai/team) account (\$20/month) to get an API key.
32
+ <Card title="Sign Up for TestDriver" icon="user-plus" horizontal href="https://app.testdriver.ai/team" />
33
+ </Step>
34
+ <Step title="Set up your environment">
35
+ Copy your API key from [the TestDriver dashboard](https://app.testdriver.ai/team), and set it as an environment variable.
36
+
37
+ <Tabs>
38
+ <Tab title="macOS / Linux">
39
+ Export an environment variable on macOS or Linux systems:
40
+
41
+ ```bash
42
+ export TD_API_KEY="your_api_key_here"
43
+ ```
44
+ </Tab>
45
+ <Tab title="Windows">
46
+ Export an environment variable in PowerShell:
47
+
48
+ ```powershell
49
+ setx TD_API_KEY "your_api_key_here"
50
+ ```
51
+ </Tab>
52
+ </Tabs>
53
+ </Step>
54
+
55
+ </Steps>
56
+
57
+ </Accordion>
58
+
59
+ <Accordion title="Create a Tauri project">
60
+
61
+ <Note>
62
+ Follow Tauri's [Create a Project](https://tauri.app/start/create-project/)
63
+ guide.
64
+ </Note>
65
+
66
+ </Accordion>
67
+
68
+ <Accordion title="Create a Playwright project">
69
+ <Info>
70
+ This is a condensed version of [Playwright's Installation Instructions](https://playwright.dev/docs/intro).
71
+
72
+ **If you're new to Playwright, you should follow their guide first.**
73
+ </Info>
74
+
75
+ In your Tauri project, run:
76
+
77
+ <Tabs>
78
+ <Tab title="npm">
79
+ ```bash
80
+ npm init playwright@latest
81
+ ```
82
+ </Tab>
83
+ <Tab title="yarn">
84
+ ```bash
85
+ yarn create playwright
86
+ ```
87
+ </Tab>
88
+ <Tab title="pnpm">
89
+ ```bash
90
+ pnpm create playwright
91
+ ```
92
+ </Tab>
93
+ </Tabs>
94
+ Select the following options when prompted:
95
+
96
+ ```console
97
+ ✔ Do you want to use TypeScript or JavaScript?
98
+ > TypeScript
99
+ ✔ Where to put your end-to-end tests?
100
+ > tests
101
+ ✔ Add a GitHub Actions workflow? (y/N)
102
+ > N
103
+ ✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n)
104
+ > Y
105
+ ```
106
+
107
+ </Accordion>
108
+
109
+ <Accordion title="Install the TestDriver Playwright SDK">
110
+ `@testdriver.ai/playwright` is an AI-powered extension of `@playwright/test`.
111
+
112
+ <Tabs>
113
+ <Tab title="npm">
114
+ ```bash
115
+ npm install @testdriver.ai/playwright
116
+ ```
117
+ </Tab>
118
+ <Tab title="yarn">
119
+ ```bash
120
+ yarn add @testdriver.ai/playwright
121
+ ```
122
+ </Tab>
123
+ <Tab title="pnpm">
124
+ ```bash
125
+ pnpm add @testdriver.ai/playwright
126
+ ```
127
+ </Tab>
128
+ </Tabs>
129
+
130
+ </Accordion>
131
+
132
+ </AccordionGroup>
133
+
134
+ ## Testing the Tauri Web App
135
+
136
+ ### Setup
137
+
138
+ First, we need to modify the default Playwright configuration and our Tauri project to work together:
139
+
140
+ <Steps>
141
+ <Step title="Configure Playwright to start the Tauri frontend">
142
+
143
+ In the `playwright.config.ts` file, we'll configure the [`webServer`](https://playwright.dev/docs/test-webserver)
144
+ to start the Tauri frontend for Playwright to test against:
145
+
146
+ ```typescript playwright.config.ts
147
+ /* Run your local dev server before starting the tests */
148
+ // [!code ++:8]
149
+ webServer: {
150
+ command: "npm run dev",
151
+ url: "http://localhost:1420",
152
+ reuseExistingServer: !process.env.CI,
153
+ env: {
154
+ VITE_PLAYWRIGHT: "true",
155
+ },
156
+ },
157
+ });
158
+ ```
159
+
160
+ </Step>
161
+
162
+ <Step title="Mock Tauri APIs">
163
+
164
+ Since we're testing the Tauri frontend, we need to [mock IPC Requests](https://tauri.app/develop/tests/mocking/)
165
+ to simulate `invoke` calls to the Rust backend:
166
+
167
+ ```html src/index.html
168
+ <body>
169
+ <div id="root"></div>
170
+ <!-- [!code ++:7] -->
171
+ <script type="module">
172
+ // This is set in playwright.config.ts to allow our tests to mock Tauri IPC `invoke` calls
173
+ if (import.meta.env.VITE_PLAYWRIGHT) {
174
+ const { mockIPC } = await import("@tauri-apps/api/mocks");
175
+ window.mockIPC = mockIPC;
176
+ }
177
+ </script>
178
+ <script type="module" src="/src/main.tsx"></script>
179
+ </body>
180
+ ```
181
+
182
+ We only need to do this once, as we'll be accessing `window.mockIPC` in our tests.
183
+
184
+ </Step>
185
+
186
+ <Step title="Create a new test file">
187
+ Create a new file (e.g. `tests/testdriver.spec.ts`) with:
188
+
189
+ ```typescript tests/testdriver.spec.ts
190
+ import type { mockIPC } from "@tauri-apps/api/mocks";
191
+ import { expect, test } from "@playwright/test";
192
+
193
+ test.beforeEach(async ({ page }) => {
194
+ await page.goto("http://localhost:1420");
195
+ });
196
+
197
+ test("should have title", async ({ page }) => {
198
+ await expect(page).toHaveTitle("Tauri + React + TypeScript");
199
+ });
200
+ ```
201
+
202
+ </Step>
203
+
204
+ <Step title="Run Playwright in UI Mode">
205
+
206
+ Now we're ready to run Playwright and start working on our tests:
207
+
208
+ <Tabs>
209
+ <Tab title="npm">
210
+
211
+ ```bash
212
+ npx playwright test --ui
213
+ ```
214
+
215
+ </Tab>
216
+ <Tab title="yarn">
217
+
218
+ ```bash
219
+ yarn playwright test --ui
220
+ ```
221
+
222
+ </Tab>
223
+ <Tab title="pnpm">
224
+
225
+ ```bash
226
+ pnpm exec playwright test --ui
227
+ ```
228
+
229
+ </Tab>
230
+ </Tabs>
231
+
232
+ ![Playwright UI Mode](https://playwright.dev/assets/ideal-img/ui-mode.4e54d6b.3598.png)
233
+
234
+ Click the <Icon icon="play" /> button to successfully run the tests in the UI.
235
+
236
+ <Tip>
237
+ Click the <Icon icon="eye" /> button to automatically re-run tests on save.
238
+ </Tip>
239
+
240
+ </Step>
241
+
242
+ </Steps>
243
+
244
+ ### Usage
245
+
246
+ #### Import the TestDriver Playwright SDK
247
+
248
+ By changing **1 line**, we can add TestDriver's AI capabilities to Playwright:
249
+
250
+ ```typescript tests/testdriver.spec.ts
251
+ import type { mockIPC } from "@tauri-apps/api/mocks";
252
+ // [!code --]
253
+ import { expect, test } from "@playwright/test";
254
+ // [!code ++]
255
+ import { expect, test } from "@testdriver.ai/playwright";
256
+
257
+ // For type-safety of the mockIPC function
258
+ declare global {
259
+ interface Window {
260
+ mockIPC: typeof mockIPC;
261
+ }
262
+ }
263
+
264
+ test.beforeEach(async ({ page }) => {
265
+ await page.goto("http://localhost:1420");
266
+ });
267
+
268
+ test("should have title", async ({ page }) => {
269
+ await expect(page).toHaveTitle("Tauri + React + TypeScript");
270
+ });
271
+ ```
272
+
273
+ Notice how we're able to continue using Playwright's API (`toHaveTitle`)
274
+ with `@testdriver.ai/playwright`.
275
+
276
+ The test continues to pass as before, so now we can update our test to use natural language instead of selectors.
277
+
278
+ #### Assertions with `expect.toMatchPrompt`
279
+
280
+ With Playwright, we would normally use a `getByRole` selector to assert the heading text:
281
+
282
+ ```typescript tests/example.spec.ts
283
+ test("should have heading", async ({ page }) => {
284
+ await expect(
285
+ page.getByRole("heading", { name: "Installation" }),
286
+ ).toBeVisible();
287
+ });
288
+ ```
289
+
290
+ With TestDriver, we can use natural language with `toMatchPrompt` instead:
291
+
292
+ ```typescript tests/testdriver.spec.ts
293
+ test("should have heading", async ({ page }) => {
294
+ // [!code --:3]
295
+ await expect(
296
+ page.getByRole("heading", { name: "Installation" }),
297
+ ).toBeVisible();
298
+ // [!code ++:1]
299
+ await expect(page).toMatchPrompt("Heading says 'Welcome to Tauri + React'");
300
+ });
301
+ ```
302
+
303
+ #### Agentic tests with `test.agent`
304
+
305
+ With TestDriver, we can skip the test implementation **entirely** and let AI perform the test for us:
306
+
307
+ <Steps>
308
+ <Step title="Mock the `greet` call">
309
+
310
+ First, we need to [mock our `invoke` calls](https://tauri.app/develop/tests/mocking/#ipc-requests),
311
+ since we're testing the frontend behavior and not our Tauri backend:
312
+
313
+ ```typescript tests/testdriver.spec.ts
314
+ test.beforeEach(async ({ page }) => {
315
+ await page.goto("http://localhost:1420");
316
+ // [!code ++:12]
317
+ await page.evaluate(() => {
318
+ // https://tauri.app/develop/tests/mocking/#mocking-commands-for-invoke
319
+ window.mockIPC((cmd, args) => {
320
+ switch (cmd) {
321
+ case "greet":
322
+ args = args as { name: string };
323
+ return `Hello, ${args.name}! You've been greeted from Rust!`;
324
+ default:
325
+ throw new Error(`Unsupported command: ${cmd}`);
326
+ }
327
+ });
328
+ });
329
+ });
330
+ ```
331
+
332
+ </Step>
333
+
334
+ <Step title="Add an Agentic Test">
335
+
336
+ Next, wrap a _prompt_ in `test.agent` to perform the test:
337
+
338
+ ```typescript tests/testdriver.spec.ts
339
+ test.describe("should greet with name", () => {
340
+ test.agent(`
341
+ - Enter the name "Tauri"
342
+ - Click the "Greet" button
343
+ - You should see the text "Hello, Tauri! You've been greeted from Rust!"
344
+ `);
345
+ });
346
+ ```
347
+
348
+ </Step>
349
+ </Steps>
350
+
351
+ #### Continued Reading
352
+
353
+ [Learn more about TestDriver's Playwright SDK](/getting-started/playwright)
354
+
355
+ ## Testing the Tauri Desktop App
356
+
357
+ We can use TestDriver and natural language to test our Tauri desktop app:
358
+
359
+ <Steps>
360
+ <Step title="Run the Desktop App">
361
+ <Tabs>
362
+ <Tab title="npm">
363
+ ```bash
364
+ npm run tauri dev
365
+ ```
366
+ </Tab>
367
+ <Tab title="yarn">
368
+ ```bash
369
+ yarn tauri dev
370
+ ```
371
+ </Tab>
372
+ <Tab title="pnpm">
373
+ ```bash
374
+ pnpm tauri dev
375
+ ```
376
+ </Tab>
377
+ </Tabs>
378
+ </Step>
379
+
380
+ <Step title="Continued Reading">
381
+ <Note>See [Desktop Apps](/apps/desktop-apps) for more information.</Note>
382
+ </Step>
383
+ </Steps>
384
+
385
+ ## Testing the Tauri Mobile App
386
+
387
+ We can use TestDriver and natural language to test our Tauri iOS app:
388
+
389
+ <Steps>
390
+ <Step title="Run the Mobile App">
391
+ <Tabs>
392
+ <Tab title="npm">
393
+ ```bash
394
+ npm run tauri ios dev
395
+ ```
396
+ </Tab>
397
+ <Tab title="yarn">
398
+ ```bash
399
+ yarn tauri ios dev
400
+ ```
401
+ </Tab>
402
+ <Tab title="pnpm">
403
+ ```bash
404
+ pnpm tauri ios dev
405
+ ```
406
+ </Tab>
407
+ </Tabs>
408
+ </Step>
409
+
410
+ <Step title="Continued Reading">
411
+ <Note>See [Mobile Apps](/apps/mobile-apps) for more information.</Note>
412
+ </Step>
413
+ </Steps>
package/docs/docs.json CHANGED
@@ -64,7 +64,8 @@
64
64
  "apps/static-websites",
65
65
  "apps/desktop-apps",
66
66
  "apps/chrome-extensions",
67
- "apps/mobile-apps"
67
+ "apps/mobile-apps",
68
+ "apps/tauri-apps"
68
69
  ]
69
70
  },
70
71
  {
package/docs/styles.css CHANGED
@@ -53,4 +53,13 @@
53
53
  transform: translateX(-50%);
54
54
  justify-content: center;
55
55
  display: flex;
56
- }
56
+ }
57
+
58
+ img[src$=".svg"] {
59
+ background: transparent !important;
60
+ }
61
+
62
+ img[src$="https://tauri.app/favicon.svg"]
63
+ {
64
+ opacity: 0.3;
65
+ }
@@ -0,0 +1,3 @@
1
+ const { createOclifCommand } = require("../utils/factory.js");
2
+
3
+ module.exports = createOclifCommand("generate");
@@ -55,7 +55,8 @@ class BaseCommand extends Command {
55
55
  `testdriverai-cli-${process.pid}.log`,
56
56
  );
57
57
 
58
- console.log(`Log file created at: ${this.logFilePath}`);
58
+ console.log(`Log file: ${this.logFilePath}`);
59
+ console.log("");
59
60
  fs.writeFileSync(this.logFilePath, ""); // Initialize the log file
60
61
  }
61
62
 
@@ -68,11 +69,15 @@ class BaseCommand extends Command {
68
69
  );
69
70
  };
70
71
 
72
+ let isConnected = false;
73
+
71
74
  // Use pattern matching for log events, but skip log:Debug
72
75
  this.agent.emitter.on("log:*", (message) => {
73
76
  const event = this.agent.emitter.event;
74
77
 
75
78
  if (event === events.log.debug) return;
79
+
80
+ if (event === events.log.narration && isConnected) return;
76
81
  console.log(message);
77
82
  });
78
83
 
@@ -90,6 +95,7 @@ class BaseCommand extends Command {
90
95
 
91
96
  // Handle sandbox connection with pattern matching for subsequent events
92
97
  this.agent.emitter.on("sandbox:connected", () => {
98
+ isConnected = true;
93
99
  // Once sandbox is connected, send all log and error events to sandbox
94
100
  this.agent.emitter.on("log:*", (message) => {
95
101
  this.sendToSandbox(message);
@@ -134,6 +140,7 @@ class BaseCommand extends Command {
134
140
 
135
141
  // Handle show window events
136
142
  this.agent.emitter.on("show-window", async (url) => {
143
+ console.log("");
137
144
  console.log(`Live test execution: `);
138
145
  if (this.agent.config.CI) {
139
146
  let u = new URL(url);
@@ -178,7 +185,7 @@ class BaseCommand extends Command {
178
185
  // Prepare CLI args for the agent with all derived options
179
186
  const cliArgs = {
180
187
  command: this.id,
181
- args: [filePath], // Pass the resolved file path as the first argument
188
+ args: filePath ? [filePath] : [], // Only pass file path if it exists
182
189
  options: {
183
190
  ...flags,
184
191
  resultFile:
@@ -44,7 +44,7 @@ class CustomTransport extends Transport {
44
44
  // responsible for rendering ai markdown output
45
45
  const { marked } = require("marked");
46
46
  const { markedTerminal } = require("marked-terminal");
47
- const { censorSensitiveData } = require("../agent/lib/censorship");
47
+ const { censorSensitiveDataDeep } = require("../agent/lib/censorship");
48
48
 
49
49
  const { printf } = winston.format;
50
50
 
@@ -56,7 +56,7 @@ const logger = winston.createLogger({
56
56
  format: winston.format.combine(
57
57
  winston.format.splat(),
58
58
  winston.format((info) => {
59
- info.message = censorSensitiveData(info.message);
59
+ info.message = censorSensitiveDataDeep(info.message);
60
60
  return info;
61
61
  })(),
62
62
  logFormat,
@@ -340,7 +340,7 @@ const createMarkdownLogger = (emitter) => {
340
340
 
341
341
  let diff = consoleOutput.replace(previousConsoleOutput, "");
342
342
  if (diff) {
343
- diff = censorSensitiveData(diff);
343
+ diff = censorSensitiveDataDeep(diff);
344
344
  process.stdout.write(diff);
345
345
  }
346
346
  });
@@ -357,7 +357,7 @@ const createMarkdownLogger = (emitter) => {
357
357
  let diff = consoleOutput.replace(previousConsoleOutput, "");
358
358
 
359
359
  if (diff) {
360
- diff = censorSensitiveData(diff);
360
+ diff = censorSensitiveDataDeep(diff);
361
361
  process.stdout.write(diff);
362
362
  }
363
363
  process.stdout.write("\n\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "6.0.32",
3
+ "version": "6.1.1-canary.613bfa3.0",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,7 @@
1
+ version: 6.0.0
2
+ steps:
3
+ - prompt: stop dashcam
4
+ commands:
5
+ - command: exec
6
+ lang: pwsh
7
+ code: dashcam -t 'Web Test Recording' -p
@@ -2,6 +2,12 @@ version: 6.0.0
2
2
  steps:
3
3
  - prompt: launch chrome
4
4
  commands:
5
+ - command: exec
6
+ lang: pwsh
7
+ code: dashcam track --name=TestDriver --type=application --pattern="C:\Users\testdriver\Documents\testdriver.log"
8
+ - command: exec
9
+ lang: pwsh
10
+ code: dashcam start
5
11
  - command: exec
6
12
  lang: pwsh
7
13
  code: |
@@ -1,8 +1,7 @@
1
- version: 5.1.1
2
- session: 67f00511acbd9ccac373edf7
1
+ version: 6.0.0
3
2
  steps:
4
3
  - prompt: stop dashcam
5
4
  commands:
6
5
  - command: exec
7
6
  lang: pwsh
8
- code: dashcam -t '${TD_THIS_FILE}' -p
7
+ code: dashcam -t 'Web Test Recording' -p
@@ -1,7 +1,6 @@
1
1
  version: 6.0.0
2
- session: 67f00511acbd9ccac373edf7
3
2
  steps:
4
- - prompt: start dashcam
3
+ - prompt: launch chrome
5
4
  commands:
6
5
  - command: exec
7
6
  lang: pwsh
@@ -9,3 +8,10 @@ steps:
9
8
  - command: exec
10
9
  lang: pwsh
11
10
  code: dashcam start
11
+ - command: exec
12
+ lang: pwsh
13
+ code: |
14
+ Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList "--start-maximized", "--guest", "https://testdriver-sandbox.vercel.app/login"
15
+ - command: wait-for-text
16
+ text: "TestDriver.ai Sandbox"
17
+ timeout: 60000
@@ -1,12 +0,0 @@
1
- version: 5.1.1
2
- session: 67f00511acbd9ccac373edf7
3
- steps:
4
- - prompt: launch chrome
5
- commands:
6
- - command: exec
7
- lang: pwsh
8
- code: |
9
- Start-Process "C:\Program Files\Google\Chrome\Application\chrome.exe" -ArgumentList "--start-maximized --disable-infobars --disable-fre --no-default-browser-check --no-first-run --guest --load-extension=$(pwd)/node_modules/dashcam-chrome/build", "https://testdriver-sandbox.vercel.app/"
10
- - command: wait-for-text
11
- text: ${TD_WEBSITE}
12
- timeout: 60000