testdriverai 7.2.2 → 7.2.9

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.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * TestDriver SDK - Launch VS Code on Linux Test (Vitest)
3
+ * Tests launching Visual Studio Code on Debian/Ubuntu using provision.vscode()
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import { TestDriver } from "../../lib/vitest/hooks.mjs";
8
+
9
+ describe("Launch VS Code on Linux", () => {
10
+ it(
11
+ "should launch VS Code on Debian/Ubuntu",
12
+ async (context) => {
13
+ const testdriver = TestDriver(context, { newSandbox: true });
14
+
15
+ // provision.vscode() automatically calls ready() and starts dashcam
16
+ await testdriver.provision.vscode();
17
+
18
+ // Assert that VS Code is running
19
+ const result = await testdriver.assert(
20
+ "Visual Studio Code window is visible on screen",
21
+ );
22
+ expect(result).toBeTruthy();
23
+ },
24
+ );
25
+
26
+ it(
27
+ "should install and use a VS Code extension",
28
+ async (context) => {
29
+ const testdriver = TestDriver(context, { newSandbox: true });
30
+
31
+ // Launch VS Code with the Prettier extension installed
32
+ await testdriver.provision.vscode({
33
+ extensions: ["esbenp.prettier-vscode"],
34
+ });
35
+
36
+ // Assert that VS Code is running
37
+ const vsCodeVisible = await testdriver.assert(
38
+ "Visual Studio Code window is visible on screen",
39
+ );
40
+ expect(vsCodeVisible).toBeTruthy();
41
+
42
+ // Open the extensions panel to verify Prettier is installed
43
+ await testdriver.pressKeys(["ctrl", "shift", "x"]);
44
+
45
+ // Wait for extensions panel to open
46
+ await new Promise((resolve) => setTimeout(resolve, 2000));
47
+
48
+ // Assert that Prettier extension is visible in the installed extensions
49
+ const prettierVisible = await testdriver.assert(
50
+ "Prettier extension is visible in the extensions panel or sidebar",
51
+ );
52
+ expect(prettierVisible).toBeTruthy();
53
+ },
54
+ );
55
+ });
@@ -6,7 +6,6 @@
6
6
  import crypto from "crypto";
7
7
  import { config } from "dotenv";
8
8
  import fs from "fs";
9
- import os from "os";
10
9
  import path, { dirname } from "path";
11
10
  import { fileURLToPath } from "url";
12
11
  import TestDriver from "../../../sdk.js";
@@ -51,85 +50,6 @@ console.log(
51
50
  process.env.TD_OS || "Not set (will default to linux)",
52
51
  );
53
52
 
54
- // Global test results storage
55
- const testResults = {
56
- tests: [],
57
- startTime: Date.now(),
58
- };
59
-
60
- /**
61
- * Store test result with dashcam URL
62
- * @param {string} testName - Name of the test
63
- * @param {string} testFile - Test file path
64
- * @param {string|null} dashcamUrl - Dashcam URL if available
65
- * @param {Object} sessionInfo - Session information
66
- */
67
- export function storeTestResult(
68
- testName,
69
- testFile,
70
- dashcamUrl,
71
- sessionInfo = {},
72
- ) {
73
-
74
- // Extract replay object ID from dashcam URL
75
- let replayObjectId = null;
76
- if (dashcamUrl) {
77
- const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
78
- replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
79
- if (replayObjectId) {
80
- console.log(` Replay Object ID: ${replayObjectId}`);
81
- }
82
- }
83
-
84
- testResults.tests.push({
85
- name: testName,
86
- file: testFile,
87
- dashcamUrl,
88
- replayObjectId,
89
- sessionId: sessionInfo.sessionId,
90
- timestamp: new Date().toISOString(),
91
- });
92
- }
93
-
94
- /**
95
- * Get all test results
96
- * @returns {Object} All collected test results
97
- */
98
- export function getTestResults() {
99
- return {
100
- ...testResults,
101
- endTime: Date.now(),
102
- duration: Date.now() - testResults.startTime,
103
- };
104
- }
105
-
106
- /**
107
- * Save test results to a JSON file
108
- * @param {string} outputPath - Path to save the results
109
- */
110
- export function saveTestResults(outputPath = "test-results/sdk-summary.json") {
111
- const results = getTestResults();
112
- const dir = path.dirname(outputPath);
113
-
114
- // Create directory if it doesn't exist
115
- if (!fs.existsSync(dir)) {
116
- fs.mkdirSync(dir, { recursive: true });
117
- }
118
-
119
- fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
120
- console.log(`\n📊 Test results saved to: ${outputPath}`);
121
-
122
- // Also print dashcam URLs to console
123
- console.log("\n🎥 Dashcam URLs:");
124
- results.tests.forEach((test) => {
125
- if (test.dashcamUrl) {
126
- console.log(` ${test.name}: ${test.dashcamUrl}`);
127
- }
128
- });
129
-
130
- return results;
131
- }
132
-
133
53
  /**
134
54
  * Intercept console logs and forward to TestDriver sandbox
135
55
  * @param {TestDriver} client - TestDriver client instance
@@ -494,21 +414,9 @@ export async function teardownTest(client, options = {}) {
494
414
  console.log("⏭️ Postrun skipped (disabled in options)");
495
415
  }
496
416
 
497
- // Write test result to a file for the reporter to pick up (cross-process communication)
417
+ // Use Vitest's task.meta for cross-process communication with the reporter
498
418
  if (options.task) {
499
- const testResultFile = path.join(
500
- os.tmpdir(),
501
- "testdriver-results",
502
- `${options.task.id}.json`,
503
- );
504
-
505
419
  try {
506
- // Ensure directory exists
507
- const dir = path.dirname(testResultFile);
508
- if (!fs.existsSync(dir)) {
509
- fs.mkdirSync(dir, { recursive: true });
510
- }
511
-
512
420
  // Get test file path - make it relative to project root
513
421
  const absolutePath =
514
422
  options.task.file?.filepath || options.task.file?.name || "unknown";
@@ -523,33 +431,15 @@ export async function teardownTest(client, options = {}) {
523
431
  testOrder = options.task.suite.tasks.indexOf(options.task);
524
432
  }
525
433
 
526
- // Note: Duration is calculated by Vitest and passed via result.duration
527
- // We include it in the test result file so the reporter can use it
528
-
529
- // Get duration from Vitest result
530
- const result = options.task.result?.();
531
- const duration = result?.duration || 0;
532
-
533
- // Write test result with dashcam URL, platform, and metadata
534
- const testResult = {
535
- testId: options.task.id,
536
- testName: options.task.name,
537
- testFile: testFile,
538
- testOrder: testOrder,
539
- dashcamUrl: dashcamUrl,
540
- replayObjectId: dashcamUrl
541
- ? dashcamUrl.match(/\/replay\/([^?]+)/)?.[1]
542
- : null,
543
- platform: client.os, // Include platform from SDK client (source of truth)
544
- timestamp: Date.now(),
545
- duration: duration, // Include duration from Vitest
546
- };
547
-
548
- fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
549
-
434
+ // Set metadata on task for the reporter to pick up
435
+ options.task.meta.dashcamUrl = dashcamUrl;
436
+ options.task.meta.platform = client.os; // Include platform from SDK client (source of truth)
437
+ options.task.meta.testFile = testFile;
438
+ options.task.meta.testOrder = testOrder;
439
+ options.task.meta.sessionId = client.getSessionId?.() || null;
550
440
  } catch (error) {
551
441
  console.error(
552
- `[TestHelpers] ❌ Failed to write test result file:`,
442
+ `[TestHelpers] ❌ Failed to set test metadata:`,
553
443
  error.message,
554
444
  );
555
445
  }
@@ -0,0 +1,33 @@
1
+ import { test, expect } from 'vitest';
2
+ import { TestDriver } from 'testdriverai/vitest/hooks';
3
+ import { login } from './login.js';
4
+
5
+ test('should login and add item to cart', async (context) => {
6
+
7
+ // Create TestDriver instance - automatically connects to sandbox
8
+ const testdriver = TestDriver(context);
9
+
10
+ // Launch chrome and navigate to demo app
11
+ await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
12
+
13
+ // Use the login snippet to handle authentication
14
+ // This demonstrates how to reuse test logic across multiple tests
15
+ await login(testdriver);
16
+
17
+ // Add item to cart
18
+ const addToCartButton = await testdriver.find(
19
+ 'add to cart button under TestDriver Hat'
20
+ );
21
+ await addToCartButton.click();
22
+
23
+ // Open cart
24
+ const cartButton = await testdriver.find(
25
+ 'cart button in the top right corner'
26
+ );
27
+ await cartButton.click();
28
+
29
+ // Verify item in cart
30
+ const result = await testdriver.assert('TestDriver Hat is in the cart');
31
+ expect(result).toBeTruthy();
32
+
33
+ });
package/tests/login.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Login snippet - reusable login function
3
+ *
4
+ * This demonstrates how to create reusable test snippets that can be
5
+ * imported and used across multiple test files.
6
+ */
7
+ export async function login(testdriver) {
8
+
9
+ // The password is displayed on screen, have TestDriver extract it
10
+ const password = await testdriver.extract('the password');
11
+
12
+ // Find the username field
13
+ const usernameField = await testdriver.find(
14
+ 'Username, label above the username input field on the login form'
15
+ );
16
+ await usernameField.click();
17
+
18
+ // Type username
19
+ await testdriver.type('standard_user');
20
+
21
+ // Enter password form earlier
22
+ // Marked as secret so it's not logged or stored
23
+ await testdriver.pressKeys(['tab']);
24
+ await testdriver.type(password, { secret: true });
25
+
26
+ // Submit the form
27
+ await testdriver.find('submit button on the login form').click();
28
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import TestDriver from 'testdriverai/vitest';
3
+ import { config } from 'dotenv';
4
+
5
+ // Load environment variables from .env file
6
+ config();
7
+
8
+ export default defineConfig({
9
+ test: {
10
+ testTimeout: 300000,
11
+ hookTimeout: 300000,
12
+ reporters: [
13
+ 'default',
14
+ TestDriver(),
15
+ ],
16
+ setupFiles: ['testdriverai/vitest/setup'],
17
+ },
18
+ });
package/vitest.config.mjs CHANGED
@@ -10,6 +10,7 @@ export default defineConfig({
10
10
  test: {
11
11
  testTimeout: 900000,
12
12
  hookTimeout: 900000,
13
+ disableConsoleIntercept: true,
13
14
  reporters: [
14
15
  'default',
15
16
  TestDriver(),
@@ -1,142 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
- const crypto = require("crypto");
4
-
5
- /**
6
- * Generate a cache key from a prompt
7
- * Uses a hash to create a safe filename
8
- */
9
- function getCacheKey(prompt) {
10
- // Normalize the prompt by trimming and converting to lowercase
11
- const normalized = prompt.trim().toLowerCase();
12
-
13
- // Create a hash for the filename
14
- const hash = crypto.createHash("md5").update(normalized).digest("hex");
15
-
16
- // Also create a sanitized version of the prompt for readability
17
- const sanitized = normalized
18
- .replace(/[^a-z0-9\s]/g, "") // Remove special chars
19
- .replace(/\s+/g, "-") // Replace spaces with hyphens
20
- .substring(0, 50); // Limit length
21
-
22
- // Combine sanitized prompt with hash for uniqueness
23
- return `${sanitized}-${hash}.yaml`;
24
- }
25
-
26
- /**
27
- * Get the cache directory path
28
- * Creates it if it doesn't exist
29
- */
30
- function getCacheDir() {
31
- const cacheDir = path.join(process.cwd(), ".testdriver", ".cache");
32
-
33
- if (!fs.existsSync(cacheDir)) {
34
- fs.mkdirSync(cacheDir, { recursive: true });
35
- }
36
-
37
- return cacheDir;
38
- }
39
-
40
- /**
41
- * Get the full path to a cache file
42
- */
43
- function getCachePath(prompt) {
44
- const cacheDir = getCacheDir();
45
- const key = getCacheKey(prompt);
46
- return path.join(cacheDir, key);
47
- }
48
-
49
- /**
50
- * Check if a cached response exists for a prompt
51
- */
52
- function hasCache(prompt) {
53
- const cachePath = getCachePath(prompt);
54
- return fs.existsSync(cachePath);
55
- }
56
-
57
- /**
58
- * Read cached YAML for a prompt
59
- * Returns null if no cache exists
60
- */
61
- function readCache(prompt) {
62
- if (!hasCache(prompt)) {
63
- return null;
64
- }
65
-
66
- try {
67
- const cachePath = getCachePath(prompt);
68
- const yaml = fs.readFileSync(cachePath, "utf8");
69
- return yaml;
70
- } catch {
71
- // If there's an error reading the cache, return null
72
- return null;
73
- }
74
- }
75
-
76
- /**
77
- * Write YAML to cache for a prompt
78
- */
79
- function writeCache(prompt, yaml) {
80
- try {
81
- const cachePath = getCachePath(prompt);
82
- fs.writeFileSync(cachePath, yaml, "utf8");
83
- return cachePath;
84
- } catch {
85
- // Silently fail if we can't write to cache
86
- return null;
87
- }
88
- }
89
-
90
- /**
91
- * Clear all cached prompts
92
- */
93
- function clearCache() {
94
- const cacheDir = getCacheDir();
95
-
96
- try {
97
- const files = fs.readdirSync(cacheDir);
98
-
99
- for (const file of files) {
100
- if (file.endsWith(".yaml")) {
101
- fs.unlinkSync(path.join(cacheDir, file));
102
- }
103
- }
104
-
105
- return true;
106
- } catch {
107
- return false;
108
- }
109
- }
110
-
111
- /**
112
- * Get cache statistics
113
- */
114
- function getCacheStats() {
115
- const cacheDir = getCacheDir();
116
-
117
- try {
118
- const files = fs.readdirSync(cacheDir);
119
- const yamlFiles = files.filter((f) => f.endsWith(".yaml"));
120
-
121
- return {
122
- count: yamlFiles.length,
123
- files: yamlFiles,
124
- };
125
- } catch {
126
- return {
127
- count: 0,
128
- files: [],
129
- };
130
- }
131
- }
132
-
133
- module.exports = {
134
- getCacheKey,
135
- getCacheDir,
136
- getCachePath,
137
- hasCache,
138
- readCache,
139
- writeCache,
140
- clearCache,
141
- getCacheStats,
142
- };