testdriverai 6.0.28 → 6.1.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/.cursor/rules/trigger.basic.mdc +190 -0
- package/.github/instructions/trigger-basic.instructions.md +188 -0
- package/CLAUDE.md +188 -0
- package/agent/index.js +53 -25
- package/agent/interface.js +36 -0
- package/agent/lib/censorship.js +15 -10
- package/agent/lib/generator.js +2 -2
- package/agent/lib/sandbox.js +1 -1
- package/agent/lib/sdk.js +2 -1
- package/index.html +147 -0
- package/interfaces/cli/commands/generate.js +3 -0
- package/interfaces/cli/lib/base.js +1 -1
- package/interfaces/cli/utils/factory.js +16 -7
- package/package.json +3 -1
- package/static-analysis.datadog.yml +10 -0
- package/testdriver/examples/playwright/postrun.yaml +44 -0
- package/testdriver/examples/playwright/provision.yaml +43 -0
- package/testdriver/examples/web/lifecycle/postrun.yaml +7 -0
- package/testdriver/examples/web/lifecycle/{provision.yaml → prerun.yaml} +6 -0
- package/testdriver/generate/test-locked-out-user-login.yaml +37 -0
- package/testdriver/generate/test-standard-user-login.yaml +37 -0
- package/testdriver/generate/test-successful-logout.yaml +36 -0
- package/testdriver/lifecycle/postrun.yaml +2 -3
- package/testdriver/lifecycle/prerun.yaml +8 -2
- package/testdriver/lifecycle/provision.yaml +0 -12
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;
|
|
@@ -87,6 +91,8 @@ class TestDriverAgent extends EventEmitter2 {
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
|
|
95
|
+
|
|
90
96
|
// Create parser instance with this agent's emitter
|
|
91
97
|
this.parser = createParser(this.emitter);
|
|
92
98
|
|
|
@@ -887,30 +893,33 @@ commands:
|
|
|
887
893
|
// based on the current state of the system (primarily the current screenshot)
|
|
888
894
|
// it will generate files that contain only "prompts"
|
|
889
895
|
// @todo revit the generate command
|
|
890
|
-
async generate(
|
|
891
|
-
this.emitter.emit(events.log.debug, "generate called, %s", type);
|
|
896
|
+
async generate(count = 1) {
|
|
892
897
|
|
|
893
|
-
|
|
898
|
+
console.log('generate being called with count:', count)
|
|
894
899
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
+
this.emitter.emit(events.log.debug, `generate called with count: ${count}`);
|
|
901
|
+
|
|
902
|
+
await this.runLifecycle("prerun");
|
|
903
|
+
|
|
904
|
+
this.emitter.emit(events.log.narration, theme.dim("thinking..."), true);
|
|
900
905
|
|
|
901
906
|
let image = await this.system.captureScreenBase64();
|
|
902
907
|
|
|
903
908
|
const streamId = `generate-${Date.now()}`;
|
|
904
909
|
this.emitter.emit(events.log.markdown.start, streamId);
|
|
905
910
|
|
|
911
|
+
let mouse = await this.system.getMousePosition();
|
|
912
|
+
let activeWindow = await this.system.activeWin();
|
|
913
|
+
|
|
906
914
|
let message = await this.sdk.req(
|
|
907
915
|
"generate",
|
|
908
916
|
{
|
|
909
|
-
|
|
917
|
+
prompt: 'make sure to do a spellcheck',
|
|
910
918
|
image,
|
|
911
|
-
mousePosition:
|
|
912
|
-
activeWindow:
|
|
919
|
+
mousePosition: mouse,
|
|
920
|
+
activeWindow: activeWindow,
|
|
913
921
|
count,
|
|
922
|
+
stream: false
|
|
914
923
|
},
|
|
915
924
|
(chunk) => {
|
|
916
925
|
if (chunk.type === "data") {
|
|
@@ -923,6 +932,8 @@ commands:
|
|
|
923
932
|
|
|
924
933
|
let testPrompts = await this.parser.findGenerativePrompts(message.data);
|
|
925
934
|
|
|
935
|
+
console.log(testPrompts)
|
|
936
|
+
|
|
926
937
|
// for each testPrompt
|
|
927
938
|
for (const testPrompt of testPrompts) {
|
|
928
939
|
// with the contents of the testPrompt
|
|
@@ -933,35 +944,37 @@ commands:
|
|
|
933
944
|
.replace(/['"`]/g, "")
|
|
934
945
|
.replace(/[^a-zA-Z0-9-]/g, "") // remove any non-alphanumeric chars except hyphens
|
|
935
946
|
.toLowerCase() + ".yaml";
|
|
947
|
+
|
|
936
948
|
let path1 = path.join(
|
|
937
949
|
this.workingDir,
|
|
938
950
|
"testdriver",
|
|
939
951
|
"generate",
|
|
940
952
|
fileName,
|
|
941
953
|
);
|
|
942
|
-
|
|
943
954
|
// create generate directory if it doesn't exist
|
|
944
|
-
|
|
945
|
-
|
|
955
|
+
const generateDir = path.join(this.workingDir, "testdriver", "generate");
|
|
956
|
+
if (!fs.existsSync(generateDir)) {
|
|
957
|
+
fs.mkdirSync(generateDir);
|
|
958
|
+
console.log('Created generate directory:', generateDir);
|
|
959
|
+
} else {
|
|
960
|
+
console.log('Generate directory already exists:', generateDir);
|
|
946
961
|
}
|
|
947
962
|
|
|
948
963
|
let list = testPrompt.steps;
|
|
949
964
|
|
|
950
|
-
if (baseYaml && fs.existsSync(baseYaml)) {
|
|
951
|
-
list.unshift({
|
|
952
|
-
step: {
|
|
953
|
-
command: "run",
|
|
954
|
-
file: baseYaml,
|
|
955
|
-
},
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
965
|
let contents = yaml.dump({
|
|
959
966
|
version: packageJson.version,
|
|
960
967
|
steps: list,
|
|
961
968
|
});
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
console.log('writing file', path1, contents)
|
|
972
|
+
|
|
962
973
|
fs.writeFileSync(path1, contents);
|
|
963
974
|
}
|
|
964
975
|
|
|
976
|
+
await this.runLifecycle("postrun");
|
|
977
|
+
|
|
965
978
|
this.exit(false);
|
|
966
979
|
}
|
|
967
980
|
|
|
@@ -1813,7 +1826,7 @@ ${regression}
|
|
|
1813
1826
|
}
|
|
1814
1827
|
|
|
1815
1828
|
// if the directory for thisFile doesn't exist, create it
|
|
1816
|
-
if (this.cliArgs.command !== "sandbox") {
|
|
1829
|
+
if (this.cliArgs.command !== "sandbox" && this.cliArgs.command !== "generate") {
|
|
1817
1830
|
const dir = path.dirname(this.thisFile);
|
|
1818
1831
|
if (!fs.existsSync(dir)) {
|
|
1819
1832
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -1838,7 +1851,7 @@ ${regression}
|
|
|
1838
1851
|
await this.sdk.auth();
|
|
1839
1852
|
}
|
|
1840
1853
|
|
|
1841
|
-
if (this.cliArgs.command !== "sandbox") {
|
|
1854
|
+
if (this.cliArgs.command !== "sandbox" && this.cliArgs.command !== "generate") {
|
|
1842
1855
|
this.emitter.emit(
|
|
1843
1856
|
events.log.log,
|
|
1844
1857
|
theme.dim(`Working on ${this.thisFile}`),
|
|
@@ -2032,9 +2045,21 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2032
2045
|
}
|
|
2033
2046
|
|
|
2034
2047
|
async runLifecycle(lifecycleName) {
|
|
2048
|
+
|
|
2049
|
+
console.log("Running lifecycle:", lifecycleName);
|
|
2050
|
+
|
|
2035
2051
|
// Use the current file path from sourceMapper to find the lifecycle directory
|
|
2036
2052
|
// If sourceMapper doesn't have a current file, use thisFile which should be the file being run
|
|
2037
2053
|
let currentFilePath = this.sourceMapper.currentFilePath || this.thisFile;
|
|
2054
|
+
|
|
2055
|
+
console.log("Current file path:", currentFilePath);
|
|
2056
|
+
|
|
2057
|
+
// If we still don't have a currentFilePath, fall back to the default testdriver directory
|
|
2058
|
+
if (!currentFilePath) {
|
|
2059
|
+
currentFilePath = path.join(this.workingDir, "testdriver", "testdriver.yaml");
|
|
2060
|
+
console.log("No currentFilePath found, using fallback:", currentFilePath);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2038
2063
|
// Ensure we have an absolute path
|
|
2039
2064
|
if (currentFilePath && !path.isAbsolute(currentFilePath)) {
|
|
2040
2065
|
currentFilePath = path.resolve(this.workingDir, currentFilePath);
|
|
@@ -2071,6 +2096,9 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2071
2096
|
}
|
|
2072
2097
|
}
|
|
2073
2098
|
}
|
|
2099
|
+
|
|
2100
|
+
console.log(lifecycleFile)
|
|
2101
|
+
|
|
2074
2102
|
if (lifecycleFile) {
|
|
2075
2103
|
// Store current source mapping state before running lifecycle file
|
|
2076
2104
|
const previousContext = this.sourceMapper.saveContext();
|
|
@@ -2140,7 +2168,7 @@ Please check your network connection, TD_API_KEY, or the service status.`,
|
|
|
2140
2168
|
}
|
|
2141
2169
|
|
|
2142
2170
|
// Move environment setup and special handling here
|
|
2143
|
-
if (["edit", "run"].includes(commandName)) {
|
|
2171
|
+
if (["edit", "run", "generate"].includes(commandName)) {
|
|
2144
2172
|
await this.buildEnv(options);
|
|
2145
2173
|
}
|
|
2146
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/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/sandbox.js
CHANGED
|
@@ -23,7 +23,7 @@ const createSandbox = (emitter, analytics) => {
|
|
|
23
23
|
if (this.socket) {
|
|
24
24
|
this.messageId++;
|
|
25
25
|
message.requestId = `${this.uniqueId}-${this.messageId}`;
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
// Start timing for this message
|
|
28
28
|
const timingKey = `sandbox-${message.type}`;
|
|
29
29
|
marky.mark(timingKey);
|
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
|
|
package/index.html
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Login</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
16
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.login-container {
|
|
24
|
+
background: white;
|
|
25
|
+
padding: 2rem;
|
|
26
|
+
border-radius: 10px;
|
|
27
|
+
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
|
28
|
+
width: 100%;
|
|
29
|
+
max-width: 400px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.login-header {
|
|
33
|
+
text-align: center;
|
|
34
|
+
margin-bottom: 2rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.login-header h1 {
|
|
38
|
+
color: #333;
|
|
39
|
+
font-size: 1.8rem;
|
|
40
|
+
margin-bottom: 0.5rem;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.login-header p {
|
|
44
|
+
color: #666;
|
|
45
|
+
font-size: 0.9rem;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.form-group {
|
|
49
|
+
margin-bottom: 1.5rem;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.form-group label {
|
|
53
|
+
display: block;
|
|
54
|
+
margin-bottom: 0.5rem;
|
|
55
|
+
color: #333;
|
|
56
|
+
font-weight: 500;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.form-group input {
|
|
60
|
+
width: 100%;
|
|
61
|
+
padding: 0.75rem;
|
|
62
|
+
border: 2px solid #e1e5e9;
|
|
63
|
+
border-radius: 5px;
|
|
64
|
+
font-size: 1rem;
|
|
65
|
+
transition: border-color 0.3s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.form-group input:focus {
|
|
69
|
+
outline: none;
|
|
70
|
+
border-color: #667eea;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.login-btn {
|
|
74
|
+
width: 100%;
|
|
75
|
+
padding: 0.75rem;
|
|
76
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
77
|
+
color: white;
|
|
78
|
+
border: none;
|
|
79
|
+
border-radius: 5px;
|
|
80
|
+
font-size: 1rem;
|
|
81
|
+
font-weight: 500;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
transition: transform 0.2s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.login-btn:hover {
|
|
87
|
+
transform: translateY(-2px);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.forgot-password {
|
|
91
|
+
text-align: center;
|
|
92
|
+
margin-top: 1rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.forgot-password a {
|
|
96
|
+
color: #667eea;
|
|
97
|
+
text-decoration: none;
|
|
98
|
+
font-size: 0.9rem;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.forgot-password a:hover {
|
|
102
|
+
text-decoration: underline;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
</head>
|
|
106
|
+
<body>
|
|
107
|
+
<div class="login-container">
|
|
108
|
+
<div class="login-header">
|
|
109
|
+
<h1>Welcome Back</h1>
|
|
110
|
+
<p>Please sign in to your account</p>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<form id="loginForm">
|
|
114
|
+
<div class="form-group">
|
|
115
|
+
<label for="email">Email Address</label>
|
|
116
|
+
<input type="email" id="email" name="email" required>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div class="form-group">
|
|
120
|
+
<label for="password">Password</label>
|
|
121
|
+
<input type="password" id="password" name="password" required>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<button type="submit" class="login-btn">Sign In</button>
|
|
125
|
+
</form>
|
|
126
|
+
|
|
127
|
+
<div class="forgot-password">
|
|
128
|
+
<a href="#" onclick="alert('Password reset functionality would go here')">Forgot your password?</a>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<script>
|
|
133
|
+
document.getElementById('loginForm').addEventListener('submit', function(e) {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
|
|
136
|
+
const email = document.getElementById('email').value;
|
|
137
|
+
const password = document.getElementById('password').value;
|
|
138
|
+
|
|
139
|
+
if (email && password) {
|
|
140
|
+
alert('Login functionality would be implemented here.\nEmail: ' + email);
|
|
141
|
+
} else {
|
|
142
|
+
alert('Please fill in all fields');
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
</script>
|
|
146
|
+
</body>
|
|
147
|
+
</html>
|
|
@@ -178,7 +178,7 @@ class BaseCommand extends Command {
|
|
|
178
178
|
// Prepare CLI args for the agent with all derived options
|
|
179
179
|
const cliArgs = {
|
|
180
180
|
command: this.id,
|
|
181
|
-
args: [filePath], //
|
|
181
|
+
args: filePath ? [filePath] : [], // Only pass file path if it exists
|
|
182
182
|
options: {
|
|
183
183
|
...flags,
|
|
184
184
|
resultFile:
|
|
@@ -28,17 +28,26 @@ function createOclifCommand(commandName) {
|
|
|
28
28
|
this.agent.readlineInterface = readlineInterface;
|
|
29
29
|
await readlineInterface.start();
|
|
30
30
|
} else {
|
|
31
|
-
// For
|
|
32
|
-
const fileArg = args.file || args.action || null;
|
|
33
|
-
await this.setupAgent(fileArg, flags);
|
|
34
|
-
|
|
31
|
+
// For other commands, use the unified command system
|
|
35
32
|
if (commandName === "run") {
|
|
33
|
+
const fileArg = args.file || args.action || null;
|
|
34
|
+
await this.setupAgent(fileArg, flags);
|
|
36
35
|
// Set error limit higher for run command
|
|
37
36
|
this.agent.errorLimit = 100;
|
|
37
|
+
// Execute through unified command system
|
|
38
|
+
await this.agent.executeUnifiedCommand(commandName, [fileArg], flags);
|
|
39
|
+
} else if (commandName === "generate") {
|
|
40
|
+
// For generate command, pass the file argument if provided
|
|
41
|
+
const fileArg = args.file || null;
|
|
42
|
+
await this.setupAgent(fileArg, flags);
|
|
43
|
+
// Execute through unified command system
|
|
44
|
+
await this.agent.executeUnifiedCommand(commandName, [fileArg], flags);
|
|
45
|
+
} else {
|
|
46
|
+
const primaryArg = args.file || args.action || null;
|
|
47
|
+
await this.setupAgent(primaryArg, flags);
|
|
48
|
+
// Execute through unified command system
|
|
49
|
+
await this.agent.executeUnifiedCommand(commandName, [primaryArg], flags);
|
|
38
50
|
}
|
|
39
|
-
|
|
40
|
-
// Execute through unified command system
|
|
41
|
-
await this.agent.executeUnifiedCommand(commandName, [fileArg], flags);
|
|
42
51
|
}
|
|
43
52
|
} catch (error) {
|
|
44
53
|
console.error(`Error executing ${commandName} command:`, error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testdriverai",
|
|
3
|
-
"version": "6.0
|
|
3
|
+
"version": "6.1.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": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@oclif/plugin-not-found": "^3.2.59",
|
|
35
35
|
"@oclif/plugin-warn-if-update-available": "^3.1.43",
|
|
36
36
|
"@stoplight/yaml-ast-parser": "^0.0.50",
|
|
37
|
+
"@trigger.dev/sdk": "^4.0.2",
|
|
37
38
|
"ajv": "^8.17.1",
|
|
38
39
|
"arktype": "^2.1.19",
|
|
39
40
|
"axios": "^1.7.7",
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
68
|
"@eslint/js": "^9.10.0",
|
|
69
|
+
"@trigger.dev/build": "^4.0.2",
|
|
68
70
|
"chai": "^5.1.2",
|
|
69
71
|
"esbuild": "0.20.2",
|
|
70
72
|
"esbuild-plugin-fileloc": "^0.0.6",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
version: 6.0.23
|
|
2
|
+
session: 68acf25541189ac907c336fe
|
|
3
|
+
steps:
|
|
4
|
+
- prompt: Upload test to Airtable
|
|
5
|
+
commands:
|
|
6
|
+
- command: exec
|
|
7
|
+
lang: pwsh
|
|
8
|
+
timeout: 30000
|
|
9
|
+
code: |
|
|
10
|
+
# Find the test file
|
|
11
|
+
$testFile = Join-Path ([Environment]::GetFolderPath("Desktop")) "pw-test.spec.js"
|
|
12
|
+
|
|
13
|
+
if (!(Test-Path $testFile)) {
|
|
14
|
+
Write-Host "No test file found"
|
|
15
|
+
exit 0
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Read file content
|
|
19
|
+
$content = Get-Content -Path $testFile -Raw
|
|
20
|
+
$testName = [System.IO.Path]::GetFileNameWithoutExtension("${TD_THIS_FILE}")
|
|
21
|
+
|
|
22
|
+
# Upload to Airtable
|
|
23
|
+
$data = @{
|
|
24
|
+
records = @(@{
|
|
25
|
+
fields = @{
|
|
26
|
+
"Name" = $testName
|
|
27
|
+
"Content" = $content
|
|
28
|
+
"Created" = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
} | ConvertTo-Json -Depth 3
|
|
32
|
+
|
|
33
|
+
$headers = @{
|
|
34
|
+
"Authorization" = "Bearer ${TD_AIRTABLE_TOKEN}"
|
|
35
|
+
"Content-Type" = "application/json"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
$response = Invoke-RestMethod -Uri "https://api.airtable.com/v0/${TD_AIRTABLE_BASE_ID}/${TD_AIRTABLE_TABLE_NAME}" -Method Post -Headers $headers -Body $data
|
|
40
|
+
Write-Host "✅ Uploaded: $($response.records[0].id)"
|
|
41
|
+
} catch {
|
|
42
|
+
Write-Host "❌ Upload failed: $($_.Exception.Message)"
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
version: 6.0.23
|
|
2
|
+
session: 68acf25541189ac907c336fe
|
|
3
|
+
steps:
|
|
4
|
+
- prompt: Download and start playwright codegen
|
|
5
|
+
commands:
|
|
6
|
+
- command: exec
|
|
7
|
+
lang: pwsh
|
|
8
|
+
timeout: 480000
|
|
9
|
+
code: |
|
|
10
|
+
echo "Step 1: Setting up npm environment"
|
|
11
|
+
# Create npm directory if it doesn't exist
|
|
12
|
+
$npmDir = "$env:APPDATA\npm"
|
|
13
|
+
if (!(Test-Path $npmDir)) {
|
|
14
|
+
New-Item -ItemType Directory -Force -Path $npmDir
|
|
15
|
+
echo "Created npm directory: $npmDir"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
echo "Step 2: Installing Node.js and npm (if needed)"
|
|
19
|
+
# Check if node is available, if not try to install it
|
|
20
|
+
try {
|
|
21
|
+
node --version
|
|
22
|
+
npm --version
|
|
23
|
+
} catch {
|
|
24
|
+
echo "Node.js/npm not found, attempting to install..."
|
|
25
|
+
# Try to install Node.js via winget if available
|
|
26
|
+
try {
|
|
27
|
+
winget install OpenJS.NodeJS
|
|
28
|
+
} catch {
|
|
29
|
+
echo "Could not install Node.js automatically. Please install Node.js manually."
|
|
30
|
+
exit 1
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
echo "Step 3: Installing Playwright dependencies"
|
|
35
|
+
npm install -g playwright
|
|
36
|
+
npx playwright install --with-deps chromium
|
|
37
|
+
|
|
38
|
+
echo "Step 4: Generating Playwright script (non-blocking)"
|
|
39
|
+
$desktopPath = [Environment]::GetFolderPath("Desktop")
|
|
40
|
+
$outputFile = Join-Path $desktopPath "pw-test.spec.js"
|
|
41
|
+
Start-Process cmd.exe -ArgumentList "/c npx playwright codegen --target playwright-test -o `"$outputFile`" https://airbnb.com"
|
|
42
|
+
echo "Step 5: Completed"
|
|
43
|
+
exit
|
|
@@ -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: |
|