testdriverai 5.2.1 → 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.
Files changed (107) hide show
  1. package/.github/workflows/test-install.yml +1 -1
  2. package/README.md +5 -11
  3. package/agent.js +135 -99
  4. package/docs/30x30.mdx +84 -0
  5. package/docs/action/browser.mdx +129 -0
  6. package/docs/action/os.mdx +157 -0
  7. package/docs/action/output.mdx +98 -0
  8. package/docs/action/performance.mdx +71 -0
  9. package/docs/action/prerun.mdx +80 -0
  10. package/docs/action/secrets.mdx +103 -0
  11. package/docs/action/setup.mdx +115 -0
  12. package/docs/bugs/jira.mdx +208 -0
  13. package/docs/cli/overview.mdx +65 -0
  14. package/docs/commands/assert.mdx +31 -0
  15. package/docs/commands/exec.mdx +42 -0
  16. package/docs/commands/focus-application.mdx +29 -0
  17. package/docs/commands/hover-image.mdx +32 -0
  18. package/docs/commands/hover-text.mdx +37 -0
  19. package/docs/commands/if.mdx +43 -0
  20. package/docs/commands/match-image.mdx +41 -0
  21. package/docs/commands/press-keys.mdx +30 -0
  22. package/docs/commands/run.mdx +30 -0
  23. package/docs/commands/scroll-until-image.mdx +33 -0
  24. package/docs/commands/scroll-until-text.mdx +37 -0
  25. package/docs/commands/scroll.mdx +33 -0
  26. package/docs/commands/type.mdx +29 -0
  27. package/docs/commands/wait-for-image.mdx +31 -0
  28. package/docs/commands/wait-for-text.mdx +35 -0
  29. package/docs/commands/wait.mdx +30 -0
  30. package/docs/docs.json +226 -0
  31. package/docs/exporting/playwright.mdx +159 -0
  32. package/docs/features/auto-healing.mdx +124 -0
  33. package/docs/features/cross-platform.mdx +106 -0
  34. package/docs/features/generation.mdx +180 -0
  35. package/docs/features/github.mdx +161 -0
  36. package/docs/features/parallel-testing.mdx +130 -0
  37. package/docs/features/reusable-snippets.mdx +124 -0
  38. package/docs/features/selectorless.mdx +62 -0
  39. package/docs/features/visual-assertions.mdx +123 -0
  40. package/docs/getting-started/ci.mdx +196 -0
  41. package/docs/getting-started/generating.mdx +210 -0
  42. package/docs/getting-started/running.mdx +67 -0
  43. package/docs/getting-started/setup.mdx +133 -0
  44. package/docs/getting-started/writing.mdx +99 -0
  45. package/docs/guide/assertions.mdx +195 -0
  46. package/docs/guide/authentication.mdx +150 -0
  47. package/docs/guide/code.mdx +169 -0
  48. package/docs/guide/locating.mdx +136 -0
  49. package/docs/guide/setup-teardown.mdx +161 -0
  50. package/docs/guide/variables.mdx +218 -0
  51. package/docs/guide/waiting.mdx +199 -0
  52. package/docs/importing/csv.mdx +196 -0
  53. package/docs/importing/gherkin.mdx +142 -0
  54. package/docs/importing/jira.mdx +172 -0
  55. package/docs/importing/testrail.mdx +161 -0
  56. package/docs/integrations/electron.mdx +152 -0
  57. package/docs/integrations/netlify.mdx +98 -0
  58. package/docs/integrations/vercel.mdx +177 -0
  59. package/docs/interactive/assert.mdx +51 -0
  60. package/docs/interactive/generate.mdx +41 -0
  61. package/docs/interactive/run.mdx +36 -0
  62. package/docs/interactive/save.mdx +53 -0
  63. package/docs/interactive/undo.mdx +47 -0
  64. package/docs/issues.mdx +9 -0
  65. package/docs/overview/comparison.mdx +82 -0
  66. package/docs/overview/faq.mdx +122 -0
  67. package/docs/overview/quickstart.mdx +66 -0
  68. package/docs/overview/what-is-testdriver.mdx +73 -0
  69. package/docs/quickstart.mdx +66 -0
  70. package/docs/reference/commands/scroll.mdx +0 -0
  71. package/docs/reference/interactive/assert.mdx +0 -0
  72. package/docs/security/action.mdx +62 -0
  73. package/docs/security/agent.mdx +62 -0
  74. package/docs/security/dashboard.mdx +0 -0
  75. package/docs/security/platform.mdx +54 -0
  76. package/docs/tutorials/advanced-test.mdx +79 -0
  77. package/docs/tutorials/basic-test.mdx +41 -0
  78. package/electron/icon.png +0 -0
  79. package/electron/overlay.html +7 -3
  80. package/electron/overlay.js +76 -16
  81. package/electron/tray-buffered.png +0 -0
  82. package/electron/tray.png +0 -0
  83. package/index.js +75 -34
  84. package/lib/commander.js +22 -1
  85. package/lib/commands.js +87 -19
  86. package/lib/config.js +10 -1
  87. package/lib/focus-application.js +30 -23
  88. package/lib/generator.js +58 -7
  89. package/lib/init.js +48 -19
  90. package/lib/ipc.js +50 -0
  91. package/lib/logger.js +19 -6
  92. package/lib/overlay.js +82 -36
  93. package/lib/parser.js +9 -7
  94. package/lib/resources/prerun.yaml +17 -0
  95. package/lib/sandbox.js +2 -3
  96. package/lib/sdk.js +0 -2
  97. package/lib/session.js +3 -1
  98. package/lib/speak.js +0 -2
  99. package/lib/subimage/opencv.js +0 -4
  100. package/lib/system.js +56 -39
  101. package/lib/upload-secrets.js +65 -0
  102. package/lib/validation.js +175 -0
  103. package/package.json +2 -1
  104. package/postinstall.js +0 -24
  105. package/lib/websockets.js +0 -85
  106. package/test.md +0 -8
  107. package/test.yml +0 -18
@@ -18,7 +18,7 @@ jobs:
18
18
  fail-fast: false
19
19
  matrix:
20
20
  runner: [macos-latest, windows-latest]
21
- node-version: ['18', '20']
21
+ node-version: ['18', '20', '22']
22
22
 
23
23
  steps:
24
24
  - name: Checkout repository
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.gg/a8Cq739VWn)
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 websocketserver = require("./lib/websockets.js");
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 = false;
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(process.cwd(), "testdriver");
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(process.cwd(), args[file]);
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
- const thisFile = a.file;
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(process.cwd(), dir);
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: ${process.cwd()}`);
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
- await aiExecute(message.data, validateAndLoop, dry);
565
-
566
- logger.debug("showing prompt from exploratoryLoop response check");
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.headings[0])
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() + ".md";
615
- let path1 = path.join(process.cwd(), "testdriver", "generate", fileName);
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(process.cwd(), "testdriver", "generate"))) {
619
- fs.mkdirSync(path.join(process.cwd(), "testdriver", "generate"));
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.listsOrdered[0];
639
+ let list = testPrompt.steps;
623
640
 
624
641
  if (baseYaml && fs.existsSync(baseYaml)) {
625
- list.unshift(`/run ${baseYaml} --embed`);
642
+ list.unshift({step: {
643
+ command: "run",
644
+ file: baseYaml,
645
+ }});
626
646
  }
627
- let contents = list
628
- .map((item, index) => `${index + 1}. ${item}`)
629
- .join("\n");
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
- if (shouldEmbed && (shouldSave || shouldExit)) {
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
- if (shouldEmbed) {
806
- const relativePath = path.relative(
807
- process.cwd(),
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.forEach((step) => {
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(`${step.prompt || "no prompt"}`), null);
1049
+ logger.info(chalk.yellow(`> ${step.prompt || "no prompt"}`), null);
1054
1050
 
1055
- executionHistory.push({
1056
- prompt: step.prompt,
1057
- commands: [], // run will overwrite the commands
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 = process.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 linux sandbox...`));
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(`- setting up...`));
1211
- await sandbox.send({type: 'create', resolution: [1024, 768]});
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.