testdriverai 5.2.2 → 5.3.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/.github/workflows/test-install.yml +1 -1
- package/README.md +5 -11
- package/agent.js +135 -99
- package/docs/30x30.mdx +84 -0
- package/docs/action/browser.mdx +129 -0
- package/docs/action/os.mdx +157 -0
- package/docs/action/output.mdx +98 -0
- package/docs/action/performance.mdx +71 -0
- package/docs/action/prerun.mdx +80 -0
- package/docs/action/secrets.mdx +103 -0
- package/docs/action/setup.mdx +115 -0
- package/docs/bugs/jira.mdx +208 -0
- package/docs/cli/overview.mdx +65 -0
- package/docs/commands/assert.mdx +31 -0
- package/docs/commands/exec.mdx +42 -0
- package/docs/commands/focus-application.mdx +29 -0
- package/docs/commands/hover-image.mdx +32 -0
- package/docs/commands/hover-text.mdx +37 -0
- package/docs/commands/if.mdx +43 -0
- package/docs/commands/match-image.mdx +41 -0
- package/docs/commands/press-keys.mdx +30 -0
- package/docs/commands/run.mdx +30 -0
- package/docs/commands/scroll-until-image.mdx +33 -0
- package/docs/commands/scroll-until-text.mdx +37 -0
- package/docs/commands/scroll.mdx +33 -0
- package/docs/commands/type.mdx +29 -0
- package/docs/commands/wait-for-image.mdx +31 -0
- package/docs/commands/wait-for-text.mdx +35 -0
- package/docs/commands/wait.mdx +30 -0
- package/docs/docs.json +226 -0
- package/docs/exporting/playwright.mdx +159 -0
- package/docs/features/auto-healing.mdx +124 -0
- package/docs/features/cross-platform.mdx +106 -0
- package/docs/features/generation.mdx +180 -0
- package/docs/features/github.mdx +161 -0
- package/docs/features/parallel-testing.mdx +130 -0
- package/docs/features/reusable-snippets.mdx +124 -0
- package/docs/features/selectorless.mdx +62 -0
- package/docs/features/visual-assertions.mdx +123 -0
- package/docs/getting-started/ci.mdx +196 -0
- package/docs/getting-started/generating.mdx +210 -0
- package/docs/getting-started/running.mdx +67 -0
- package/docs/getting-started/setup.mdx +133 -0
- package/docs/getting-started/writing.mdx +99 -0
- package/docs/guide/assertions.mdx +195 -0
- package/docs/guide/authentication.mdx +150 -0
- package/docs/guide/code.mdx +169 -0
- package/docs/guide/locating.mdx +136 -0
- package/docs/guide/setup-teardown.mdx +161 -0
- package/docs/guide/variables.mdx +218 -0
- package/docs/guide/waiting.mdx +199 -0
- package/docs/importing/csv.mdx +196 -0
- package/docs/importing/gherkin.mdx +142 -0
- package/docs/importing/jira.mdx +172 -0
- package/docs/importing/testrail.mdx +161 -0
- package/docs/integrations/electron.mdx +152 -0
- package/docs/integrations/netlify.mdx +98 -0
- package/docs/integrations/vercel.mdx +177 -0
- package/docs/interactive/assert.mdx +51 -0
- package/docs/interactive/generate.mdx +41 -0
- package/docs/interactive/run.mdx +36 -0
- package/docs/interactive/save.mdx +53 -0
- package/docs/interactive/undo.mdx +47 -0
- package/docs/issues.mdx +9 -0
- package/docs/overview/comparison.mdx +82 -0
- package/docs/overview/faq.mdx +122 -0
- package/docs/overview/quickstart.mdx +66 -0
- package/docs/overview/what-is-testdriver.mdx +73 -0
- package/docs/quickstart.mdx +66 -0
- package/docs/reference/commands/scroll.mdx +0 -0
- package/docs/reference/interactive/assert.mdx +0 -0
- package/docs/security/action.mdx +62 -0
- package/docs/security/agent.mdx +62 -0
- package/docs/security/dashboard.mdx +0 -0
- package/docs/security/platform.mdx +54 -0
- package/docs/tutorials/advanced-test.mdx +79 -0
- package/docs/tutorials/basic-test.mdx +41 -0
- package/electron/icon.png +0 -0
- package/electron/overlay.html +7 -3
- package/electron/overlay.js +75 -15
- package/electron/tray-buffered.png +0 -0
- package/electron/tray.png +0 -0
- package/index.js +75 -34
- package/lib/commander.js +22 -1
- package/lib/commands.js +87 -19
- package/lib/config.js +10 -1
- package/lib/focus-application.js +30 -23
- package/lib/generator.js +58 -7
- package/lib/init.js +48 -19
- package/lib/ipc.js +50 -0
- package/lib/logger.js +19 -6
- package/lib/overlay.js +82 -36
- package/lib/parser.js +9 -7
- package/lib/resources/prerun.yaml +17 -0
- package/lib/sandbox.js +2 -3
- package/lib/sdk.js +0 -2
- package/lib/session.js +3 -1
- package/lib/speak.js +0 -2
- package/lib/subimage/opencv.js +0 -4
- package/lib/system.js +56 -39
- package/lib/upload-secrets.js +65 -0
- package/lib/validation.js +175 -0
- package/package.json +2 -1
- package/postinstall.js +0 -24
- package/lib/websockets.js +0 -85
- package/test.md +0 -8
- package/test.yml +0 -18
package/README.md
CHANGED
|
@@ -4,17 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
Automate and scale QA with computer-use agents.
|
|
6
6
|
|
|
7
|
-
[Docs](https://docs.testdriver.ai) | [Website](https://testdriver.ai) | [GitHub Action](https://github.com/marketplace/actions/testdriver-ai) | [Join our Discord](https://discord.
|
|
7
|
+
[Docs](https://docs.testdriver.ai) | [Website](https://testdriver.ai) | [GitHub Action](https://github.com/marketplace/actions/testdriver-ai) | [Join our Discord](https://discord.com/invite/cWDFW8DzPm)
|
|
8
8
|
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
https://github.com/user-attachments/assets/4719e834-652a-43ba-8b8c-24ea6f357ae3
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# Install via NPM
|
|
9
|
+
---https://github.com/user-attachments/assets/4719e834-652a-43ba-8b8c-24ea6f357ae3# Install via NPM
|
|
18
10
|
|
|
19
11
|
```sh
|
|
20
12
|
npm install testdriverai -g
|
|
@@ -36,7 +28,9 @@ TestDriver isn't like any test framework you've used before. TestDriver is an OS
|
|
|
36
28
|
- **Less Maintenance:** Tests don't break when code changes
|
|
37
29
|
- **More Power:** TestDriver can test any application and control any OS setting
|
|
38
30
|
|
|
39
|
-
### Demo
|
|
31
|
+
### Demo (Playing Balatro Desktop)
|
|
32
|
+
|
|
33
|
+
https://github.com/user-attachments/assets/7cb9ee5a-0d05-4ff0-a4fa-084bcee12e98
|
|
40
34
|
|
|
41
35
|
# Examples
|
|
42
36
|
|
package/agent.js
CHANGED
|
@@ -31,7 +31,7 @@ const sanitizeFilename = require("sanitize-filename");
|
|
|
31
31
|
const macScreenPerms = require("mac-screen-capture-permissions");
|
|
32
32
|
|
|
33
33
|
// local modules
|
|
34
|
-
const
|
|
34
|
+
const { server } = require("./lib/ipc.js");
|
|
35
35
|
const speak = require("./lib/speak.js");
|
|
36
36
|
const analytics = require("./lib/analytics.js");
|
|
37
37
|
const log = require("./lib/logger.js");
|
|
@@ -44,6 +44,7 @@ const commands = require("./lib/commands.js");
|
|
|
44
44
|
const init = require("./lib/init.js");
|
|
45
45
|
const config = require("./lib/config.js");
|
|
46
46
|
const sandbox = require("./lib/sandbox.js");
|
|
47
|
+
const uploadSecrets = require("./lib/upload-secrets.js");
|
|
47
48
|
|
|
48
49
|
const { showTerminal, hideTerminal } = require("./lib/focus-application.js");
|
|
49
50
|
const isValidVersion = require("./lib/valid-version.js");
|
|
@@ -63,14 +64,17 @@ let checkCount = 0;
|
|
|
63
64
|
let checkLimit = 7;
|
|
64
65
|
let lastScreenshot = null;
|
|
65
66
|
let rl;
|
|
66
|
-
let wss;
|
|
67
67
|
|
|
68
68
|
// list of prompts that the user has given us
|
|
69
69
|
let tasks = [];
|
|
70
70
|
|
|
71
|
-
let isInteractive =
|
|
71
|
+
let isInteractive = true;
|
|
72
72
|
emitter.on(events.interactive, (data) => {
|
|
73
73
|
isInteractive = data;
|
|
74
|
+
server.broadcast(events.interactive, data);
|
|
75
|
+
});
|
|
76
|
+
emitter.on(events.vm.show, ({ url }) => {
|
|
77
|
+
server.broadcast(events.vm.show, url);
|
|
74
78
|
});
|
|
75
79
|
|
|
76
80
|
// get args from terminal
|
|
@@ -78,6 +82,8 @@ const args = process.argv.slice(2);
|
|
|
78
82
|
|
|
79
83
|
const commandHistoryFile = path.join(os.homedir(), ".testdriver_history");
|
|
80
84
|
|
|
85
|
+
let workingDir = process.cwd();
|
|
86
|
+
|
|
81
87
|
let getArgs = () => {
|
|
82
88
|
let command = 0;
|
|
83
89
|
let file = 1;
|
|
@@ -95,6 +101,8 @@ let getArgs = () => {
|
|
|
95
101
|
|
|
96
102
|
if (args[command] == "init") {
|
|
97
103
|
args[command] = "init";
|
|
104
|
+
} else if (args[command] == "upload-secrets") {
|
|
105
|
+
args[command] = "upload-secrets";
|
|
98
106
|
} else if (args[command] !== "run" && !args[file]) {
|
|
99
107
|
args[file] = args[command];
|
|
100
108
|
args[command] = "edit";
|
|
@@ -104,7 +112,7 @@ let getArgs = () => {
|
|
|
104
112
|
|
|
105
113
|
if (!args[file]) {
|
|
106
114
|
// make testdriver directory if it doesn't exist
|
|
107
|
-
let testdriverFolder = path.join(
|
|
115
|
+
let testdriverFolder = path.join(workingDir, "testdriver");
|
|
108
116
|
if (!fs.existsSync(testdriverFolder)) {
|
|
109
117
|
fs.mkdirSync(testdriverFolder);
|
|
110
118
|
}
|
|
@@ -114,7 +122,7 @@ let getArgs = () => {
|
|
|
114
122
|
|
|
115
123
|
// turn args[file] into local path
|
|
116
124
|
if (args[file]) {
|
|
117
|
-
args[file] = path.join(
|
|
125
|
+
args[file] = path.join(workingDir, args[file]);
|
|
118
126
|
if (!args[file].endsWith(".yaml")) {
|
|
119
127
|
args[file] += ".yaml";
|
|
120
128
|
}
|
|
@@ -125,7 +133,7 @@ let getArgs = () => {
|
|
|
125
133
|
|
|
126
134
|
let a = getArgs();
|
|
127
135
|
|
|
128
|
-
|
|
136
|
+
let thisFile = a.file;
|
|
129
137
|
const thisCommand = a.command;
|
|
130
138
|
|
|
131
139
|
logger.info(chalk.green(`Howdy! I'm TestDriver v${package.version}`));
|
|
@@ -153,7 +161,7 @@ function fileCompleter(line) {
|
|
|
153
161
|
partial = line.slice(lastSepIndex + 1);
|
|
154
162
|
}
|
|
155
163
|
try {
|
|
156
|
-
const dirPath = path.resolve(
|
|
164
|
+
const dirPath = path.resolve(workingDir, dir);
|
|
157
165
|
|
|
158
166
|
let files = fs.readdirSync(dirPath);
|
|
159
167
|
files = files.map((file) => {
|
|
@@ -171,10 +179,10 @@ function fileCompleter(line) {
|
|
|
171
179
|
}
|
|
172
180
|
|
|
173
181
|
function completer(line) {
|
|
174
|
-
let completions = "/summarize /save /run /quit /assert /undo /manual /yml".split(
|
|
182
|
+
let completions = "/summarize /save /run /quit /assert /undo /manual /yml /js /exec".split(
|
|
175
183
|
" ",
|
|
176
184
|
);
|
|
177
|
-
if (line.startsWith("/run ")) {
|
|
185
|
+
if (line.startsWith("/run ") || line.startsWith("/explore ")) {
|
|
178
186
|
return fileCompleter(line);
|
|
179
187
|
} else {
|
|
180
188
|
completions.concat(tasks);
|
|
@@ -270,7 +278,7 @@ const haveAIResolveError = async (error, markdown, depth = 0, undo = true) => {
|
|
|
270
278
|
|
|
271
279
|
speak("thinking...");
|
|
272
280
|
notify("thinking...");
|
|
273
|
-
|
|
281
|
+
server.broadcast("status", `thinking...`);
|
|
274
282
|
logger.info(chalk.dim("thinking..."), true);
|
|
275
283
|
logger.info("");
|
|
276
284
|
|
|
@@ -310,6 +318,7 @@ const check = async () => {
|
|
|
310
318
|
|
|
311
319
|
logger.info("");
|
|
312
320
|
logger.info(chalk.dim("checking..."), "testdriver");
|
|
321
|
+
server.broadcast("status", `checking...`);
|
|
313
322
|
logger.info("");
|
|
314
323
|
|
|
315
324
|
let thisScreenshot = await system.captureScreenBase64(1, false, true);
|
|
@@ -345,6 +354,8 @@ const check = async () => {
|
|
|
345
354
|
const runCommand = async (command, depth) => {
|
|
346
355
|
let yml = await yaml.dump(command);
|
|
347
356
|
|
|
357
|
+
await save({});
|
|
358
|
+
|
|
348
359
|
logger.debug(`running command: \n\n${yml}`);
|
|
349
360
|
|
|
350
361
|
try {
|
|
@@ -467,7 +478,7 @@ const loadYML = async (file) => {
|
|
|
467
478
|
} catch (e) {
|
|
468
479
|
logger.error(e);
|
|
469
480
|
logger.error(`File not found: ${file}`);
|
|
470
|
-
logger.error(`Current directory: ${
|
|
481
|
+
logger.error(`Current directory: ${workingDir}`);
|
|
471
482
|
|
|
472
483
|
await summarize("File not found");
|
|
473
484
|
await exit(true);
|
|
@@ -513,6 +524,7 @@ const assert = async (expect) => {
|
|
|
513
524
|
|
|
514
525
|
speak("thinking...");
|
|
515
526
|
notify("thinking...");
|
|
527
|
+
server.broadcast("status", `thinking...`);
|
|
516
528
|
logger.info(chalk.dim("thinking..."), true);
|
|
517
529
|
logger.info("");
|
|
518
530
|
|
|
@@ -539,6 +551,7 @@ const exploratoryLoop = async (currentTask, dry = false, validateAndLoop = false
|
|
|
539
551
|
|
|
540
552
|
speak("thinking...");
|
|
541
553
|
notify("thinking...");
|
|
554
|
+
server.broadcast("status", `thinking...`);
|
|
542
555
|
logger.info(chalk.dim("thinking..."), true);
|
|
543
556
|
logger.info("");
|
|
544
557
|
|
|
@@ -561,11 +574,14 @@ const exploratoryLoop = async (currentTask, dry = false, validateAndLoop = false
|
|
|
561
574
|
);
|
|
562
575
|
mdStream.end();
|
|
563
576
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
577
|
+
if (message) {
|
|
578
|
+
await aiExecute(message.data, validateAndLoop, dry);
|
|
579
|
+
logger.debug("showing prompt from exploratoryLoop response check");
|
|
580
|
+
}
|
|
581
|
+
|
|
568
582
|
await save({ silent: false });
|
|
583
|
+
|
|
584
|
+
return;
|
|
569
585
|
};
|
|
570
586
|
|
|
571
587
|
const generate = async (type, count, baseYaml, skipYaml = false) => {
|
|
@@ -573,7 +589,7 @@ const generate = async (type, count, baseYaml, skipYaml = false) => {
|
|
|
573
589
|
|
|
574
590
|
speak("thinking...");
|
|
575
591
|
notify("thinking...");
|
|
576
|
-
|
|
592
|
+
server.broadcast("status", `thinking...`);
|
|
577
593
|
logger.info(chalk.dim("thinking..."), true);
|
|
578
594
|
logger.info("");
|
|
579
595
|
|
|
@@ -604,29 +620,34 @@ const generate = async (type, count, baseYaml, skipYaml = false) => {
|
|
|
604
620
|
|
|
605
621
|
// for each testPrompt
|
|
606
622
|
for (const testPrompt of testPrompts) {
|
|
623
|
+
|
|
607
624
|
// with the contents of the testPrompt
|
|
608
625
|
let fileName =
|
|
609
|
-
sanitizeFilename(testPrompt.
|
|
626
|
+
sanitizeFilename(testPrompt.name)
|
|
610
627
|
.trim()
|
|
611
628
|
.replace(/ /g, "-")
|
|
612
629
|
.replace(/['"`]/g, "")
|
|
613
630
|
.replace(/[^a-zA-Z0-9-]/g, "") // remove any non-alphanumeric chars except hyphens
|
|
614
|
-
.toLowerCase() + ".
|
|
615
|
-
let path1 = path.join(
|
|
631
|
+
.toLowerCase() + ".yaml";
|
|
632
|
+
let path1 = path.join(workingDir, "testdriver", "generate", fileName);
|
|
616
633
|
|
|
617
634
|
// create generate directory if it doesn't exist
|
|
618
|
-
if (!fs.existsSync(path.join(
|
|
619
|
-
fs.mkdirSync(path.join(
|
|
635
|
+
if (!fs.existsSync(path.join(workingDir, "testdriver", "generate"))) {
|
|
636
|
+
fs.mkdirSync(path.join(workingDir, "testdriver", "generate"));
|
|
620
637
|
}
|
|
621
638
|
|
|
622
|
-
let list = testPrompt.
|
|
639
|
+
let list = testPrompt.steps;
|
|
623
640
|
|
|
624
641
|
if (baseYaml && fs.existsSync(baseYaml)) {
|
|
625
|
-
list.unshift(
|
|
642
|
+
list.unshift({step: {
|
|
643
|
+
command: "run",
|
|
644
|
+
file: baseYaml,
|
|
645
|
+
}});
|
|
626
646
|
}
|
|
627
|
-
let contents =
|
|
628
|
-
.
|
|
629
|
-
|
|
647
|
+
let contents = yaml.dump({
|
|
648
|
+
version: package.version,
|
|
649
|
+
steps: list
|
|
650
|
+
});
|
|
630
651
|
fs.writeFileSync(path1, contents);
|
|
631
652
|
}
|
|
632
653
|
|
|
@@ -712,21 +733,9 @@ const ensureMacScreenPerms = async () => {
|
|
|
712
733
|
}
|
|
713
734
|
}
|
|
714
735
|
|
|
715
|
-
const newSession = async () => {
|
|
716
|
-
// should be start of new session
|
|
717
|
-
const sessionRes = await sdk.req("session/start", {
|
|
718
|
-
systemInformationOsInfo: await system.getSystemInformationOsInfo(),
|
|
719
|
-
mousePosition: await system.getMousePosition(),
|
|
720
|
-
activeWindow: await system.activeWin(),
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
session.set(sessionRes.data.id);
|
|
724
|
-
};
|
|
725
|
-
|
|
726
736
|
// simple function to backfill the chat history with a prompt and
|
|
727
737
|
// then call `promptUser()` to get the user input
|
|
728
738
|
const firstPrompt = async () => {
|
|
729
|
-
await newSession();
|
|
730
739
|
|
|
731
740
|
// readline is what allows us to get user input
|
|
732
741
|
rl = readline.createInterface({
|
|
@@ -748,6 +757,7 @@ const firstPrompt = async () => {
|
|
|
748
757
|
// this is how we parse user input
|
|
749
758
|
// notice that the AI is only called if the input is not a command
|
|
750
759
|
const handleInput = async (input) => {
|
|
760
|
+
|
|
751
761
|
if (!isInteractive) return;
|
|
752
762
|
if (!input.trim().length) return promptUser();
|
|
753
763
|
|
|
@@ -790,37 +800,16 @@ const firstPrompt = async () => {
|
|
|
790
800
|
await manualInput(commands.slice(1).join(" "));
|
|
791
801
|
} else if (input.indexOf("/run") == 0) {
|
|
792
802
|
const file = commands[1];
|
|
803
|
+
thisFile = file;
|
|
793
804
|
const flags = commands.slice(2);
|
|
794
805
|
let shouldSave = flags.includes("--save") ? true : false;
|
|
795
806
|
let shouldExit = flags.includes("--exit") ? true : false;
|
|
796
|
-
let shouldEmbed = flags.includes("--embed") ? true : false;
|
|
797
807
|
|
|
798
|
-
|
|
799
|
-
await dieOnFatal({
|
|
800
|
-
message:
|
|
801
|
-
"Cannot embed AND save or exit. Please either use --embed or use any combination of --save and --exit.",
|
|
802
|
-
});
|
|
803
|
-
}
|
|
808
|
+
await run(file, shouldSave, shouldExit);
|
|
804
809
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
path.resolve(process.cwd(), file),
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
executionHistory.push({
|
|
812
|
-
prompt: `/run ${relativePath}`,
|
|
813
|
-
commands: [
|
|
814
|
-
{
|
|
815
|
-
command: "run",
|
|
816
|
-
file: relativePath,
|
|
817
|
-
},
|
|
818
|
-
],
|
|
819
|
-
});
|
|
820
|
-
await embed(file, 0);
|
|
821
|
-
} else {
|
|
822
|
-
await run(file, shouldSave, shouldExit);
|
|
823
|
-
}
|
|
810
|
+
} else if (input.indexOf("/explore") == 0) {
|
|
811
|
+
const file = commands[1];
|
|
812
|
+
await run(file, true, true);
|
|
824
813
|
} else if (input.indexOf("/generate") == 0) {
|
|
825
814
|
const skipYaml = commands[4] === "--skip-yaml";
|
|
826
815
|
await generate(commands[1], commands[2], commands[3], skipYaml);
|
|
@@ -828,6 +817,26 @@ const firstPrompt = async () => {
|
|
|
828
817
|
await exploratoryLoop(input.replace('/dry', ''), true, false);
|
|
829
818
|
} else if (input.indexOf("/yaml") == 0) {
|
|
830
819
|
await runRawYML(commands[1]);
|
|
820
|
+
} else if (input.indexOf("/js") == 0) {
|
|
821
|
+
let result = await commander.run({
|
|
822
|
+
command: "exec",
|
|
823
|
+
js: commands.slice(1).join(" "),
|
|
824
|
+
});
|
|
825
|
+
if (result.out) {
|
|
826
|
+
logger.info(result.out.stdout);
|
|
827
|
+
} else if (result.error) {
|
|
828
|
+
logger.error(result.error.result.stdout);
|
|
829
|
+
}
|
|
830
|
+
} else if (input.indexOf("/exec") == 0) {
|
|
831
|
+
let result = await commander.run({
|
|
832
|
+
command: "exec",
|
|
833
|
+
cli: commands.slice(1).join(" "),
|
|
834
|
+
});
|
|
835
|
+
if (result.out) {
|
|
836
|
+
logger.info(result.out.stdout);
|
|
837
|
+
} else if (result.error) {
|
|
838
|
+
logger.error(result.error.result.stdout);
|
|
839
|
+
}
|
|
831
840
|
} else {
|
|
832
841
|
await exploratoryLoop(input, false, true);
|
|
833
842
|
}
|
|
@@ -837,10 +846,7 @@ const firstPrompt = async () => {
|
|
|
837
846
|
};
|
|
838
847
|
|
|
839
848
|
rl.on("line", handleInput);
|
|
840
|
-
|
|
841
|
-
config.TD_VM && wss.addEventListener("input", async (message) => {
|
|
842
|
-
handleInput(message.data);
|
|
843
|
-
});
|
|
849
|
+
server.on("input", handleInput);
|
|
844
850
|
|
|
845
851
|
// if file exists, load it
|
|
846
852
|
if (fs.existsSync(thisFile)) {
|
|
@@ -851,15 +857,8 @@ const firstPrompt = async () => {
|
|
|
851
857
|
fs.readFileSync(thisFile, "utf-8"),
|
|
852
858
|
);
|
|
853
859
|
|
|
854
|
-
if (!object?.steps) {
|
|
855
|
-
analytics.track("load invalid yaml");
|
|
856
|
-
logger.error("Invalid YAML. No steps found.");
|
|
857
|
-
logger.info("Invalid YAML: " + thisFile);
|
|
858
|
-
return await exit(true);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
860
|
// push each step to executionHistory from { commands: {steps: [ { commands: [Array] } ] } }
|
|
862
|
-
object.steps
|
|
861
|
+
object.steps?.forEach((step) => {
|
|
863
862
|
executionHistory.push(step);
|
|
864
863
|
});
|
|
865
864
|
|
|
@@ -973,7 +972,6 @@ let save = async ({ filepath = thisFile, silent = false } = {}) => {
|
|
|
973
972
|
}
|
|
974
973
|
|
|
975
974
|
if (!executionHistory.length) {
|
|
976
|
-
console.log('no exeuction history, not saving')
|
|
977
975
|
return;
|
|
978
976
|
}
|
|
979
977
|
|
|
@@ -1001,6 +999,8 @@ ${regression}
|
|
|
1001
999
|
logger.info(chalk.dim(`saved as ${fileName}`));
|
|
1002
1000
|
}
|
|
1003
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
return;
|
|
1004
1004
|
};
|
|
1005
1005
|
|
|
1006
1006
|
let runRawYML = async (yml) => {
|
|
@@ -1022,14 +1022,10 @@ let runRawYML = async (yml) => {
|
|
|
1022
1022
|
// it parses the markdown file and executes the codeblocks exactly as if they were
|
|
1023
1023
|
// generated by the AI in a single prompt
|
|
1024
1024
|
let run = async (file = thisFile, shouldSave = false, shouldExit = true) => {
|
|
1025
|
-
await newSession();
|
|
1026
1025
|
|
|
1027
1026
|
setTerminalWindowTransparency(true);
|
|
1028
1027
|
emitter.emit(events.interactive, false);
|
|
1029
1028
|
|
|
1030
|
-
// get the current wowrking directory where this file is being executed
|
|
1031
|
-
let cwd = process.cwd();
|
|
1032
|
-
|
|
1033
1029
|
logger.info(chalk.cyan(`running ${file}...`));
|
|
1034
1030
|
|
|
1035
1031
|
let ymlObj = await loadYML(file);
|
|
@@ -1050,12 +1046,24 @@ let run = async (file = thisFile, shouldSave = false, shouldExit = true) => {
|
|
|
1050
1046
|
|
|
1051
1047
|
for (const step of ymlObj.steps) {
|
|
1052
1048
|
logger.info(``, null);
|
|
1053
|
-
logger.info(chalk.yellow(
|
|
1049
|
+
logger.info(chalk.yellow(`> ${step.prompt || "no prompt"}`), null);
|
|
1054
1050
|
|
|
1055
|
-
|
|
1056
|
-
prompt
|
|
1057
|
-
|
|
1058
|
-
})
|
|
1051
|
+
if (!step.commands && !step.prompt) {
|
|
1052
|
+
logger.info(chalk.red("No commands or prompt found"));
|
|
1053
|
+
return await exit(true);
|
|
1054
|
+
} else if (!step.commands) {
|
|
1055
|
+
logger.info(chalk.yellow("No commands found, running exploratory"));
|
|
1056
|
+
await exploratoryLoop(step.prompt, false, true);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (shouldSave) {
|
|
1060
|
+
|
|
1061
|
+
executionHistory.push({
|
|
1062
|
+
prompt: step.prompt,
|
|
1063
|
+
commands: [], // run will overwrite the commands
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
}
|
|
1059
1067
|
|
|
1060
1068
|
let markdown = `\`\`\`yaml
|
|
1061
1069
|
${yaml.dump(step)}
|
|
@@ -1073,7 +1081,6 @@ ${yaml.dump(step)}
|
|
|
1073
1081
|
}
|
|
1074
1082
|
|
|
1075
1083
|
setTerminalWindowTransparency(false);
|
|
1076
|
-
emitter.emit(events.interactive, true);
|
|
1077
1084
|
|
|
1078
1085
|
if (shouldExit) {
|
|
1079
1086
|
await summarize();
|
|
@@ -1082,7 +1089,6 @@ ${yaml.dump(step)}
|
|
|
1082
1089
|
};
|
|
1083
1090
|
|
|
1084
1091
|
const promptUser = () => {
|
|
1085
|
-
config.TD_VM && wss.sendToClients("done");
|
|
1086
1092
|
emitter.emit(events.interactive, true);
|
|
1087
1093
|
rl.prompt(true);
|
|
1088
1094
|
};
|
|
@@ -1122,7 +1128,7 @@ const embed = async (file, depth) => {
|
|
|
1122
1128
|
logger.info(`${file} (start)`);
|
|
1123
1129
|
|
|
1124
1130
|
// get the current wowrking directory where this file is being executed
|
|
1125
|
-
let cwd =
|
|
1131
|
+
let cwd = workingDir;
|
|
1126
1132
|
|
|
1127
1133
|
// if the file is not an absolute path, we will try to resolve it
|
|
1128
1134
|
if (!path.isAbsolute(file)) {
|
|
@@ -1145,13 +1151,12 @@ const embed = async (file, depth) => {
|
|
|
1145
1151
|
|
|
1146
1152
|
const buildEnv = async () => {
|
|
1147
1153
|
let win = await system.activeWin();
|
|
1148
|
-
if (config.TD_VM) {
|
|
1149
|
-
wss = websocketserver.create();
|
|
1150
|
-
}
|
|
1151
1154
|
setTerminalApp(win);
|
|
1152
1155
|
await ensureMacScreenPerms();
|
|
1153
1156
|
await makeSandbox();
|
|
1154
|
-
|
|
1157
|
+
await newSession();
|
|
1158
|
+
await runPrerun();
|
|
1159
|
+
};
|
|
1155
1160
|
|
|
1156
1161
|
const start = async () => {
|
|
1157
1162
|
// logger.info(await system.getPrimaryDisplay());
|
|
@@ -1168,18 +1173,21 @@ const start = async () => {
|
|
|
1168
1173
|
|
|
1169
1174
|
if (thisCommand !== "run") {
|
|
1170
1175
|
speak("Howdy! I am TestDriver version " + package.version);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (thisCommand !== "init" || thisCommand !== "upload-secrets") {
|
|
1171
1179
|
|
|
1172
1180
|
if (!config.TD_VM) {
|
|
1173
1181
|
logger.info(
|
|
1174
|
-
chalk.red("Warning!" ) +
|
|
1175
|
-
chalk.dim("Local mode sends screenshots of the desktop to our API."),
|
|
1182
|
+
chalk.red("Warning! " ) +
|
|
1183
|
+
chalk.dim("Local mode sends screenshots of the desktop to our API. Set `TD_VM=true` to run in a secure VM."),
|
|
1176
1184
|
);
|
|
1177
1185
|
logger.info(
|
|
1178
1186
|
chalk.dim("https://docs.testdriver.ai/security-and-privacy/agent"),
|
|
1179
1187
|
);
|
|
1180
1188
|
logger.info("");
|
|
1181
1189
|
}
|
|
1182
|
-
|
|
1190
|
+
|
|
1183
1191
|
}
|
|
1184
1192
|
|
|
1185
1193
|
analytics.track("command", { command: thisCommand, file: thisFile });
|
|
@@ -1194,6 +1202,8 @@ const start = async () => {
|
|
|
1194
1202
|
} else if (thisCommand == "init") {
|
|
1195
1203
|
await init();
|
|
1196
1204
|
process.exit(0);
|
|
1205
|
+
} else if (thisCommand == "upload-secrets") {
|
|
1206
|
+
await uploadSecrets();
|
|
1197
1207
|
}
|
|
1198
1208
|
};
|
|
1199
1209
|
|
|
@@ -1203,19 +1213,25 @@ const makeSandbox = async () => {
|
|
|
1203
1213
|
|
|
1204
1214
|
try {
|
|
1205
1215
|
|
|
1206
|
-
logger.info(chalk.gray(`- creating
|
|
1216
|
+
logger.info(chalk.gray(`- creating sandbox...`));
|
|
1217
|
+
server.broadcast("status", `Creating new sandbox...`);
|
|
1207
1218
|
await sandbox.boot();
|
|
1208
1219
|
logger.info(chalk.gray(`- authenticating...`));
|
|
1220
|
+
server.broadcast("status", `Authenticating...`);
|
|
1209
1221
|
await sandbox.send({type: 'authenticate', apiKey: config.TD_API_KEY, secret: config.TD_SECRET} );
|
|
1210
|
-
logger.info(chalk.gray(`-
|
|
1211
|
-
|
|
1222
|
+
logger.info(chalk.gray(`- configuring...`));
|
|
1223
|
+
server.broadcast("status", `Configuring...`);
|
|
1224
|
+
await sandbox.send({type: 'create', resolution: config.TD_VM_RESOLUTION});
|
|
1212
1225
|
logger.info(chalk.gray(`- starting stream...`));
|
|
1226
|
+
server.broadcast("status", `Starting stream...`);
|
|
1213
1227
|
await sandbox.send({type: 'stream.start'});
|
|
1214
1228
|
let {url} = await sandbox.send({type: 'stream.getUrl'});
|
|
1215
1229
|
logger.info(chalk.gray(`- rendering...`));
|
|
1230
|
+
server.broadcast("status", `Rendering...`);
|
|
1216
1231
|
await sandbox.send({type: 'ready'});
|
|
1217
1232
|
emitter.emit(events.vm.show, {url});
|
|
1218
1233
|
logger.info(chalk.gray(`- booting...`));
|
|
1234
|
+
server.broadcast("status", `Starting...`);
|
|
1219
1235
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1220
1236
|
logger.info(chalk.green(``));
|
|
1221
1237
|
logger.info(chalk.green(`sandbox runner ready!`));
|
|
@@ -1233,6 +1249,26 @@ const makeSandbox = async () => {
|
|
|
1233
1249
|
|
|
1234
1250
|
}
|
|
1235
1251
|
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
const newSession = async () => {
|
|
1255
|
+
// should be start of new session
|
|
1256
|
+
const sessionRes = await sdk.req("session/start", {
|
|
1257
|
+
systemInformationOsInfo: await system.getSystemInformationOsInfo(),
|
|
1258
|
+
mousePosition: await system.getMousePosition(),
|
|
1259
|
+
activeWindow: await system.activeWin(),
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
session.set(sessionRes.data.id);
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
const runPrerun = async () => {
|
|
1266
|
+
const prerunFile = path.join(workingDir, "testdriver", "lifecycle", "prerun.yaml");
|
|
1267
|
+
if (fs.existsSync(prerunFile)) {
|
|
1268
|
+
await run(prerunFile, false, false);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1236
1272
|
process.on("uncaughtException", async (err) => {
|
|
1237
1273
|
analytics.track("uncaughtException", { err });
|
|
1238
1274
|
logger.error("Uncaught Exception: %s", err);
|
package/docs/30x30.mdx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# 30x30 Promotion: 30 Tests in 30 Days Free!
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
We understand that QA coverage is critical and often needed immediately. That’s why we’re offering our **30x30 Promotion**: our team will build and deploy **30 custom tests in 30 days** for free during your trial! Unlike other QA services that take months and cost hundreds of thousands of dollars, TestDriver gets you up and running in just 30 days.
|
|
5
|
+
|
|
6
|
+
This is a **limited-time offer**, so don’t miss your chance! [**Schedule an onboarding call now**](#).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Pricing
|
|
11
|
+
We’re confident you’ll love TestDriver, which is why we’re offering this risk-free trial. After the trial, plans start at just **$995/month**, which includes:
|
|
12
|
+
- **12,000 runner minutes per month**: Enough to run your 30 tests **2–5 times per day**.
|
|
13
|
+
- Full access to our enterprise test dashboards.
|
|
14
|
+
- Seamless integration with GitHub Actions.
|
|
15
|
+
|
|
16
|
+
For more details, see the [Promotion Details](#promotion-details) and [Contract Details](#contract-details).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why Choose TestDriver?
|
|
21
|
+
|
|
22
|
+
### Quickly Deploy AI QA Tests
|
|
23
|
+
- Safeguard your most important user flows with AI-generated tests.
|
|
24
|
+
- Increase your total coverage with minimal effort.
|
|
25
|
+
|
|
26
|
+
### Test Flows Never Possible Before
|
|
27
|
+
- Our powerful computer-use agent can test desktop apps, Chrome extensions, mobile apps, and websites.
|
|
28
|
+
|
|
29
|
+
### Spend Less Time on Maintenance
|
|
30
|
+
- TestDriver tests automatically repair themselves, reducing the time spent on maintenance.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Promotion Details
|
|
35
|
+
Here’s what’s included in the **30x30 Promotion**:
|
|
36
|
+
|
|
37
|
+
- **Test Coverage**: Test any publicly available desktop app, Chrome extension, mobile app, or website.
|
|
38
|
+
- **AI-Generated Tests**: Choose from **250 AI-generated tests** within the first 7 days.
|
|
39
|
+
- **Daily Test Runs**: Run tests **3–5 times every day**.
|
|
40
|
+
- **AI Quality Reports**: Receive detailed reports delivered directly to your email.
|
|
41
|
+
- **Self-Healing Tests**: AI automatically fixes and maintains your tests.
|
|
42
|
+
- **GitHub Integration**: Seamlessly integrate with GitHub Actions.
|
|
43
|
+
- **Enterprise Dashboards**: Gain full access to our enterprise-grade test dashboards.
|
|
44
|
+
|
|
45
|
+
For more details, see the [Contract Details](#contract-details).
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## How Does It Work?
|
|
50
|
+
|
|
51
|
+
1. **Book an Onboarding Call**: Share the specific flows you want to test.
|
|
52
|
+
2. **Generate a Test Suite**: We’ll explore your app and generate hundreds of tests using our AI.
|
|
53
|
+
3. **Deploy Tests**: Your new tests will be deployed to our GitHub Actions.
|
|
54
|
+
4. **Custom Test Creation**: Our support team will work with you to create tests for any features TestDriver might have missed.
|
|
55
|
+
5. **Engineer Training**: We’ll train your engineers on best practices for creating and maintaining TestDriver tests.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Onboarding Process
|
|
60
|
+
|
|
61
|
+
| Service | Timeline | Description |
|
|
62
|
+
|-----------------------|----------------|-----------------------------------------------------------------------------|
|
|
63
|
+
| **Custom Onboarding** | First 7 Days | Get set up quickly with custom workflows and Prerun Scripts developed by the TestDriver team. |
|
|
64
|
+
| **AI Test Generation**| First 7 Days | Instant coverage: We’ll generate hundreds of tests for you to choose from. |
|
|
65
|
+
| **Custom Test Creation** | First 30 Days | Our team will create tests for any features TestDriver might have missed. |
|
|
66
|
+
| **Training** | First 30 Days | Our support team will train your engineers on best practices. |
|
|
67
|
+
| **Test Execution** | Recurring | Seamless deployment: Tests are executed on a schedule or via GitHub Actions.|
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Contract Details
|
|
72
|
+
|
|
73
|
+
- **Free Trial**: Get 30 custom tests free during a 30-day trial when subscribing to the $995/month "30x30" plan.
|
|
74
|
+
- **Ownership**: The tests are yours! You can modify, duplicate, or distribute them however you wish.
|
|
75
|
+
- **Payment**: A payment method is required to begin the trial.
|
|
76
|
+
- **Renewal**: The contract renews monthly.
|
|
77
|
+
- **Cancellation**: Cancel anytime, for any reason.
|
|
78
|
+
- **Credits**: Unused credits do not roll over and expire at the end of each month.
|
|
79
|
+
- **Additional Usage**: Additional usage is billed at standard rates.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Ready to Get Started?
|
|
84
|
+
Don’t wait—this is a limited-time offer! [**Schedule your onboarding call now**](#) and let us help you achieve QA coverage in just 30 days.
|