testdriverai 6.2.0 → 6.2.2

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 (64) hide show
  1. package/.github/workflows/acceptance-tests.yml +2 -0
  2. package/.github/workflows/acceptance-v6.yml +2 -0
  3. package/.github/workflows/lint.yml +4 -1
  4. package/.github/workflows/publish-canary.yml +2 -0
  5. package/.github/workflows/publish-latest.yml +1 -0
  6. package/.github/workflows/self-hosted.yml +102 -0
  7. package/.prettierignore +1 -0
  8. package/.vscode/settings.json +4 -1
  9. package/agent/events.js +1 -10
  10. package/agent/index.js +98 -55
  11. package/agent/interface.js +43 -6
  12. package/agent/lib/censorship.js +15 -10
  13. package/agent/lib/commander.js +31 -18
  14. package/agent/lib/commands.js +62 -17
  15. package/agent/lib/debugger-server.js +0 -5
  16. package/agent/lib/generator.js +2 -2
  17. package/agent/lib/sdk.js +2 -1
  18. package/agent/lib/source-mapper.js +1 -1
  19. package/debugger/index.html +2 -2
  20. package/docs/account/enterprise.mdx +8 -12
  21. package/docs/account/pricing.mdx +2 -2
  22. package/docs/account/projects.mdx +5 -0
  23. package/docs/apps/tauri-apps.mdx +361 -0
  24. package/docs/cli/overview.mdx +6 -6
  25. package/docs/commands/assert.mdx +1 -0
  26. package/docs/commands/hover-text.mdx +3 -1
  27. package/docs/commands/match-image.mdx +5 -4
  28. package/docs/commands/press-keys.mdx +6 -8
  29. package/docs/commands/scroll-until-image.mdx +8 -7
  30. package/docs/commands/scroll-until-text.mdx +7 -6
  31. package/docs/commands/wait-for-image.mdx +5 -4
  32. package/docs/commands/wait-for-text.mdx +6 -5
  33. package/docs/docs.json +42 -40
  34. package/docs/getting-started/playwright.mdx +342 -0
  35. package/docs/getting-started/self-hosting.mdx +370 -0
  36. package/docs/getting-started/vscode.mdx +67 -56
  37. package/docs/guide/dashcam.mdx +118 -0
  38. package/docs/guide/environment-variables.mdx +5 -5
  39. package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
  40. package/docs/images/content/vscode/ide-full.png +0 -0
  41. package/docs/images/content/vscode/running.png +0 -0
  42. package/docs/overview/comparison.mdx +22 -39
  43. package/docs/overview/quickstart.mdx +84 -32
  44. package/docs/styles.css +10 -1
  45. package/interfaces/cli/commands/generate.js +3 -0
  46. package/interfaces/cli/lib/base.js +27 -5
  47. package/interfaces/cli/utils/factory.js +17 -4
  48. package/interfaces/logger.js +4 -4
  49. package/interfaces/readline.js +1 -1
  50. package/package.json +3 -3
  51. package/schema.json +21 -0
  52. package/setup/aws/cloudformation.yaml +463 -0
  53. package/setup/aws/spawn-runner.sh +190 -0
  54. package/testdriver/acceptance/hover-text.yaml +2 -1
  55. package/testdriver/acceptance/prompt.yaml +4 -1
  56. package/testdriver/acceptance/scroll-until-image.yaml +5 -0
  57. package/testdriver/edge-cases/js-exception.yaml +8 -0
  58. package/testdriver/edge-cases/js-promise.yaml +19 -0
  59. package/testdriver/edge-cases/lifecycle/postrun.yaml +10 -0
  60. package/testdriver/edge-cases/success-test.yaml +9 -0
  61. package/testdriver/examples/web/lifecycle/postrun.yaml +7 -0
  62. package/testdriver/examples/web/lifecycle/{provision.yaml → prerun.yaml} +6 -0
  63. package/testdriver/lifecycle/postrun.yaml +7 -0
  64. package/testdriver/lifecycle/prerun.yaml +17 -0
@@ -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,24 +124,25 @@ 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,
135
135
  object.action,
136
136
  object.method,
137
+ object.timeout,
137
138
  );
138
139
  break;
139
140
  case "hover-image":
141
+ emitter.emit(events.log.log, generator.jsonToManual(object));
140
142
  emitter.emit(
141
143
  events.log.narration,
142
144
  `searching for image of ${object.description}`,
143
145
  );
144
- emitter.emit(events.log.log, generator.jsonToManual(object));
145
146
  response = await commands["hover-image"](
146
147
  object.description,
147
148
  object.action,
@@ -153,32 +154,38 @@ commands:
153
154
  events.log.narration,
154
155
  `${object.action} image ${object.path}`,
155
156
  );
156
- response = await commands["match-image"](object.path, object.action);
157
+ response = await commands["match-image"](
158
+ object.path,
159
+ object.action,
160
+ object.invert,
161
+ );
157
162
  break;
158
163
  case "wait-for-image":
164
+ emitter.emit(events.log.log, generator.jsonToManual(object));
159
165
  emitter.emit(
160
166
  events.log.narration,
161
167
  `waiting for ${object.description}`,
162
168
  );
163
- emitter.emit(events.log.log, generator.jsonToManual(object));
164
169
  response = await commands["wait-for-image"](
165
170
  object.description,
166
171
  object.timeout,
172
+ object.invert,
167
173
  );
168
174
  break;
169
175
  case "wait-for-text":
170
- emitter.emit(events.log.narration, `waiting for ${object.text}`);
171
176
  emitter.emit(events.log.log, generator.jsonToManual(object));
177
+ emitter.emit(events.log.narration, `waiting for ${object.text}`);
172
178
  copy.text = "*****";
173
179
  response = await commands["wait-for-text"](
174
180
  object.text,
175
181
  object.timeout,
176
182
  object.method,
183
+ object.invert,
177
184
  );
178
185
  break;
179
186
  case "scroll-until-text":
180
- emitter.emit(events.log.narration, `scrolling until ${object.text}`);
181
187
  emitter.emit(events.log.log, generator.jsonToManual(object));
188
+ emitter.emit(events.log.narration, `scrolling until ${object.text}`);
182
189
  copy.text = "*****";
183
190
  response = await commands["scroll-until-text"](
184
191
  object.text,
@@ -186,24 +193,26 @@ commands:
186
193
  object.distance,
187
194
  object.textMatchMethod,
188
195
  object.method,
196
+ object.invert,
189
197
  );
190
198
  break;
191
199
  case "scroll-until-image": {
192
200
  const needle = object.description || object.path;
193
- emitter.emit(events.log.narration, `scrolling until ${needle}`);
194
201
  emitter.emit(events.log.log, generator.jsonToManual(object));
202
+ emitter.emit(events.log.narration, `scrolling until ${needle}`);
195
203
  response = await commands["scroll-until-image"](
196
204
  object.description,
197
205
  object.direction,
198
206
  object.distance,
199
207
  object.method,
200
208
  object.path,
209
+ object.invert,
201
210
  );
202
211
  break;
203
212
  }
204
213
  case "focus-application":
205
- emitter.emit(events.log.narration, `focusing ${object.name}`);
206
214
  emitter.emit(events.log.log, generator.jsonToManual(object));
215
+ emitter.emit(events.log.narration, `focusing ${object.name}`);
207
216
  response = await commands["focus-application"](object.name);
208
217
  break;
209
218
  case "remember": {
@@ -214,12 +223,16 @@ commands:
214
223
  break;
215
224
  }
216
225
  case "assert":
217
- emitter.emit(events.log.narration, `asserting ${object.expect}`);
218
226
  emitter.emit(events.log.log, generator.jsonToManual(object));
219
- response = await commands.assert(object.expect, object.async);
227
+ emitter.emit(events.log.narration, `asserting ${object.expect}`);
228
+ response = await commands.assert(
229
+ object.expect,
230
+ object.async,
231
+ object.invert,
232
+ );
233
+
220
234
  break;
221
235
  case "exec":
222
- emitter.emit(events.log.narration, `exec`);
223
236
  emitter.emit(
224
237
  events.log.log,
225
238
  generator.jsonToManual({
@@ -170,20 +170,34 @@ const createCommands = (
170
170
  return result;
171
171
  };
172
172
 
173
- const assert = async (assertion, shouldThrow = false, async = false) => {
173
+ const assert = async (
174
+ assertion,
175
+ shouldThrow = false,
176
+ async = false,
177
+ invert = false,
178
+ ) => {
174
179
  if (async) {
175
180
  shouldThrow = true;
176
181
  }
177
182
 
178
183
  const handleAssertResponse = (response) => {
179
- emitter.emit(events.log.markdown.static, response);
184
+ emitter.emit(events.log.log, response);
185
+
186
+ let valid = response.indexOf("The task passed") > -1;
187
+
188
+ if (invert) {
189
+ valid = !valid;
190
+ }
180
191
 
181
- if (response.indexOf("The task passed") > -1) {
192
+ if (valid) {
182
193
  return true;
183
194
  } else {
184
195
  if (shouldThrow) {
185
196
  // Is fatal, othewise it just changes the assertion to be true
186
- throw new MatchError(`AI Assertion failed`, true);
197
+ throw new MatchError(
198
+ `AI Assertion failed ${invert && "(Inverted)"}`,
199
+ true,
200
+ );
187
201
  } else {
188
202
  return false;
189
203
  }
@@ -357,11 +371,12 @@ const createCommands = (
357
371
  description = null,
358
372
  action = "click",
359
373
  method = "turbo",
374
+ timeout = 5000, // we pass this to the subsequent wait-for-text block
360
375
  ) => {
361
376
  text = text ? text.toString() : null;
362
377
 
363
378
  // wait for the text to appear on screen
364
- await commands["wait-for-text"](text, 5000);
379
+ await commands["wait-for-text"](text, timeout);
365
380
 
366
381
  description = description ? description.toString() : null;
367
382
 
@@ -416,7 +431,7 @@ const createCommands = (
416
431
  return response.data;
417
432
  }
418
433
  },
419
- "match-image": async (relativePath, action = "click") => {
434
+ "match-image": async (relativePath, action = "click", invert = false) => {
420
435
  // Resolve the image path relative to the current file
421
436
  const resolvedPath = resolveRelativePath(relativePath);
422
437
 
@@ -424,6 +439,10 @@ const createCommands = (
424
439
 
425
440
  let result = await findImageOnScreen(resolvedPath, image);
426
441
 
442
+ if (invert) {
443
+ result = !result;
444
+ }
445
+
427
446
  if (!result) {
428
447
  throw new CommandError(`Image not found: ${resolvedPath}`);
429
448
  } else {
@@ -463,7 +482,7 @@ const createCommands = (
463
482
  wait: async (timeout = 3000) => {
464
483
  return await delay(timeout);
465
484
  },
466
- "wait-for-image": async (description, timeout = 10000) => {
485
+ "wait-for-image": async (description, timeout = 10000, invert = false) => {
467
486
  emitter.emit(
468
487
  events.log.narration,
469
488
  theme.dim(
@@ -481,6 +500,7 @@ const createCommands = (
481
500
  `An image matching the description "${description}" appears on screen.`,
482
501
  false,
483
502
  false,
503
+ invert,
484
504
  );
485
505
 
486
506
  durationPassed = new Date().getTime() - startTime;
@@ -511,7 +531,12 @@ const createCommands = (
511
531
  );
512
532
  }
513
533
  },
514
- "wait-for-text": async (text, timeout = 5000, method = "turbo") => {
534
+ "wait-for-text": async (
535
+ text,
536
+ timeout = 5000,
537
+ method = "turbo",
538
+ invert = false,
539
+ ) => {
515
540
  await redraw.start();
516
541
 
517
542
  emitter.emit(
@@ -541,7 +566,12 @@ const createCommands = (
541
566
  );
542
567
 
543
568
  passed = response.data;
569
+
570
+ if (invert) {
571
+ passed = !passed;
572
+ }
544
573
  durationPassed = new Date().getTime() - startTime;
574
+
545
575
  if (!passed) {
546
576
  emitter.emit(
547
577
  events.log.narration,
@@ -569,6 +599,7 @@ const createCommands = (
569
599
  maxDistance = 10000,
570
600
  textMatchMethod = "turbo",
571
601
  method = "keyboard",
602
+ invert = false,
572
603
  ) => {
573
604
  await redraw.start();
574
605
 
@@ -616,6 +647,11 @@ const createCommands = (
616
647
  );
617
648
 
618
649
  passed = response.data;
650
+
651
+ if (invert) {
652
+ passed = !passed;
653
+ }
654
+
619
655
  if (!passed) {
620
656
  emitter.emit(
621
657
  events.log.narration,
@@ -644,6 +680,7 @@ const createCommands = (
644
680
  maxDistance = 10000,
645
681
  method = "keyboard",
646
682
  path,
683
+ invert = false,
647
684
  ) => {
648
685
  const needle = description || path;
649
686
 
@@ -673,6 +710,7 @@ const createCommands = (
673
710
  `An image matching the description "${description}" appears on screen.`,
674
711
  false,
675
712
  false,
713
+ invert,
676
714
  );
677
715
  }
678
716
 
@@ -726,8 +764,10 @@ const createCommands = (
726
764
  });
727
765
  return result.data;
728
766
  },
729
- assert: async (assertion, async = false) => {
730
- return await assert(assertion, true, async);
767
+ assert: async (assertion, async = false, invert = false) => {
768
+ let response = await assert(assertion, true, async, invert);
769
+
770
+ return response;
731
771
  },
732
772
  exec: async (language = "pwsh", code, timeout, silent = false) => {
733
773
  emitter.emit(events.log.narration, theme.dim(`calling exec...`), true);
@@ -753,14 +793,14 @@ const createCommands = (
753
793
  `Command failed with exit code ${result.out.returncode}: ${result.out.stderr}`,
754
794
  );
755
795
  } else {
756
- if (!silent) {
757
- emitter.emit(events.log.log, theme.dim(`Command stdout:`), true);
796
+ if (!silent && result.out?.stdout) {
797
+ emitter.emit(events.log.log, theme.dim(`stdout:`), true);
758
798
  emitter.emit(events.log.log, `${result.out.stdout}`, true);
799
+ }
759
800
 
760
- if (result.out.stderr) {
761
- emitter.emit(events.log.log, theme.dim(`Command stderr:`), true);
762
- emitter.emit(events.log.log, `${result.out.stderr}`, true);
763
- }
801
+ if (!silent && result.out.stderr) {
802
+ emitter.emit(events.log.log, theme.dim(`stderr:`), true);
803
+ emitter.emit(events.log.log, `${result.out.stderr}`, true);
764
804
  }
765
805
 
766
806
  return result.out?.stdout?.trim();
@@ -792,7 +832,12 @@ const createCommands = (
792
832
  try {
793
833
  await script.runInNewContext(context);
794
834
  } catch (e) {
795
- console.error(e);
835
+ // Log the error to the emitter instead of console.error to maintain consistency
836
+ emitter.emit(
837
+ events.log.debug,
838
+ `JavaScript execution error: ${e.message}`,
839
+ );
840
+ // Wait a tick to allow any promise rejections to be handled
796
841
  throw new CommandError(`Error running script: ${e.message}`);
797
842
  }
798
843
 
@@ -65,11 +65,6 @@ function createDebuggerServer(config = {}) {
65
65
 
66
66
  ws.on("close", () => {
67
67
  clients.delete(ws);
68
-
69
- // If no clients connected, we can optionally shut down
70
- if (clients.size === 0) {
71
- console.log("No clients connected, keeping server alive");
72
- }
73
68
  });
74
69
 
75
70
  ws.on("error", (error) => {
@@ -1,6 +1,6 @@
1
1
  // parses markdown content to find code blocks, and then extracts yaml from those code blocks
2
2
  const yaml = require("js-yaml");
3
- const package = require("../../package.json");
3
+ const pkg = require("../../package.json");
4
4
  const session = require("./session");
5
5
  const theme = require("./theme");
6
6
  // do the actual parsing
@@ -56,7 +56,7 @@ const jsonToManual = function (json, colors = true) {
56
56
  const dumpToYML = async function (inputArray, sessionInstance = null) {
57
57
  // use yml dump to convert json to yml
58
58
  let yml = await yaml.dump({
59
- version: package.version,
59
+ version: pkg.version,
60
60
  session: sessionInstance ? sessionInstance.get() : session.get(),
61
61
  steps: inputArray,
62
62
  });
package/agent/lib/sdk.js CHANGED
@@ -95,7 +95,7 @@ const createSDK = (emitter, config, sessionInstance) => {
95
95
  return token;
96
96
  } catch (error) {
97
97
  outputError(error);
98
- return;
98
+ throw error; // Re-throw the error so calling code can handle it properly
99
99
  }
100
100
  }
101
101
  };
@@ -194,6 +194,7 @@ const createSDK = (emitter, config, sessionInstance) => {
194
194
  return value;
195
195
  } catch (error) {
196
196
  outputError(error);
197
+ throw error; // Re-throw the error so calling code can handle it properly
197
198
  }
198
199
  };
199
200
 
@@ -263,7 +263,7 @@ class SourceMapper {
263
263
  let description = `${fileName}:${(sourcePosition.step.startLine || 0) + 1}`;
264
264
 
265
265
  if (sourcePosition.command) {
266
- description += `:${(sourcePosition.command.startLine || 0) + 1} (${sourcePosition.command.command || "unknown command"})`;
266
+ description += `:${(sourcePosition.command.startLine || 0) + 1}`;
267
267
  } else {
268
268
  description += ` (step ${sourcePosition.step.stepIndex + 1})`;
269
269
  }
@@ -330,7 +330,7 @@
330
330
  <div class="message">Click to interact with VM</div>
331
331
  </div>
332
332
  </div>
333
- <iframe id="vm-iframe" src=""></iframe>
333
+ <iframe id="vm-iframe" src="" credentialless></iframe>
334
334
  </div>
335
335
 
336
336
  <script>
@@ -340,7 +340,7 @@
340
340
  let parsedData;
341
341
  if (data) {
342
342
  try {
343
- parsedData = JSON.parse(decodeURIComponent(data));
343
+ parsedData = JSON.parse(atob(data));
344
344
  console.log("Data from URL:", parsedData);
345
345
  // You can use parsedData here if needed
346
346
  } catch (error) {
@@ -24,15 +24,15 @@ We specialize in testing scenarios that other tools can't handle - desktop appli
24
24
 
25
25
  ## Enterprise Plans
26
26
 
27
- TestDriver Enterprise plans start at **$995/month** and include:
27
+ TestDriver Enterprise plans start at **$2,000/month** and include:
28
28
 
29
+ - A pilot program with our expert team to create 4 custom tests within your first 4 weeks (4x4 Guarantee).
30
+ - 4 parallel tests
29
31
  - **12,500 runner minutes per month**: Sufficient capacity for continuous testing of your custom test suite.
30
- - Enterprise-grade test dashboards with advanced analytics.
31
32
  - Full CI/CD pipeline integration with custom configurations.
32
33
  - Dedicated infrastructure and ongoing support for complex testing scenarios.
33
- - Expert test creation and maintenance services.
34
34
 
35
- For detailed pricing and contract information, see [Contract Details](#contract-details). For other plans, visit our [Pricing](/account/pricing) page.
35
+ For detailed pricing and contract information our [Pricing](/account/pricing) page. Want unlimited minutes or enhanced security? We also support self-hosted options with 16 parallel tests starting at $2,000/month. See our [Self Hosting](/getting-started/self-hosting) docs for more info.
36
36
 
37
37
  <CardGroup cols={3}>
38
38
  <Card title="Custom Desktop & Extension Testing">
@@ -66,17 +66,14 @@ TestDriver Enterprise provides comprehensive support for fast-moving teams with
66
66
 
67
67
  Testing complex applications requires more than standard automation tools. Desktop applications, browser extensions, and multi-platform workflows demand specialized infrastructure, custom integrations, and deep technical expertise. TestDriver Enterprise provides the complete solution - from initial setup through ongoing maintenance and support.
68
68
 
69
- For more details, see [Contract Details](#contract-details).
70
-
71
69
  ---
72
70
 
73
71
  ## Implementation Process
74
72
 
75
73
  1. **Initial Consultation**: Discuss your specific testing challenges, application architecture, and infrastructure requirements.
76
- 2. **Custom Infrastructure Design**: Configure specialized testing environments tailored to your technology stack and workflow requirements.
77
- 3. **Expert Test Development**: Our team develops 4 custom tests designed specifically for your application's critical user flows and business logic.
78
- 4. **Integration & Deployment**: Implement tests within your CI/CD pipeline with custom monitoring and reporting configurations.
79
- 5. **Team Training & Ongoing Support**: Comprehensive training for your team plus ongoing technical support and consultation.
74
+ 2. **Expert Test Development**: Our team develops 4 custom tests designed specifically for your application's critical user flows and business logic.
75
+ 3. **Integration & Deployment**: Implement tests within your CI/CD pipeline with custom monitoring and reporting configurations.
76
+ 4. **Team Training & Ongoing Support**: Comprehensive training for your team plus ongoing technical support and consultation.
80
77
 
81
78
  Complex applications - particularly desktop software, browser extensions, and multi-platform workflows - present unique testing challenges that require specialized infrastructure and deep technical expertise. TestDriver Enterprise addresses these challenges with custom solutions designed specifically for your application and development process.
82
79
 
@@ -86,7 +83,6 @@ Complex applications - particularly desktop software, browser extensions, and mu
86
83
 
87
84
  | Service | Timeline | Description |
88
85
  | --------------------------------- | ------------- | ------------------------------------------------------------------------------------------------ |
89
- | **Infrastructure Design** | First 7 Days | Analysis and configuration of specialized testing environments for your application stack. |
90
86
  | **Requirements Analysis** | First 7 Days | Comprehensive review of testing requirements and technical specifications. |
91
87
  | **Custom Test Development** | First 4 Weeks | Expert creation of 4 fully customized tests (4x4 Guarantee) tailored to your critical workflows. |
92
88
  | **Training & Knowledge Transfer** | First 30 Days | Technical training for your team and establishment of ongoing support processes. |
@@ -103,7 +99,7 @@ Complex applications - particularly desktop software, browser extensions, and mu
103
99
  - **Service Level**: Dedicated support team and technical consultation included.
104
100
  - **Usage Tracking**: Monthly runner minute allocation with standard overage rates.
105
101
  - **Custom Infrastructure**: Specialized testing environments included for complex applications.
106
- - **Enterprise Options**: On-premises and BYOC (Bring Your Own Cloud) configurations available.
102
+ - **Enterprise Options**: [Self hosting](/getting-started/self-hosting) configurations available.
107
103
 
108
104
  ---
109
105
 
@@ -19,12 +19,12 @@ TestDriver offers a range of pricing plans to suit different needs, from individ
19
19
  </Card>
20
20
  <Card title="Enterprise" icon="shield">
21
21
  Need advanced features? Contact us for tailored solutions. Starting at
22
- $995/month.
22
+ $2,000/month.
23
23
  </Card>
24
24
  </CardGroup>
25
25
 
26
26
  <Tip>
27
- Every plan starts with $100 in TestDriver credits to get you off the starting
27
+ Every plan comes with access to the Playwright SDK to get you off the starting
28
28
  line!
29
29
  </Tip>
30
30
 
@@ -23,6 +23,11 @@ From the Project view, you can see all the replays (Dashes) stored for that proj
23
23
 
24
24
  When you create a new Project, you can also enable the <Icon icon="jira" /> Jira integration to create issues automatically each time a replay (Dash) is created.
25
25
 
26
+ <Info>
27
+ The project ID can be used in conjunction with your `lifecycle/postrun.yaml`
28
+ script to automatically assign a replay to a project. For more info see the
29
+ (Dashcam section)[/guide/dashcam].
30
+ </Info>
26
31
  <Frame caption="Click a Project to view its replays">
27
32
  <img src="/images/content/account/newprojectsettings.png" />
28
33
  </Frame>