testdriverai 7.1.2 → 7.1.4

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 (38) hide show
  1. package/.github/workflows/test-init.yml +145 -0
  2. package/agent/lib/commander.js +2 -2
  3. package/agent/lib/commands.js +10 -5
  4. package/docs/docs.json +29 -84
  5. package/docs/v7/_drafts/migration.mdx +3 -3
  6. package/docs/v7/api/act.mdx +1 -1
  7. package/docs/v7/api/assert.mdx +1 -1
  8. package/docs/v7/api/assertions.mdx +14 -14
  9. package/docs/v7/getting-started/quickstart.mdx +11 -9
  10. package/docs/v7/getting-started/running-and-debugging.mdx +3 -2
  11. package/docs/v7/getting-started/writing-tests.mdx +4 -5
  12. package/docs/v7/overview/readme.mdx +1 -1
  13. package/docs/v7/presets/chrome.mdx +38 -41
  14. package/docs/v7/presets/electron.mdx +107 -100
  15. package/docs/v7/presets/webapp.mdx +40 -43
  16. package/interfaces/cli/commands/init.js +79 -21
  17. package/interfaces/vitest-plugin.mjs +36 -9
  18. package/lib/vitest/hooks.mjs +5 -1
  19. package/manual/test-init-command.js +223 -0
  20. package/package.json +2 -2
  21. package/schema.json +5 -5
  22. package/sdk-log-formatter.js +1 -1
  23. package/sdk.d.ts +8 -8
  24. package/sdk.js +64 -14
  25. package/test/testdriver/exec-output.test.mjs +1 -1
  26. package/test/testdriver/exec-pwsh.test.mjs +1 -1
  27. package/test/testdriver/focus-window.test.mjs +1 -1
  28. package/test/testdriver/hover-image.test.mjs +1 -1
  29. package/test/testdriver/hover-text-with-description.test.mjs +0 -3
  30. package/test/testdriver/match-image.test.mjs +1 -1
  31. package/test/testdriver/scroll-keyboard.test.mjs +1 -1
  32. package/test/testdriver/scroll-until-image.test.mjs +1 -1
  33. package/test/testdriver/scroll-until-text.test.mjs +18 -1
  34. package/test/testdriver/scroll.test.mjs +1 -1
  35. package/test/testdriver/setup/lifecycleHelpers.mjs +105 -5
  36. package/test/testdriver/setup/testHelpers.mjs +6 -2
  37. package/vitest.config.mjs +7 -2
  38. package/test/testdriver/exec-js.test.mjs +0 -43
@@ -13,13 +13,12 @@ The `webApp()` preset provides a generic interface for testing web applications.
13
13
 
14
14
  ```javascript
15
15
  import { test } from 'vitest';
16
- import { webApp } from 'testdriverai/presets';
16
+ import { TestDriver } from 'testdriverai/vitest/hooks';
17
17
 
18
18
  test('web app test', async (context) => {
19
- const { testdriver } = await webApp(context, {
20
- url: 'https://myapp.com',
21
- browser: 'chrome'
22
- });
19
+ const testdriver = TestDriver(context, { headless: true });
20
+
21
+ await testdriver.provision.chrome({ url: 'https://myapp.com' });
23
22
 
24
23
  await testdriver.find('Login button').click();
25
24
  });
@@ -83,12 +82,12 @@ webApp(context, options): Promise<WebAppResult>
83
82
 
84
83
  ```javascript
85
84
  import { test } from 'vitest';
86
- import { webApp } from 'testdriverai/presets';
85
+ import { TestDriver } from 'testdriverai/vitest/hooks';
87
86
 
88
87
  test('user login flow', async (context) => {
89
- const { testdriver } = await webApp(context, {
90
- url: 'https://myapp.com/login'
91
- });
88
+ const testdriver = TestDriver(context, { headless: true });
89
+
90
+ await testdriver.provision.chrome({ url: 'https://myapp.com/login' });
92
91
 
93
92
  await testdriver.find('email input').type('user@example.com');
94
93
  await testdriver.find('password input').type('password123');
@@ -102,12 +101,12 @@ test('user login flow', async (context) => {
102
101
 
103
102
  ```javascript
104
103
  import { test } from 'vitest';
105
- import { webApp } from 'testdriverai/presets';
104
+ import { TestDriver } from 'testdriverai/vitest/hooks';
106
105
 
107
106
  test('product purchase', async (context) => {
108
- const { testdriver, dashcam } = await webApp(context, {
109
- url: 'https://shop.example.com'
110
- });
107
+ const testdriver = TestDriver(context, { headless: true });
108
+
109
+ await testdriver.provision.chrome({ url: 'https://shop.example.com' });
111
110
 
112
111
  // Search for product
113
112
  await testdriver.find('search input').type('laptop');
@@ -131,12 +130,12 @@ test('product purchase', async (context) => {
131
130
 
132
131
  ```javascript
133
132
  import { test } from 'vitest';
134
- import { webApp } from 'testdriverai/presets';
133
+ import { TestDriver } from 'testdriverai/vitest/hooks';
135
134
 
136
135
  test('single page app routing', async (context) => {
137
- const { testdriver } = await webApp(context, {
138
- url: 'https://spa.example.com'
139
- });
136
+ const testdriver = TestDriver(context, { headless: true });
137
+
138
+ await testdriver.provision.chrome({ url: 'https://spa.example.com' });
140
139
 
141
140
  // Navigate through SPA routes
142
141
  await testdriver.find('About link').click();
@@ -154,12 +153,12 @@ test('single page app routing', async (context) => {
154
153
 
155
154
  ```javascript
156
155
  import { test } from 'vitest';
157
- import { webApp } from 'testdriverai/presets';
156
+ import { TestDriver } from 'testdriverai/vitest/hooks';
158
157
 
159
158
  test('form validation errors', async (context) => {
160
- const { testdriver } = await webApp(context, {
161
- url: 'https://myapp.com/register'
162
- });
159
+ const testdriver = TestDriver(context, { headless: true });
160
+
161
+ await testdriver.provision.chrome({ url: 'https://myapp.com/register' });
163
162
 
164
163
  // Submit empty form
165
164
  await testdriver.find('Submit button').click();
@@ -183,12 +182,12 @@ test('form validation errors', async (context) => {
183
182
 
184
183
  ```javascript
185
184
  import { test } from 'vitest';
186
- import { webApp } from 'testdriverai/presets';
185
+ import { TestDriver } from 'testdriverai/vitest/hooks';
187
186
 
188
187
  test('data fetching and display', async (context) => {
189
- const { testdriver } = await webApp(context, {
190
- url: 'https://api-demo.example.com'
191
- });
188
+ const testdriver = TestDriver(context, { headless: true });
189
+
190
+ await testdriver.provision.chrome({ url: 'https://api-demo.example.com' });
192
191
 
193
192
  // Trigger data fetch
194
193
  await testdriver.find('Load Data button').click();
@@ -208,13 +207,12 @@ test('data fetching and display', async (context) => {
208
207
 
209
208
  ```javascript
210
209
  import { test } from 'vitest';
211
- import { webApp } from 'testdriverai/presets';
210
+ import { TestDriver } from 'testdriverai/vitest/hooks';
212
211
 
213
212
  test('mobile menu', async (context) => {
214
- const { testdriver } = await webApp(context, {
215
- url: 'https://responsive.example.com',
216
- maximized: false // Start in default window size
217
- });
213
+ const testdriver = TestDriver(context, { headless: true });
214
+
215
+ await testdriver.provision.chrome({ url: 'https://responsive.example.com' });
218
216
 
219
217
  // Resize to mobile
220
218
  await testdriver.exec('sh', 'xdotool getactivewindow windowsize 375 667', 5000);
@@ -231,13 +229,12 @@ test('mobile menu', async (context) => {
231
229
 
232
230
  ## Browser Support
233
231
 
234
- Currently, `webApp()` only supports Chrome:
232
+ Currently supports Chrome:
235
233
 
236
234
  ```javascript
237
235
  // ✅ Supported
238
- const { testdriver } = await webApp(context, {
239
- browser: 'chrome'
240
- });
236
+ const testdriver = TestDriver(context, { headless: true });
237
+ await testdriver.provision.chrome({ url: 'https://myapp.com' });
241
238
 
242
239
  // ❌ Not yet supported
243
240
  const { testdriver } = await webApp(context, {
@@ -310,9 +307,9 @@ The behavior is identical, but `webApp()` provides a more generic interface for
310
307
 
311
308
  ```javascript
312
309
  test('login and navigate', async (context) => {
313
- const { testdriver } = await webApp(context, {
314
- url: 'https://myapp.com'
315
- });
310
+ const testdriver = TestDriver(context, { headless: true });
311
+
312
+ await testdriver.provision.chrome({ url: 'https://myapp.com' });
316
313
 
317
314
  // Login
318
315
  await testdriver.find('email').type('user@example.com');
@@ -332,9 +329,9 @@ test('login and navigate', async (context) => {
332
329
 
333
330
  ```javascript
334
331
  test('upload file', async (context) => {
335
- const { testdriver } = await webApp(context, {
336
- url: 'https://upload.example.com'
337
- });
332
+ const testdriver = TestDriver(context, { headless: true });
333
+
334
+ await testdriver.provision.chrome({ url: 'https://upload.example.com' });
338
335
 
339
336
  // Click upload button
340
337
  await testdriver.find('Upload file button').click();
@@ -352,9 +349,9 @@ test('upload file', async (context) => {
352
349
 
353
350
  ```javascript
354
351
  test('search with filters', async (context) => {
355
- const { testdriver } = await webApp(context, {
356
- url: 'https://catalog.example.com'
357
- });
352
+ const testdriver = TestDriver(context, { headless: true });
353
+
354
+ await testdriver.provision.chrome({ url: 'https://catalog.example.com' });
358
355
 
359
356
  // Enter search query
360
357
  await testdriver.find('search input').type('laptop');
@@ -143,7 +143,10 @@ class InitCommand extends BaseCommand {
143
143
  },
144
144
  keywords: ["testdriver", "testing", "e2e"],
145
145
  author: "",
146
- license: "ISC"
146
+ license: "ISC",
147
+ engines: {
148
+ node: ">=20.19.0"
149
+ }
147
150
  };
148
151
 
149
152
  fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
@@ -159,6 +162,7 @@ class InitCommand extends BaseCommand {
159
162
  async createVitestExample() {
160
163
  const testDir = path.join(process.cwd(), "tests");
161
164
  const testFile = path.join(testDir, "example.test.js");
165
+ const loginSnippetFile = path.join(testDir, "login.js");
162
166
  const configFile = path.join(process.cwd(), "vitest.config.js");
163
167
 
164
168
  // Create test directory if it doesn't exist
@@ -167,23 +171,73 @@ class InitCommand extends BaseCommand {
167
171
  console.log(chalk.gray(` Created directory: ${testDir}`));
168
172
  }
169
173
 
170
- // Create example Vitest test
174
+ // Create login snippet file
175
+ const loginSnippetContent = `/**
176
+ * Login snippet - reusable login function
177
+ *
178
+ * This demonstrates how to create reusable test snippets that can be
179
+ * imported and used across multiple test files.
180
+ */
181
+ export async function login(testdriver) {
182
+
183
+ // The password is displayed on screen, have TestDriver extract it
184
+ const password = await testdriver.extract('the password');
185
+
186
+ // Find the username field
187
+ const usernameField = await testdriver.find(
188
+ 'Username, label above the username input field on the login form'
189
+ );
190
+ await usernameField.click();
191
+
192
+ // Type username
193
+ await testdriver.type('standard_user');
194
+
195
+ // Enter password form earlier
196
+ // Marked as secret so it's not logged or stored
197
+ await testdriver.pressKeys(['tab']);
198
+ await testdriver.type(password, { secret: true });
199
+
200
+ // Submit the form
201
+ await testdriver.find('submit button on the login form').click();
202
+ }
203
+ `;
204
+
205
+ fs.writeFileSync(loginSnippetFile, loginSnippetContent);
206
+ console.log(chalk.green(` Created login snippet: ${loginSnippetFile}`));
207
+
208
+ // Create example Vitest test that uses the login snippet
171
209
  const vitestContent = `import { test, expect } from 'vitest';
172
- import { chrome } from 'testdriverai/presets';
210
+ import { TestDriver } from 'testdriverai/vitest/hooks';
211
+ import { login } from './login.js';
212
+
213
+ test('should login and add item to cart', async (context) => {
214
+
215
+ // Create TestDriver instance - automatically connects to sandbox
216
+ const testdriver = TestDriver(context);
217
+
218
+ // Launch chrome and navigate to demo app
219
+ await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
220
+
221
+ // Use the login snippet to handle authentication
222
+ // This demonstrates how to reuse test logic across multiple tests
223
+ await login(testdriver);
173
224
 
174
- test('should navigate to example.com and find elements', async (context) => {
175
- // The chrome preset handles connection, browser launch, and cleanup automatically
176
- const { testdriver } = await chrome(context, {
177
- url: 'https://example.com'
178
- // apiKey automatically read from process.env.TD_API_KEY via .env file
179
- });
225
+ // Add item to cart
226
+ const addToCartButton = await testdriver.find(
227
+ 'add to cart button under TestDriver Hat'
228
+ );
229
+ await addToCartButton.click();
180
230
 
181
- // Find and verify elements
182
- const heading = await testdriver.find('heading that says Example Domain');
183
- expect(heading.found()).toBe(true);
231
+ // Open cart
232
+ const cartButton = await testdriver.find(
233
+ 'cart button in the top right corner'
234
+ );
235
+ await cartButton.click();
184
236
 
185
- const link = await testdriver.find('More information link');
186
- expect(link.found()).toBe(true);
237
+ // Verify item in cart
238
+ const result = await testdriver.assert('TestDriver Hat is in the cart');
239
+ expect(result).toBeTruthy();
240
+
187
241
  });
188
242
  `;
189
243
 
@@ -194,16 +248,20 @@ test('should navigate to example.com and find elements', async (context) => {
194
248
  if (!fs.existsSync(configFile)) {
195
249
  const configContent = `import { defineConfig } from 'vitest/config';
196
250
  import TestDriver from 'testdriverai/vitest';
197
- import dotenv from 'dotenv';
251
+ import { config } from 'dotenv';
198
252
 
199
253
  // Load environment variables from .env file
200
- dotenv.config();
254
+ config();
201
255
 
202
256
  export default defineConfig({
203
- plugins: [TestDriver()],
204
257
  test: {
205
- testTimeout: 120000,
206
- hookTimeout: 120000,
258
+ testTimeout: 300000,
259
+ hookTimeout: 300000,
260
+ reporters: [
261
+ 'default',
262
+ TestDriver(),
263
+ ],
264
+ setupFiles: ['testdriverai/vitest/setup'],
207
265
  },
208
266
  });
209
267
  `;
@@ -315,7 +373,7 @@ jobs:
315
373
  console.log(chalk.cyan("\n Installing dependencies...\n"));
316
374
 
317
375
  try {
318
- execSync("npm install -D vitest testdriverai && npm install dotenv", {
376
+ execSync("npm install -D vitest testdriverai@beta && npm install dotenv", {
319
377
  cwd: process.cwd(),
320
378
  stdio: "inherit"
321
379
  });
@@ -326,7 +384,7 @@ jobs:
326
384
  "\n⚠️ Failed to install dependencies automatically. Please run:",
327
385
  ),
328
386
  );
329
- console.log(chalk.gray(" npm install -D vitest testdriverai"));
387
+ console.log(chalk.gray(" npm install -D vitest testdriverai@beta"));
330
388
  console.log(chalk.gray(" npm install dotenv\n"));
331
389
  }
332
390
  }
@@ -423,7 +423,7 @@ export default function testDriverPlugin(options = {}) {
423
423
  options.apiRoot || process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
424
424
  pluginState.ciProvider = detectCI();
425
425
  pluginState.gitInfo = getGitInfo();
426
-
426
+
427
427
  // Store TestDriver-specific options (excluding plugin-specific ones)
428
428
  const { apiKey, apiRoot, ...testDriverOptions } = options;
429
429
  pluginState.testDriverOptions = testDriverOptions;
@@ -441,7 +441,13 @@ export default function testDriverPlugin(options = {}) {
441
441
  logger.debug("Global TestDriver options:", testDriverOptions);
442
442
  }
443
443
 
444
- return new TestDriverReporter(options);
444
+ // Create reporter instance
445
+ const reporter = new TestDriverReporter(options);
446
+
447
+ // Add name property for Vitest
448
+ reporter.name = 'testdriver';
449
+
450
+ return reporter;
445
451
  }
446
452
 
447
453
  /**
@@ -458,6 +464,10 @@ class TestDriverReporter {
458
464
  this.ctx = ctx;
459
465
  logger.debug("onInit called - UPDATED VERSION");
460
466
 
467
+ // Store project root for making file paths relative
468
+ pluginState.projectRoot = ctx.config.root || process.cwd();
469
+ logger.debug("Project root:", pluginState.projectRoot);
470
+
461
471
  // NOW read the API key and API root (after setupFiles have run, including dotenv/config)
462
472
  pluginState.apiKey = this.options.apiKey || process.env.TD_API_KEY;
463
473
  pluginState.apiRoot = this.options.apiRoot || process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
@@ -668,11 +678,15 @@ class TestDriverReporter {
668
678
  dashcamUrl = testResult.dashcamUrl || null;
669
679
  const platform = testResult.platform || null;
670
680
  sessionId = testResult.sessionId || null;
671
- testFile =
681
+ const absolutePath =
672
682
  testResult.testFile ||
673
683
  test.file?.filepath ||
674
684
  test.file?.name ||
675
685
  "unknown";
686
+ // Make path relative to project root
687
+ testFile = pluginState.projectRoot && absolutePath !== "unknown"
688
+ ? path.relative(pluginState.projectRoot, absolutePath)
689
+ : absolutePath;
676
690
  testOrder =
677
691
  testResult.testOrder !== undefined ? testResult.testOrder : 0;
678
692
  // Don't override duration from file - use Vitest's result.duration
@@ -696,7 +710,7 @@ class TestDriverReporter {
696
710
  logger.debug(`No result file found for test: ${test.id}`);
697
711
  // Fallback to test object properties - try multiple sources
698
712
  // In Vitest, the file path is on test.module.task.filepath
699
- testFile =
713
+ const absolutePath =
700
714
  test.module?.task?.filepath ||
701
715
  test.module?.file?.filepath ||
702
716
  test.module?.file?.name ||
@@ -706,13 +720,17 @@ class TestDriverReporter {
706
720
  test.suite?.file?.name ||
707
721
  test.location?.file ||
708
722
  "unknown";
723
+ // Make path relative to project root
724
+ testFile = pluginState.projectRoot && absolutePath !== "unknown"
725
+ ? path.relative(pluginState.projectRoot, absolutePath)
726
+ : absolutePath;
709
727
  logger.debug(`Resolved testFile: ${testFile}`);
710
728
  }
711
729
  } catch (error) {
712
730
  logger.error("Failed to read test result file:", error.message);
713
731
  // Fallback to test object properties - try multiple sources
714
732
  // In Vitest, the file path is on test.module.task.filepath
715
- testFile =
733
+ const absolutePath =
716
734
  test.module?.task?.filepath ||
717
735
  test.module?.file?.filepath ||
718
736
  test.module?.file?.name ||
@@ -722,6 +740,10 @@ class TestDriverReporter {
722
740
  test.suite?.file?.name ||
723
741
  test.location?.file ||
724
742
  "unknown";
743
+ // Make path relative to project root
744
+ testFile = pluginState.projectRoot && absolutePath !== "unknown"
745
+ ? path.relative(pluginState.projectRoot, absolutePath)
746
+ : absolutePath;
725
747
  logger.debug(`Resolved testFile from fallback: ${testFile}`);
726
748
  }
727
749
 
@@ -901,8 +923,9 @@ function getGitInfo() {
901
923
  encoding: "utf8",
902
924
  stdio: ["pipe", "pipe", "ignore"]
903
925
  }).trim();
926
+ logger.debug("Git commit from local:", info.commit);
904
927
  } catch (e) {
905
- // Git command failed, ignore
928
+ logger.debug("Failed to get git commit:", e.message);
906
929
  }
907
930
  }
908
931
 
@@ -912,8 +935,9 @@ function getGitInfo() {
912
935
  encoding: "utf8",
913
936
  stdio: ["pipe", "pipe", "ignore"]
914
937
  }).trim();
938
+ logger.debug("Git branch from local:", info.branch);
915
939
  } catch (e) {
916
- // Git command failed, ignore
940
+ logger.debug("Failed to get git branch:", e.message);
917
941
  }
918
942
  }
919
943
 
@@ -923,8 +947,9 @@ function getGitInfo() {
923
947
  encoding: "utf8",
924
948
  stdio: ["pipe", "pipe", "ignore"]
925
949
  }).trim();
950
+ logger.debug("Git author from local:", info.author);
926
951
  } catch (e) {
927
- // Git command failed, ignore
952
+ logger.debug("Failed to get git author:", e.message);
928
953
  }
929
954
  }
930
955
 
@@ -941,12 +966,14 @@ function getGitInfo() {
941
966
  const match = remoteUrl.match(/[:/]([^/:]+\/[^/:]+?)(\.git)?$/);
942
967
  if (match) {
943
968
  info.repo = match[1];
969
+ logger.debug("Git repo from local:", info.repo);
944
970
  }
945
971
  } catch (e) {
946
- // Git command failed, ignore
972
+ logger.debug("Failed to get git repo:", e.message);
947
973
  }
948
974
  }
949
975
 
976
+ logger.info("Collected git info:", info);
950
977
  return info;
951
978
  }
952
979
 
@@ -246,7 +246,11 @@ export function TestDriver(context, options = {}) {
246
246
  if (dashcamUrl) {
247
247
  const testId = context.task.id;
248
248
  const platform = testdriver.os || 'linux';
249
- const testFile = context.task.file?.filepath || context.task.file?.name || 'unknown';
249
+ const absolutePath = context.task.file?.filepath || context.task.file?.name || 'unknown';
250
+ const projectRoot = process.cwd();
251
+ const testFile = absolutePath !== 'unknown'
252
+ ? path.relative(projectRoot, absolutePath)
253
+ : absolutePath;
250
254
 
251
255
  // Create results directory if it doesn't exist
252
256
  const resultsDir = path.join(os.tmpdir(), 'testdriver-results');