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.
- package/.github/workflows/publish.yaml +15 -7
- package/.github/workflows/testdriver.yml +36 -0
- package/agent/index.js +28 -109
- package/bin/testdriverai.js +8 -0
- package/debugger/index.html +37 -0
- package/docs/docs.json +2 -11
- package/docs/v7/_drafts/architecture.mdx +1 -26
- package/docs/v7/_drafts/provision.mdx +251 -188
- package/docs/v7/_drafts/quick-start-test-recording.mdx +0 -1
- package/docs/v7/_drafts/test-recording.mdx +0 -6
- package/docs/v7/api/act.mdx +1 -0
- package/docs/v7/getting-started/quickstart.mdx +9 -16
- package/interfaces/cli/commands/init.js +33 -19
- package/interfaces/cli/lib/base.js +24 -0
- package/interfaces/cli.js +8 -1
- package/interfaces/logger.js +8 -3
- package/interfaces/vitest-plugin.mjs +16 -71
- package/lib/sentry.js +343 -0
- package/lib/vitest/hooks.mjs +23 -31
- package/package.json +4 -3
- package/sdk-log-formatter.js +41 -0
- package/sdk.js +335 -94
- package/test/testdriver/act.test.mjs +30 -0
- package/test/testdriver/assert.test.mjs +1 -1
- package/test/testdriver/hover-text.test.mjs +1 -1
- package/test/testdriver/installer.test.mjs +47 -0
- package/test/testdriver/launch-vscode-linux.test.mjs +55 -0
- package/test/testdriver/setup/testHelpers.mjs +8 -118
- package/tests/example.test.js +33 -0
- package/tests/login.js +28 -0
- package/vitest.config.js +18 -0
- package/vitest.config.mjs +1 -0
- package/agent/lib/cache.js +0 -142
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
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
|
+
}
|
package/vitest.config.js
ADDED
|
@@ -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
package/agent/lib/cache.js
DELETED
|
@@ -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
|
-
};
|