testdriverai 7.0.0 → 7.1.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.
- package/AGENTS.md +550 -0
- package/CODEOWNERS +0 -1
- package/README.md +126 -0
- package/agent/index.js +43 -18
- package/agent/lib/commands.js +794 -135
- package/agent/lib/redraw.js +124 -39
- package/agent/lib/sandbox.js +10 -1
- package/agent/lib/sdk.js +21 -0
- package/docs/MIGRATION.md +425 -0
- package/docs/PRESETS.md +210 -0
- package/docs/docs.json +91 -37
- package/docs/guide/best-practices-polling.mdx +154 -0
- package/docs/v7/api/dashcam.mdx +497 -0
- package/docs/v7/api/doubleClick.mdx +102 -0
- package/docs/v7/api/mouseDown.mdx +161 -0
- package/docs/v7/api/mouseUp.mdx +164 -0
- package/docs/v7/api/rightClick.mdx +123 -0
- package/docs/v7/getting-started/configuration.mdx +380 -0
- package/docs/v7/getting-started/quickstart.mdx +273 -140
- package/docs/v7/guides/best-practices.mdx +486 -0
- package/docs/v7/guides/caching-ai.mdx +215 -0
- package/docs/v7/guides/caching-selectors.mdx +292 -0
- package/docs/v7/guides/caching.mdx +366 -0
- package/docs/v7/guides/ci-cd/azure.mdx +587 -0
- package/docs/v7/guides/ci-cd/circleci.mdx +523 -0
- package/docs/v7/guides/ci-cd/github-actions.mdx +457 -0
- package/docs/v7/guides/ci-cd/gitlab.mdx +498 -0
- package/docs/v7/guides/ci-cd/jenkins.mdx +664 -0
- package/docs/v7/guides/ci-cd/travis.mdx +438 -0
- package/docs/v7/guides/debugging.mdx +349 -0
- package/docs/v7/guides/faq.mdx +393 -0
- package/docs/v7/guides/performance.mdx +517 -0
- package/docs/v7/guides/troubleshooting.mdx +526 -0
- package/docs/v7/guides/vitest-plugin.mdx +477 -0
- package/docs/v7/guides/vitest.mdx +535 -0
- package/docs/v7/platforms/linux.mdx +308 -0
- package/docs/v7/platforms/macos.mdx +433 -0
- package/docs/v7/platforms/windows.mdx +430 -0
- package/docs/v7/presets/chrome-extension.mdx +223 -0
- package/docs/v7/presets/chrome.mdx +287 -0
- package/docs/v7/presets/electron.mdx +435 -0
- package/docs/v7/presets/vscode.mdx +398 -0
- package/docs/v7/presets/webapp.mdx +396 -0
- package/docs/v7/progressive-apis/CORE.md +459 -0
- package/docs/v7/progressive-apis/HOOKS.md +360 -0
- package/docs/v7/progressive-apis/PROGRESSIVE_DISCLOSURE.md +230 -0
- package/docs/v7/progressive-apis/PROVISION.md +266 -0
- package/interfaces/vitest-plugin.mjs +186 -100
- package/package.json +12 -1
- package/sdk.d.ts +335 -42
- package/sdk.js +756 -95
- package/src/core/Dashcam.js +469 -0
- package/src/core/index.d.ts +150 -0
- package/src/core/index.js +12 -0
- package/src/presets/index.mjs +331 -0
- package/src/vitest/extended.mjs +108 -0
- package/src/vitest/hooks.d.ts +119 -0
- package/src/vitest/hooks.mjs +298 -0
- package/src/vitest/index.mjs +64 -0
- package/src/vitest/lifecycle.mjs +277 -0
- package/src/vitest/utils.mjs +150 -0
- package/test/dashcam.test.js +137 -0
- package/testdriver/acceptance-sdk/assert.test.mjs +13 -31
- package/testdriver/acceptance-sdk/auto-cache-key-demo.test.mjs +56 -0
- package/testdriver/acceptance-sdk/chrome-extension.test.mjs +89 -0
- package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +7 -19
- package/testdriver/acceptance-sdk/element-not-found.test.mjs +6 -19
- package/testdriver/acceptance-sdk/exec-js.test.mjs +6 -18
- package/testdriver/acceptance-sdk/exec-output.test.mjs +8 -20
- package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +13 -25
- package/testdriver/acceptance-sdk/focus-window.test.mjs +8 -20
- package/testdriver/acceptance-sdk/formatted-logging.test.mjs +5 -20
- package/testdriver/acceptance-sdk/hooks-example.test.mjs +38 -0
- package/testdriver/acceptance-sdk/hover-image.test.mjs +10 -19
- package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +7 -19
- package/testdriver/acceptance-sdk/hover-text.test.mjs +5 -19
- package/testdriver/acceptance-sdk/match-image.test.mjs +7 -19
- package/testdriver/acceptance-sdk/presets-example.test.mjs +87 -0
- package/testdriver/acceptance-sdk/press-keys.test.mjs +5 -19
- package/testdriver/acceptance-sdk/prompt.test.mjs +6 -18
- package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +6 -20
- package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +6 -18
- package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +9 -23
- package/testdriver/acceptance-sdk/scroll.test.mjs +12 -21
- package/testdriver/acceptance-sdk/setup/testHelpers.mjs +124 -352
- package/testdriver/acceptance-sdk/sully-ai.test.mjs +234 -0
- package/testdriver/acceptance-sdk/test-console-logs.test.mjs +42 -0
- package/testdriver/acceptance-sdk/type.test.mjs +19 -58
- package/vitest.config.mjs +1 -0
- package/.vscode/mcp.json +0 -9
- package/MIGRATION.md +0 -389
- package/PLUGIN_MIGRATION.md +0 -222
- package/PROMPT_CACHE.md +0 -200
- package/SDK_LOGGING.md +0 -222
- package/SDK_MIGRATION.md +0 -474
- package/SDK_README.md +0 -1122
- package/debug-screenshot-1763401388589.png +0 -0
- package/examples/run-tests-with-recording.sh +0 -70
- package/examples/screenshot-example.js +0 -63
- package/examples/sdk-awesome-logs-demo.js +0 -177
- package/examples/sdk-cache-thresholds.js +0 -96
- package/examples/sdk-element-properties.js +0 -155
- package/examples/sdk-simple-example.js +0 -65
- package/examples/test-recording-example.test.js +0 -166
- package/mcp-server/AI_GUIDELINES.md +0 -57
- package/test-find-api.js +0 -73
- package/test-prompt-cache.js +0 -96
- package/test-sandbox-render.js +0 -28
- package/test-sdk-methods.js +0 -15
- package/test-sdk-refactor.js +0 -53
- package/test-stack-trace.mjs +0 -57
- package/testdriver/acceptance-sdk/setup/lifecycleHelpers.mjs +0 -239
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Test Helpers
|
|
3
|
-
*
|
|
2
|
+
* Repo-Specific Test Helpers
|
|
3
|
+
*
|
|
4
|
+
* These helpers are specific to this repository's test infrastructure.
|
|
5
|
+
* For reusable plugin helpers, import from 'testdriverai/vitest' (or src/vitest/index.mjs locally)
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import crypto from "crypto";
|
|
@@ -9,36 +11,62 @@ import fs from "fs";
|
|
|
9
11
|
import os from "os";
|
|
10
12
|
import path, { dirname } from "path";
|
|
11
13
|
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
// Import TestDriver SDK locally (for repo development)
|
|
12
16
|
import TestDriver from "../../../sdk.js";
|
|
17
|
+
|
|
18
|
+
// Import plugin helpers
|
|
13
19
|
import {
|
|
14
20
|
addDashcamLog,
|
|
15
21
|
authDashcam,
|
|
16
22
|
launchChrome,
|
|
23
|
+
launchChromeExtension,
|
|
24
|
+
launchChromeForTesting,
|
|
17
25
|
runPostrun,
|
|
18
26
|
runPrerun,
|
|
27
|
+
runPrerunChromeExtension,
|
|
28
|
+
runPrerunChromeForTesting,
|
|
19
29
|
startDashcam,
|
|
20
30
|
stopDashcam,
|
|
21
31
|
waitForPage,
|
|
22
|
-
} from "
|
|
32
|
+
} from "../../../src/vitest/lifecycle.mjs";
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
import {
|
|
35
|
+
retryAsync,
|
|
36
|
+
setupEventLogging,
|
|
37
|
+
sleep,
|
|
38
|
+
waitFor,
|
|
39
|
+
} from "../../../src/vitest/utils.mjs";
|
|
40
|
+
|
|
41
|
+
// Re-export plugin lifecycle helpers for backward compatibility
|
|
25
42
|
export {
|
|
26
43
|
addDashcamLog,
|
|
27
44
|
authDashcam,
|
|
28
45
|
launchChrome,
|
|
46
|
+
launchChromeExtension,
|
|
47
|
+
launchChromeForTesting,
|
|
29
48
|
runPostrun,
|
|
30
49
|
runPrerun,
|
|
50
|
+
runPrerunChromeExtension,
|
|
51
|
+
runPrerunChromeForTesting,
|
|
31
52
|
startDashcam,
|
|
32
53
|
stopDashcam,
|
|
33
54
|
waitForPage
|
|
34
55
|
};
|
|
35
56
|
|
|
57
|
+
// Re-export plugin utilities for backward compatibility
|
|
58
|
+
export {
|
|
59
|
+
retryAsync,
|
|
60
|
+
setupEventLogging,
|
|
61
|
+
sleep,
|
|
62
|
+
waitFor
|
|
63
|
+
};
|
|
64
|
+
|
|
36
65
|
// Get the directory of the current module
|
|
37
66
|
const __filename = fileURLToPath(import.meta.url);
|
|
38
67
|
const __dirname = dirname(__filename);
|
|
39
68
|
|
|
40
69
|
// Load environment variables from .env file in the project root
|
|
41
|
-
// Go up 3 levels from setup/ to reach the project root
|
|
42
70
|
const envPath = path.resolve(__dirname, "../../../.env");
|
|
43
71
|
config({ path: envPath });
|
|
44
72
|
|
|
@@ -46,12 +74,12 @@ config({ path: envPath });
|
|
|
46
74
|
console.log("🔧 Environment variables loaded from:", envPath);
|
|
47
75
|
console.log(" TD_API_KEY:", process.env.TD_API_KEY ? "✓ Set" : "✗ Not set");
|
|
48
76
|
console.log(" TD_API_ROOT:", process.env.TD_API_ROOT || "Not set");
|
|
49
|
-
console.log(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
77
|
+
console.log(" TD_OS:", process.env.TD_OS || "Not set (will default to linux)");
|
|
78
|
+
|
|
79
|
+
// =============================================================================
|
|
80
|
+
// TEST RESULTS STORAGE (Repo-specific CI/CD integration)
|
|
81
|
+
// =============================================================================
|
|
53
82
|
|
|
54
|
-
// Global test results storage
|
|
55
83
|
const testResults = {
|
|
56
84
|
tests: [],
|
|
57
85
|
startTime: Date.now(),
|
|
@@ -64,16 +92,10 @@ const testResults = {
|
|
|
64
92
|
* @param {string|null} dashcamUrl - Dashcam URL if available
|
|
65
93
|
* @param {Object} sessionInfo - Session information
|
|
66
94
|
*/
|
|
67
|
-
export function storeTestResult(
|
|
68
|
-
testName,
|
|
69
|
-
testFile,
|
|
70
|
-
dashcamUrl,
|
|
71
|
-
sessionInfo = {},
|
|
72
|
-
) {
|
|
95
|
+
export function storeTestResult(testName, testFile, dashcamUrl, sessionInfo = {}) {
|
|
73
96
|
console.log(`📝 Storing test result: ${testName}`);
|
|
74
97
|
console.log(` Dashcam URL: ${dashcamUrl || "none"}`);
|
|
75
98
|
|
|
76
|
-
// Extract replay object ID from dashcam URL
|
|
77
99
|
let replayObjectId = null;
|
|
78
100
|
if (dashcamUrl) {
|
|
79
101
|
const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
|
|
@@ -113,7 +135,6 @@ export function saveTestResults(outputPath = "test-results/sdk-summary.json") {
|
|
|
113
135
|
const results = getTestResults();
|
|
114
136
|
const dir = path.dirname(outputPath);
|
|
115
137
|
|
|
116
|
-
// Create directory if it doesn't exist
|
|
117
138
|
if (!fs.existsSync(dir)) {
|
|
118
139
|
fs.mkdirSync(dir, { recursive: true });
|
|
119
140
|
}
|
|
@@ -121,7 +142,6 @@ export function saveTestResults(outputPath = "test-results/sdk-summary.json") {
|
|
|
121
142
|
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
|
122
143
|
console.log(`\n📊 Test results saved to: ${outputPath}`);
|
|
123
144
|
|
|
124
|
-
// Also print dashcam URLs to console
|
|
125
145
|
console.log("\n🎥 Dashcam URLs:");
|
|
126
146
|
results.tests.forEach((test) => {
|
|
127
147
|
if (test.dashcamUrl) {
|
|
@@ -132,161 +152,48 @@ export function saveTestResults(outputPath = "test-results/sdk-summary.json") {
|
|
|
132
152
|
return results;
|
|
133
153
|
}
|
|
134
154
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
* @param {string} taskId - Unique task identifier for this test
|
|
139
|
-
*/
|
|
140
|
-
function setupConsoleInterceptor(client, taskId) {
|
|
141
|
-
// Store original console methods
|
|
142
|
-
const originalConsole = {
|
|
143
|
-
log: console.log,
|
|
144
|
-
error: console.error,
|
|
145
|
-
warn: console.warn,
|
|
146
|
-
info: console.info,
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
// Create wrapper that forwards to sandbox
|
|
150
|
-
const createInterceptor = (level, originalMethod) => {
|
|
151
|
-
return function (...args) {
|
|
152
|
-
// Call original console method first
|
|
153
|
-
originalMethod.apply(console, args);
|
|
154
|
-
|
|
155
|
-
// Forward to sandbox if connected
|
|
156
|
-
if (client.sandbox && client.sandbox.instanceSocketConnected) {
|
|
157
|
-
try {
|
|
158
|
-
// Format the log message
|
|
159
|
-
const message = args
|
|
160
|
-
.map((arg) =>
|
|
161
|
-
typeof arg === "object"
|
|
162
|
-
? JSON.stringify(arg, null, 2)
|
|
163
|
-
: String(arg),
|
|
164
|
-
)
|
|
165
|
-
.join(" ");
|
|
166
|
-
|
|
167
|
-
// Preserve ANSI color codes and emojis for rich sandbox output
|
|
168
|
-
// (don't add level prefix - sdk-log-formatter handles styling)
|
|
169
|
-
const logOutput = message;
|
|
170
|
-
|
|
171
|
-
client.sandbox.send({
|
|
172
|
-
type: "output",
|
|
173
|
-
output: Buffer.from(logOutput, "utf8").toString("base64"),
|
|
174
|
-
});
|
|
175
|
-
} catch (error) {
|
|
176
|
-
// Silently fail to avoid breaking the test
|
|
177
|
-
// Use original console to avoid infinite loop
|
|
178
|
-
originalConsole.error(
|
|
179
|
-
`[TestHelpers] Failed to forward log to sandbox:`,
|
|
180
|
-
error.message,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
// Replace console methods with interceptors
|
|
188
|
-
console.log = createInterceptor("log", originalConsole.log);
|
|
189
|
-
console.error = createInterceptor("error", originalConsole.error);
|
|
190
|
-
console.warn = createInterceptor("warn", originalConsole.warn);
|
|
191
|
-
console.info = createInterceptor("info", originalConsole.info);
|
|
192
|
-
|
|
193
|
-
// Store original methods and taskId on client for cleanup
|
|
194
|
-
client._consoleInterceptor = {
|
|
195
|
-
taskId,
|
|
196
|
-
original: originalConsole,
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
// Use original console for this message
|
|
200
|
-
originalConsole.log(
|
|
201
|
-
`[TestHelpers] Console interceptor enabled for task: ${taskId}`,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Remove console interceptor and restore original console methods
|
|
207
|
-
* @param {TestDriver} client - TestDriver client instance
|
|
208
|
-
*/
|
|
209
|
-
function removeConsoleInterceptor(client) {
|
|
210
|
-
if (client._consoleInterceptor) {
|
|
211
|
-
const { original, taskId } = client._consoleInterceptor;
|
|
212
|
-
|
|
213
|
-
// Restore original console methods
|
|
214
|
-
console.log = original.log;
|
|
215
|
-
console.error = original.error;
|
|
216
|
-
console.warn = original.warn;
|
|
217
|
-
console.info = original.info;
|
|
218
|
-
|
|
219
|
-
// Use original console for cleanup message
|
|
220
|
-
original.log(
|
|
221
|
-
`[TestHelpers] Console interceptor removed for task: ${taskId}`,
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
// Clean up reference
|
|
225
|
-
delete client._consoleInterceptor;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// REPO-SPECIFIC CLIENT CREATION
|
|
157
|
+
// =============================================================================
|
|
228
158
|
|
|
229
159
|
/**
|
|
230
|
-
* Create a configured TestDriver client
|
|
160
|
+
* Create a configured TestDriver client for this repo's tests
|
|
161
|
+
* Uses local SDK path for development
|
|
231
162
|
* @param {Object} options - Additional options
|
|
232
|
-
* @param {Object} options.task - Vitest task context (from beforeEach/it context)
|
|
233
163
|
* @returns {TestDriver} Configured client
|
|
234
164
|
*/
|
|
235
165
|
export function createTestClient(options = {}) {
|
|
236
|
-
// Check if API key is set
|
|
237
166
|
if (!process.env.TD_API_KEY) {
|
|
238
167
|
console.error("\n❌ Error: TD_API_KEY is not set!");
|
|
239
168
|
console.error("Please set it in one of the following ways:");
|
|
240
|
-
console.error(
|
|
241
|
-
|
|
242
|
-
);
|
|
243
|
-
console.error(
|
|
244
|
-
" 2. Pass it as an environment variable: TD_API_KEY=your_key npm run test:sdk",
|
|
245
|
-
);
|
|
169
|
+
console.error(" 1. Create a .env file in the project root with: TD_API_KEY=your_key");
|
|
170
|
+
console.error(" 2. Pass it as an environment variable: TD_API_KEY=your_key npm run test:sdk");
|
|
246
171
|
console.error(" 3. Export it in your shell: export TD_API_KEY=your_key\n");
|
|
247
172
|
throw new Error("TD_API_KEY environment variable is required");
|
|
248
173
|
}
|
|
249
174
|
|
|
250
|
-
|
|
251
|
-
const os = process.env.TEST_PLATFORM || "linux";
|
|
252
|
-
|
|
253
|
-
// Extract task context if provided - we use taskId but remove task from clientOptions
|
|
254
|
-
let taskId = options.task?.id || options.task?.name || null;
|
|
255
|
-
|
|
256
|
-
// Remove task from options before passing to TestDriver (eslint wants us to use 'task')
|
|
257
|
-
// eslint-disable-next-line no-unused-vars
|
|
175
|
+
const osConfig = process.env.TEST_PLATFORM || "linux";
|
|
258
176
|
const { task, ...clientOptions } = options;
|
|
177
|
+
const taskId = task?.id || task?.name || null;
|
|
259
178
|
|
|
260
179
|
const client = new TestDriver(process.env.TD_API_KEY, {
|
|
261
180
|
resolution: "1366x768",
|
|
262
181
|
analytics: true,
|
|
263
|
-
os:
|
|
182
|
+
os: osConfig,
|
|
264
183
|
apiKey: process.env.TD_API_KEY,
|
|
265
184
|
apiRoot: process.env.TD_API_ROOT || "https://testdriver-api.onrender.com",
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// ip: '18.217.194.23'
|
|
269
|
-
// ...clientOptions,
|
|
270
|
-
// cache: false,
|
|
185
|
+
newSandbox: true,
|
|
186
|
+
...clientOptions,
|
|
271
187
|
});
|
|
272
188
|
|
|
273
|
-
console.log(
|
|
274
|
-
"🔧 createTestClient: SDK created, cacheThresholds =",
|
|
275
|
-
client.cacheThresholds,
|
|
276
|
-
);
|
|
277
|
-
|
|
189
|
+
console.log("🔧 createTestClient: SDK created, cacheThresholds =", client.cacheThresholds);
|
|
278
190
|
console.log(`[TestHelpers] Client OS configured as: ${client.os}`);
|
|
279
191
|
|
|
280
|
-
// Set Vitest task ID if available (for log filtering in parallel tests)
|
|
281
192
|
if (taskId) {
|
|
282
193
|
console.log(`[TestHelpers] Storing task ID on client: ${taskId}`);
|
|
283
|
-
// Store task ID directly on client for later use in teardown
|
|
284
194
|
client.vitestTaskId = taskId;
|
|
285
|
-
} else {
|
|
286
|
-
console.log(`[TestHelpers] No task ID available`);
|
|
287
195
|
}
|
|
288
196
|
|
|
289
|
-
// Enable detailed event logging if requested
|
|
290
197
|
if (process.env.DEBUG_EVENTS === "true") {
|
|
291
198
|
setupEventLogging(client);
|
|
292
199
|
}
|
|
@@ -294,124 +201,67 @@ export function createTestClient(options = {}) {
|
|
|
294
201
|
return client;
|
|
295
202
|
}
|
|
296
203
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
*/
|
|
301
|
-
export function setupEventLogging(client) {
|
|
302
|
-
const emitter = client.getEmitter();
|
|
303
|
-
|
|
304
|
-
// Log all events
|
|
305
|
-
emitter.on("**", function (data) {
|
|
306
|
-
const event = this.event;
|
|
307
|
-
if (event.startsWith("log:debug")) return; // Skip debug logs
|
|
308
|
-
console.log(`[EVENT] ${event}`, data || "");
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
// Log command lifecycle
|
|
312
|
-
emitter.on("command:start", (data) => {
|
|
313
|
-
console.log("🚀 Command started:", data);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
emitter.on("command:success", (data) => {
|
|
317
|
-
console.log("✅ Command succeeded:", data);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
emitter.on("command:error", (data) => {
|
|
321
|
-
console.error("❌ Command error:", data);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
// Log sandbox events
|
|
325
|
-
emitter.on("sandbox:connected", () => {
|
|
326
|
-
console.log("🔌 Sandbox connected");
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
emitter.on("sandbox:authenticated", () => {
|
|
330
|
-
console.log("🔐 Sandbox authenticated");
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
emitter.on("sandbox:error", (error) => {
|
|
334
|
-
console.error("⚠️ Sandbox error:", error);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
// Log SDK API calls
|
|
338
|
-
emitter.on("sdk:request", (data) => {
|
|
339
|
-
console.log("📤 SDK Request:", data);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
emitter.on("sdk:response", (data) => {
|
|
343
|
-
console.log("📥 SDK Response:", data);
|
|
344
|
-
});
|
|
345
|
-
}
|
|
204
|
+
// =============================================================================
|
|
205
|
+
// REPO-SPECIFIC LOGIN HELPER (for TestDriver Sandbox)
|
|
206
|
+
// =============================================================================
|
|
346
207
|
|
|
347
208
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
209
|
+
* Perform login flow on TestDriver Sandbox
|
|
210
|
+
* This is specific to http://testdriver-sandbox.vercel.app/login
|
|
350
211
|
* @param {TestDriver} client - TestDriver client
|
|
351
|
-
* @param {
|
|
352
|
-
* @
|
|
212
|
+
* @param {string} username - Username (default: 'standard_user')
|
|
213
|
+
* @param {string} password - Password (default: retrieved from screen)
|
|
353
214
|
*/
|
|
354
|
-
export async function
|
|
355
|
-
await client.
|
|
356
|
-
const instance = await client.connect({
|
|
357
|
-
...options,
|
|
358
|
-
});
|
|
215
|
+
export async function performLogin(client, username = "standard_user", password = null) {
|
|
216
|
+
await client.focusApplication("Google Chrome");
|
|
359
217
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
setupConsoleInterceptor(client, client.vitestTaskId);
|
|
218
|
+
if (!password) {
|
|
219
|
+
password = await client.remember("the password");
|
|
363
220
|
}
|
|
364
221
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
222
|
+
const usernameField = await client.find(
|
|
223
|
+
"Username, label above the username input field on the login form",
|
|
224
|
+
);
|
|
225
|
+
await usernameField.click();
|
|
226
|
+
await client.type(username);
|
|
369
227
|
|
|
370
|
-
|
|
228
|
+
await client.pressKeys(["tab"]);
|
|
229
|
+
await client.type(password, { secret: true });
|
|
230
|
+
|
|
231
|
+
await client.pressKeys(["tab"]);
|
|
232
|
+
await client.pressKeys(["enter"]);
|
|
371
233
|
}
|
|
372
234
|
|
|
235
|
+
// =============================================================================
|
|
236
|
+
// SUITE TEST RUN MANAGEMENT (CI/CD Integration)
|
|
237
|
+
// =============================================================================
|
|
238
|
+
|
|
373
239
|
/**
|
|
374
240
|
* Initialize a test run for the entire suite
|
|
375
|
-
* Should be called once in beforeEach
|
|
376
241
|
* @param {Object} suiteTask - Vitest suite task context
|
|
377
242
|
* @returns {Promise<Object>} Test run info { runId, testRunDbId, token }
|
|
378
243
|
*/
|
|
379
244
|
export async function initializeSuiteTestRun(suiteTask) {
|
|
380
245
|
const apiKey = process.env.TD_API_KEY;
|
|
381
|
-
const apiRoot =
|
|
382
|
-
process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
|
|
246
|
+
const apiRoot = process.env.TD_API_ROOT || "https://testdriver-api.onrender.com";
|
|
383
247
|
|
|
384
248
|
if (!apiKey || !globalThis.__testdriverPlugin) {
|
|
385
|
-
console.log(
|
|
386
|
-
`[TestHelpers] Skipping suite test run initialization - no API key or plugin`,
|
|
387
|
-
);
|
|
249
|
+
console.log(`[TestHelpers] Skipping suite test run initialization - no API key or plugin`);
|
|
388
250
|
return null;
|
|
389
251
|
}
|
|
390
252
|
|
|
391
|
-
|
|
392
|
-
const existingRun = globalThis.__testdriverPlugin.getSuiteTestRun(
|
|
393
|
-
suiteTask.id,
|
|
394
|
-
);
|
|
253
|
+
const existingRun = globalThis.__testdriverPlugin.getSuiteTestRun(suiteTask.id);
|
|
395
254
|
if (existingRun) {
|
|
396
|
-
console.log(
|
|
397
|
-
`[TestHelpers] Test run already exists for suite: ${existingRun.runId}`,
|
|
398
|
-
);
|
|
255
|
+
console.log(`[TestHelpers] Test run already exists for suite: ${existingRun.runId}`);
|
|
399
256
|
return existingRun;
|
|
400
257
|
}
|
|
401
258
|
|
|
402
259
|
try {
|
|
403
|
-
console.log(
|
|
404
|
-
`[TestHelpers] Initializing test run for suite: ${suiteTask.name}`,
|
|
405
|
-
);
|
|
260
|
+
console.log(`[TestHelpers] Initializing test run for suite: ${suiteTask.name}`);
|
|
406
261
|
|
|
407
|
-
|
|
408
|
-
const token = await globalThis.__testdriverPlugin.authenticateWithApiKey(
|
|
409
|
-
apiKey,
|
|
410
|
-
apiRoot,
|
|
411
|
-
);
|
|
262
|
+
const token = await globalThis.__testdriverPlugin.authenticateWithApiKey(apiKey, apiRoot);
|
|
412
263
|
console.log(`[TestHelpers] ✅ Authenticated for suite`);
|
|
413
264
|
|
|
414
|
-
// Create test run for the suite
|
|
415
265
|
const runId = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
416
266
|
const testFile = suiteTask.file?.name || "unknown";
|
|
417
267
|
const testRunData = {
|
|
@@ -419,46 +269,53 @@ export async function initializeSuiteTestRun(suiteTask) {
|
|
|
419
269
|
suiteName: suiteTask.name || testFile,
|
|
420
270
|
};
|
|
421
271
|
|
|
422
|
-
const testRunResponse =
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
);
|
|
272
|
+
const testRunResponse = await globalThis.__testdriverPlugin.createTestRunDirect(
|
|
273
|
+
token,
|
|
274
|
+
apiRoot,
|
|
275
|
+
testRunData,
|
|
276
|
+
);
|
|
428
277
|
const testRunDbId = testRunResponse.data?.id;
|
|
429
278
|
|
|
430
279
|
const runInfo = { runId, testRunDbId, token };
|
|
431
|
-
|
|
432
|
-
// Store in plugin state
|
|
433
280
|
globalThis.__testdriverPlugin.setSuiteTestRun(suiteTask.id, runInfo);
|
|
434
281
|
|
|
435
|
-
// Set environment variables for the reporter to use
|
|
436
282
|
process.env.TD_TEST_RUN_ID = runId;
|
|
437
283
|
process.env.TD_TEST_RUN_DB_ID = testRunDbId;
|
|
438
284
|
process.env.TD_TEST_RUN_TOKEN = token;
|
|
439
285
|
|
|
440
|
-
console.log(
|
|
441
|
-
`[TestHelpers] ✅ Created test run for suite: ${runId} (DB ID: ${testRunDbId})`,
|
|
442
|
-
);
|
|
443
|
-
|
|
286
|
+
console.log(`[TestHelpers] ✅ Created test run for suite: ${runId} (DB ID: ${testRunDbId})`);
|
|
444
287
|
return runInfo;
|
|
445
288
|
} catch (error) {
|
|
446
|
-
console.error(
|
|
447
|
-
`[TestHelpers] ❌ Failed to initialize suite test run:`,
|
|
448
|
-
error.message,
|
|
449
|
-
);
|
|
289
|
+
console.error(`[TestHelpers] ❌ Failed to initialize suite test run:`, error.message);
|
|
450
290
|
return null;
|
|
451
291
|
}
|
|
452
292
|
}
|
|
453
293
|
|
|
294
|
+
// =============================================================================
|
|
295
|
+
// SETUP/TEARDOWN HELPERS
|
|
296
|
+
// =============================================================================
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Setup function to run before each test
|
|
300
|
+
* @param {TestDriver} client - TestDriver client
|
|
301
|
+
* @param {Object} options - Connection options
|
|
302
|
+
* @returns {Promise<Object>} Sandbox instance
|
|
303
|
+
*/
|
|
304
|
+
export async function setupTest(client, options = {}) {
|
|
305
|
+
await client.auth();
|
|
306
|
+
const instance = await client.connect({ ...options });
|
|
307
|
+
|
|
308
|
+
if (options.prerun !== false) {
|
|
309
|
+
await runPrerun(client);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return instance;
|
|
313
|
+
}
|
|
314
|
+
|
|
454
315
|
/**
|
|
455
316
|
* Teardown function to run after each test
|
|
456
317
|
* @param {TestDriver} client - TestDriver client
|
|
457
318
|
* @param {Object} options - Teardown options
|
|
458
|
-
* @param {Object} options.task - Vitest task context (optional, for storing in task.meta)
|
|
459
|
-
* @param {string} options.dashcamUrl - Dashcam URL if already retrieved
|
|
460
|
-
* @param {boolean} options.postrun - Whether to run postrun lifecycle (default: true)
|
|
461
|
-
* @param {boolean} options.disconnect - Whether to disconnect client (default: true)
|
|
462
319
|
* @returns {Promise<Object>} Session info including dashcam URL
|
|
463
320
|
*/
|
|
464
321
|
export async function teardownTest(client, options = {}) {
|
|
@@ -467,14 +324,10 @@ export async function teardownTest(client, options = {}) {
|
|
|
467
324
|
console.log("🧹 Running teardown...");
|
|
468
325
|
|
|
469
326
|
try {
|
|
470
|
-
// Run postrun lifecycle if enabled and dashcamUrl not already provided
|
|
471
327
|
if (options.postrun !== false && !dashcamUrl) {
|
|
472
328
|
dashcamUrl = await runPostrun(client);
|
|
473
329
|
|
|
474
|
-
|
|
475
|
-
if (dashcamUrl) {
|
|
476
|
-
// Extract replay object ID from URL
|
|
477
|
-
// URL format: https://app.testdriver.ai/replay/{replayObjectId}?share={shareToken}
|
|
330
|
+
if (dashcamUrl && options.task) {
|
|
478
331
|
const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
|
|
479
332
|
const replayObjectId = replayIdMatch ? replayIdMatch[1] : null;
|
|
480
333
|
|
|
@@ -483,20 +336,15 @@ export async function teardownTest(client, options = {}) {
|
|
|
483
336
|
console.log(`📝 Replay Object ID: ${replayObjectId}`);
|
|
484
337
|
}
|
|
485
338
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
options.task.meta.testdriverReplayObjectId = replayObjectId;
|
|
490
|
-
console.log(
|
|
491
|
-
`[TestHelpers] ✅ Stored dashcam URL in task.meta for test: ${options.task.name}`,
|
|
492
|
-
);
|
|
493
|
-
}
|
|
339
|
+
options.task.meta.testdriverDashcamUrl = dashcamUrl;
|
|
340
|
+
options.task.meta.testdriverReplayObjectId = replayObjectId;
|
|
341
|
+
console.log(`[TestHelpers] ✅ Stored dashcam URL in task.meta for test: ${options.task.name}`);
|
|
494
342
|
}
|
|
495
343
|
} else {
|
|
496
344
|
console.log("⏭️ Postrun skipped (disabled in options)");
|
|
497
345
|
}
|
|
498
346
|
|
|
499
|
-
// Write test result to
|
|
347
|
+
// Write test result to temp file for reporter
|
|
500
348
|
if (options.task) {
|
|
501
349
|
const testResultFile = path.join(
|
|
502
350
|
os.tmpdir(),
|
|
@@ -505,58 +353,42 @@ export async function teardownTest(client, options = {}) {
|
|
|
505
353
|
);
|
|
506
354
|
|
|
507
355
|
try {
|
|
508
|
-
// Ensure directory exists
|
|
509
356
|
const dir = path.dirname(testResultFile);
|
|
510
357
|
if (!fs.existsSync(dir)) {
|
|
511
358
|
fs.mkdirSync(dir, { recursive: true });
|
|
512
359
|
}
|
|
513
360
|
|
|
514
|
-
|
|
515
|
-
const testFile =
|
|
516
|
-
options.task.file?.filepath || options.task.file?.name || "unknown";
|
|
517
|
-
|
|
518
|
-
// Calculate test order (index within parent suite)
|
|
361
|
+
const testFile = options.task.file?.filepath || options.task.file?.name || "unknown";
|
|
519
362
|
let testOrder = 0;
|
|
520
363
|
if (options.task.suite && options.task.suite.tasks) {
|
|
521
364
|
testOrder = options.task.suite.tasks.indexOf(options.task);
|
|
522
365
|
}
|
|
523
366
|
|
|
524
|
-
|
|
525
|
-
|
|
367
|
+
const result = options.task.result?.();
|
|
368
|
+
const duration = result?.duration || 0;
|
|
526
369
|
|
|
527
|
-
// Write test result with dashcam URL, platform, and metadata
|
|
528
370
|
const testResult = {
|
|
529
371
|
testId: options.task.id,
|
|
530
372
|
testName: options.task.name,
|
|
531
373
|
testFile: testFile,
|
|
532
374
|
testOrder: testOrder,
|
|
533
375
|
dashcamUrl: dashcamUrl,
|
|
534
|
-
replayObjectId: dashcamUrl
|
|
535
|
-
|
|
536
|
-
: null,
|
|
537
|
-
platform: client.os, // Include platform from SDK client (source of truth)
|
|
376
|
+
replayObjectId: dashcamUrl ? dashcamUrl.match(/\/replay\/([^?]+)/)?.[1] : null,
|
|
377
|
+
platform: client.os,
|
|
538
378
|
timestamp: Date.now(),
|
|
379
|
+
duration: duration,
|
|
539
380
|
};
|
|
540
381
|
|
|
541
382
|
fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
|
|
542
|
-
console.log(
|
|
543
|
-
`[TestHelpers] ✅ Wrote test result to file: ${testResultFile} (testFile: ${testFile}, testOrder: ${testOrder})`,
|
|
544
|
-
);
|
|
383
|
+
console.log(`[TestHelpers] ✅ Wrote test result to file: ${testResultFile}`);
|
|
545
384
|
} catch (error) {
|
|
546
|
-
console.error(
|
|
547
|
-
`[TestHelpers] ❌ Failed to write test result file:`,
|
|
548
|
-
error.message,
|
|
549
|
-
);
|
|
385
|
+
console.error(`[TestHelpers] ❌ Failed to write test result file:`, error.message);
|
|
550
386
|
}
|
|
551
387
|
}
|
|
552
388
|
} catch (error) {
|
|
553
389
|
console.error("❌ Error in postrun:", error);
|
|
554
390
|
console.error("❌ Error stack:", error.stack);
|
|
555
391
|
} finally {
|
|
556
|
-
// Remove console interceptor before disconnecting
|
|
557
|
-
removeConsoleInterceptor(client);
|
|
558
|
-
|
|
559
|
-
// Only disconnect if not explicitly disabled
|
|
560
392
|
if (options.disconnect !== false) {
|
|
561
393
|
console.log("🔌 Disconnecting client...");
|
|
562
394
|
try {
|
|
@@ -564,14 +396,12 @@ export async function teardownTest(client, options = {}) {
|
|
|
564
396
|
console.log("✅ Client disconnected");
|
|
565
397
|
} catch (disconnectError) {
|
|
566
398
|
console.error("❌ Error disconnecting:", disconnectError.message);
|
|
567
|
-
// Don't throw - we're already in cleanup
|
|
568
399
|
}
|
|
569
400
|
} else {
|
|
570
401
|
console.log("⏭️ Disconnect skipped (disabled in options)");
|
|
571
402
|
}
|
|
572
403
|
}
|
|
573
404
|
|
|
574
|
-
// Extract replay object ID from dashcam URL
|
|
575
405
|
let replayObjectId = null;
|
|
576
406
|
if (dashcamUrl) {
|
|
577
407
|
const replayIdMatch = dashcamUrl.match(/\/replay\/([^?]+)/);
|
|
@@ -586,63 +416,5 @@ export async function teardownTest(client, options = {}) {
|
|
|
586
416
|
};
|
|
587
417
|
|
|
588
418
|
console.log("📊 Session info:", JSON.stringify(sessionInfo, null, 2));
|
|
589
|
-
|
|
590
419
|
return sessionInfo;
|
|
591
420
|
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* Perform login flow (reusable snippet)
|
|
595
|
-
* @param {TestDriver} client - TestDriver client
|
|
596
|
-
* @param {string} username - Username (default: 'standard_user')
|
|
597
|
-
* @param {string} password - Password (default: retrieved from screen)
|
|
598
|
-
*/
|
|
599
|
-
export async function performLogin(
|
|
600
|
-
client,
|
|
601
|
-
username = "standard_user",
|
|
602
|
-
password = null,
|
|
603
|
-
) {
|
|
604
|
-
await client.focusApplication("Google Chrome");
|
|
605
|
-
|
|
606
|
-
// Get password from screen if not provided
|
|
607
|
-
if (!password) {
|
|
608
|
-
password = await client.remember("the password");
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const usernameField = await client.find(
|
|
612
|
-
"Username, label above the username input field on the login form",
|
|
613
|
-
);
|
|
614
|
-
await usernameField.click();
|
|
615
|
-
await client.type(username);
|
|
616
|
-
|
|
617
|
-
// Enter password
|
|
618
|
-
await client.pressKeys(["tab"]);
|
|
619
|
-
await client.type(password);
|
|
620
|
-
|
|
621
|
-
// Submit form
|
|
622
|
-
await client.pressKeys(["tab"]);
|
|
623
|
-
await client.pressKeys(["enter"]);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Wait with retry logic
|
|
628
|
-
* @param {Function} fn - Async function to retry
|
|
629
|
-
* @param {number} retries - Number of retries (default: 3)
|
|
630
|
-
* @param {number} delay - Delay between retries in ms (default: 1000)
|
|
631
|
-
* @returns {Promise} Result of successful execution
|
|
632
|
-
*/
|
|
633
|
-
export async function retryAsync(fn, retries = 3, delay = 1000) {
|
|
634
|
-
let lastError;
|
|
635
|
-
|
|
636
|
-
for (let i = 0; i < retries; i++) {
|
|
637
|
-
try {
|
|
638
|
-
return await fn();
|
|
639
|
-
} catch (error) {
|
|
640
|
-
lastError = error;
|
|
641
|
-
if (i < retries - 1) {
|
|
642
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
throw lastError;
|
|
648
|
-
}
|