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 +1 -10
- package/agent/index.js +60 -31
- package/agent/interface.js +36 -0
- package/agent/lib/censorship.js +15 -10
- package/agent/lib/commander.js +16 -16
- package/agent/lib/commands.js +7 -7
- package/agent/lib/debugger-server.js +0 -5
- package/agent/lib/generator.js +2 -2
- package/agent/lib/sdk.js +2 -1
- package/agent/lib/source-mapper.js +1 -1
- package/docs/apps/tauri-apps.mdx +413 -0
- package/docs/docs.json +2 -1
- package/docs/styles.css +10 -1
- package/interfaces/cli/commands/generate.js +3 -0
- package/interfaces/cli/lib/base.js +9 -2
- package/interfaces/logger.js +4 -4
- package/package.json +1 -1
- package/testdriver/examples/web/lifecycle/postrun.yaml +7 -0
- package/testdriver/examples/web/lifecycle/{provision.yaml → prerun.yaml} +6 -0
- package/testdriver/lifecycle/postrun.yaml +2 -3
- package/testdriver/lifecycle/prerun.yaml +8 -2
- package/testdriver/lifecycle/provision.yaml +0 -12
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
|
|
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(
|
|
890
|
-
this.emitter.emit(events.log.debug,
|
|
894
|
+
async generate(count = 1) {
|
|
895
|
+
this.emitter.emit(events.log.debug, `generate called with count: ${count}`);
|
|
891
896
|
|
|
892
|
-
this.
|
|
897
|
+
await this.runLifecycle("prerun");
|
|
893
898
|
|
|
894
|
-
|
|
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
|
-
|
|
912
|
+
prompt: "make sure to do a spellcheck",
|
|
909
913
|
image,
|
|
910
|
-
mousePosition:
|
|
911
|
-
activeWindow:
|
|
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
|
-
|
|
944
|
-
|
|
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, `${
|
|
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, `${
|
|
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 (
|
|
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 (
|
|
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
|
|
package/agent/interface.js
CHANGED
|
@@ -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
|
|
package/agent/lib/censorship.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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)
|
package/agent/lib/commander.js
CHANGED
|
@@ -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({
|
package/agent/lib/commands.js
CHANGED
|
@@ -176,7 +176,7 @@ const createCommands = (
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
const handleAssertResponse = (response) => {
|
|
179
|
-
emitter.emit(events.log.
|
|
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(`
|
|
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
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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) => {
|
package/agent/lib/generator.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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}
|
|
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
|
+

|
|
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
package/docs/styles.css
CHANGED
|
@@ -55,7 +55,8 @@ class BaseCommand extends Command {
|
|
|
55
55
|
`testdriverai-cli-${process.pid}.log`,
|
|
56
56
|
);
|
|
57
57
|
|
|
58
|
-
console.log(`Log file
|
|
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], //
|
|
188
|
+
args: filePath ? [filePath] : [], // Only pass file path if it exists
|
|
182
189
|
options: {
|
|
183
190
|
...flags,
|
|
184
191
|
resultFile:
|
package/interfaces/logger.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
360
|
+
diff = censorSensitiveDataDeep(diff);
|
|
361
361
|
process.stdout.write(diff);
|
|
362
362
|
}
|
|
363
363
|
process.stdout.write("\n\n");
|
package/package.json
CHANGED
|
@@ -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,7 +1,6 @@
|
|
|
1
1
|
version: 6.0.0
|
|
2
|
-
session: 67f00511acbd9ccac373edf7
|
|
3
2
|
steps:
|
|
4
|
-
- prompt:
|
|
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
|