testdriverai 7.2.3 → 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/v7/_drafts/architecture.mdx +1 -26
- 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/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 +12 -24
- package/package.json +4 -3
- package/sdk-log-formatter.js +41 -0
- package/sdk.js +167 -56
- 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/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 +2 -1
- package/agent/lib/cache.js +0 -142
package/sdk.js
CHANGED
|
@@ -263,6 +263,48 @@ class ElementNotFoundError extends Error {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Custom error class for act() failures
|
|
268
|
+
* Includes task execution details and retry information
|
|
269
|
+
*/
|
|
270
|
+
class ActError extends Error {
|
|
271
|
+
/**
|
|
272
|
+
* @param {string} message - Error message
|
|
273
|
+
* @param {Object} details - Additional details about the failure
|
|
274
|
+
* @param {string} details.task - The task that was attempted
|
|
275
|
+
* @param {number} details.tries - Number of check attempts made
|
|
276
|
+
* @param {number} details.maxTries - Maximum tries that were allowed
|
|
277
|
+
* @param {number} details.duration - Total execution time in milliseconds
|
|
278
|
+
* @param {Error} [details.cause] - The underlying error that caused the failure
|
|
279
|
+
*/
|
|
280
|
+
constructor(message, details = {}) {
|
|
281
|
+
super(message);
|
|
282
|
+
this.name = "ActError";
|
|
283
|
+
this.task = details.task;
|
|
284
|
+
this.tries = details.tries;
|
|
285
|
+
this.maxTries = details.maxTries;
|
|
286
|
+
this.duration = details.duration;
|
|
287
|
+
this.cause = details.cause;
|
|
288
|
+
this.timestamp = new Date().toISOString();
|
|
289
|
+
|
|
290
|
+
// Capture stack trace
|
|
291
|
+
if (Error.captureStackTrace) {
|
|
292
|
+
Error.captureStackTrace(this, ActError);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Enhance error message with execution details
|
|
296
|
+
this.message += `\n\n=== Act Execution Details ===`;
|
|
297
|
+
this.message += `\nTask: "${this.task}"`;
|
|
298
|
+
this.message += `\nTries: ${this.tries}/${this.maxTries}`;
|
|
299
|
+
this.message += `\nDuration: ${this.duration}ms`;
|
|
300
|
+
this.message += `\nTimestamp: ${this.timestamp}`;
|
|
301
|
+
|
|
302
|
+
if (this.cause) {
|
|
303
|
+
this.message += `\nUnderlying error: ${this.cause.message}`;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
266
308
|
/**
|
|
267
309
|
* Element class representing a located or to-be-located element
|
|
268
310
|
*/
|
|
@@ -2188,10 +2230,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2188
2230
|
const absoluteTimestamp = Date.now();
|
|
2189
2231
|
const startTime = absoluteTimestamp;
|
|
2190
2232
|
|
|
2191
|
-
// Log finding all action
|
|
2192
2233
|
const { events } = require("./agent/events.js");
|
|
2193
|
-
const findingMessage = formatter.formatElementsFinding(description);
|
|
2194
|
-
this.emitter.emit(events.log.log, findingMessage);
|
|
2195
2234
|
|
|
2196
2235
|
try {
|
|
2197
2236
|
const screenshot = await this.system.captureScreenBase64();
|
|
@@ -2257,16 +2296,16 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2257
2296
|
const duration = Date.now() - startTime;
|
|
2258
2297
|
|
|
2259
2298
|
if (response && response.elements && response.elements.length > 0) {
|
|
2260
|
-
//
|
|
2261
|
-
const
|
|
2299
|
+
// Single log at the end - found elements
|
|
2300
|
+
const formattedMessage = formatter.formatFindAllSingleLine(
|
|
2262
2301
|
description,
|
|
2263
2302
|
response.elements.length,
|
|
2264
2303
|
{
|
|
2265
|
-
duration:
|
|
2304
|
+
duration: duration,
|
|
2266
2305
|
cacheHit: response.cached || false,
|
|
2267
2306
|
},
|
|
2268
2307
|
);
|
|
2269
|
-
this.emitter.emit(events.log.
|
|
2308
|
+
this.emitter.emit(events.log.narration, formattedMessage, true);
|
|
2270
2309
|
|
|
2271
2310
|
// Create Element instances for each found element
|
|
2272
2311
|
const elements = response.elements.map((elementData) => {
|
|
@@ -2316,7 +2355,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2316
2355
|
|
|
2317
2356
|
// Log debug information when elements are found
|
|
2318
2357
|
if (process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG) {
|
|
2319
|
-
const { events } = require("./agent/events.js");
|
|
2320
2358
|
this.emitter.emit(
|
|
2321
2359
|
events.log.debug,
|
|
2322
2360
|
`✓ Found ${elements.length} element(s): "${description}"`,
|
|
@@ -2330,6 +2368,19 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2330
2368
|
|
|
2331
2369
|
return elements;
|
|
2332
2370
|
} else {
|
|
2371
|
+
const duration = Date.now() - startTime;
|
|
2372
|
+
|
|
2373
|
+
// Single log at the end - no elements found
|
|
2374
|
+
const formattedMessage = formatter.formatFindAllSingleLine(
|
|
2375
|
+
description,
|
|
2376
|
+
0,
|
|
2377
|
+
{
|
|
2378
|
+
duration: duration,
|
|
2379
|
+
cacheHit: response?.cached || false,
|
|
2380
|
+
},
|
|
2381
|
+
);
|
|
2382
|
+
this.emitter.emit(events.log.narration, formattedMessage, true);
|
|
2383
|
+
|
|
2333
2384
|
// No elements found - track interaction (fire-and-forget, don't block)
|
|
2334
2385
|
const sessionId = this.getSessionId();
|
|
2335
2386
|
if (sessionId && this.sandbox?.send) {
|
|
@@ -2354,6 +2405,18 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2354
2405
|
return [];
|
|
2355
2406
|
}
|
|
2356
2407
|
} catch (error) {
|
|
2408
|
+
const duration = Date.now() - startTime;
|
|
2409
|
+
|
|
2410
|
+
// Single log at the end - error
|
|
2411
|
+
const formattedMessage = formatter.formatFindAllSingleLine(
|
|
2412
|
+
description,
|
|
2413
|
+
0,
|
|
2414
|
+
{
|
|
2415
|
+
duration: duration,
|
|
2416
|
+
},
|
|
2417
|
+
);
|
|
2418
|
+
this.emitter.emit(events.log.narration, formattedMessage, true);
|
|
2419
|
+
|
|
2357
2420
|
// Track findAll error interaction (fire-and-forget, don't block)
|
|
2358
2421
|
const sessionId = this.getSessionId();
|
|
2359
2422
|
if (sessionId && this.sandbox?.send) {
|
|
@@ -2371,8 +2434,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2371
2434
|
});
|
|
2372
2435
|
}
|
|
2373
2436
|
|
|
2374
|
-
const { events } = require("./agent/events.js");
|
|
2375
|
-
this.emitter.emit(events.log.log, `Error in findAll: ${error.message}`);
|
|
2376
2437
|
return [];
|
|
2377
2438
|
}
|
|
2378
2439
|
}
|
|
@@ -2771,6 +2832,11 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2771
2832
|
// handles forwarding to sandbox. This prevents duplicate output to server.
|
|
2772
2833
|
this.emitter.on("log:**", (message) => {
|
|
2773
2834
|
const event = this.emitter.event;
|
|
2835
|
+
|
|
2836
|
+
if (event.includes("markdown")) {
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2774
2840
|
if (event === events.log.debug && !debugMode) return;
|
|
2775
2841
|
if (this.loggingEnabled && message) {
|
|
2776
2842
|
const prefixedMessage = this.testContext
|
|
@@ -2920,39 +2986,11 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2920
2986
|
const platform = options.platform || this.config.TD_PLATFORM || "linux";
|
|
2921
2987
|
|
|
2922
2988
|
// Auto-detect sandbox ID from the active sandbox if not provided
|
|
2923
|
-
|
|
2989
|
+
// For E2B (Linux), the instance has sandboxId; for AWS (Windows), it has instanceId
|
|
2990
|
+
const sandboxId = options.sandboxId || this.instance?.sandboxId || this.instance?.instanceId || this.agent?.sandboxId || null;
|
|
2924
2991
|
|
|
2925
2992
|
// Get or create session ID using the agent's newSession method
|
|
2926
2993
|
let sessionId = this.agent?.sessionInstance?.get() || null;
|
|
2927
|
-
|
|
2928
|
-
// If no session exists, create one using the agent's method
|
|
2929
|
-
if (!sessionId && this.agent?.newSession) {
|
|
2930
|
-
try {
|
|
2931
|
-
await this.agent.newSession();
|
|
2932
|
-
sessionId = this.agent.sessionInstance.get();
|
|
2933
|
-
|
|
2934
|
-
// Save session ID to file for reuse across test runs
|
|
2935
|
-
if (sessionId) {
|
|
2936
|
-
const sessionFile = path.join(os.homedir(), '.testdriverai-session');
|
|
2937
|
-
fs.writeFileSync(sessionFile, sessionId, { encoding: 'utf-8' });
|
|
2938
|
-
}
|
|
2939
|
-
} catch (error) {
|
|
2940
|
-
// Log but don't fail - tests can run without a session
|
|
2941
|
-
console.warn('Failed to create session:', error.message);
|
|
2942
|
-
}
|
|
2943
|
-
}
|
|
2944
|
-
|
|
2945
|
-
// If still no session, try reading from file (for reporter/separate processes)
|
|
2946
|
-
if (!sessionId) {
|
|
2947
|
-
try {
|
|
2948
|
-
const sessionFile = path.join(os.homedir(), '.testdriverai-session');
|
|
2949
|
-
if (fs.existsSync(sessionFile)) {
|
|
2950
|
-
sessionId = fs.readFileSync(sessionFile, 'utf-8').trim();
|
|
2951
|
-
}
|
|
2952
|
-
} catch (error) {
|
|
2953
|
-
// Ignore file read errors
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
2994
|
|
|
2957
2995
|
const data = {
|
|
2958
2996
|
runId: options.runId,
|
|
@@ -3076,26 +3114,98 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
3076
3114
|
* This is the SDK equivalent of the CLI's exploratory loop
|
|
3077
3115
|
*
|
|
3078
3116
|
* @param {string} task - Natural language description of what to do
|
|
3079
|
-
* @param {Object} options - Execution options
|
|
3080
|
-
* @param {
|
|
3081
|
-
* @returns {Promise<
|
|
3117
|
+
* @param {Object} [options] - Execution options
|
|
3118
|
+
* @param {number} [options.tries=7] - Maximum number of check/retry attempts before giving up
|
|
3119
|
+
* @returns {Promise<ActResult>} Result object with success status and details
|
|
3120
|
+
* @throws {ActError} When the task fails after all tries are exhausted
|
|
3121
|
+
*
|
|
3122
|
+
* @typedef {Object} ActResult
|
|
3123
|
+
* @property {boolean} success - Whether the task completed successfully
|
|
3124
|
+
* @property {string} task - The original task that was executed
|
|
3125
|
+
* @property {number} tries - Number of check attempts made
|
|
3126
|
+
* @property {number} maxTries - Maximum tries that were allowed
|
|
3127
|
+
* @property {number} duration - Total execution time in milliseconds
|
|
3128
|
+
* @property {string} [response] - AI's final response if available
|
|
3082
3129
|
*
|
|
3083
3130
|
* @example
|
|
3084
3131
|
* // Simple execution
|
|
3085
|
-
* await client.act('Click the submit button');
|
|
3132
|
+
* const result = await client.act('Click the submit button');
|
|
3133
|
+
* console.log(result.success); // true
|
|
3134
|
+
*
|
|
3135
|
+
* @example
|
|
3136
|
+
* // With custom retry limit
|
|
3137
|
+
* const result = await client.act('Fill out the contact form', { tries: 10 });
|
|
3138
|
+
* console.log(`Completed in ${result.tries} tries`);
|
|
3086
3139
|
*
|
|
3087
3140
|
* @example
|
|
3088
|
-
* //
|
|
3089
|
-
*
|
|
3090
|
-
*
|
|
3141
|
+
* // Handle failures
|
|
3142
|
+
* try {
|
|
3143
|
+
* await client.act('Complete the checkout process', { tries: 3 });
|
|
3144
|
+
* } catch (error) {
|
|
3145
|
+
* console.log(`Failed after ${error.tries} tries: ${error.message}`);
|
|
3146
|
+
* }
|
|
3091
3147
|
*/
|
|
3092
|
-
async act(task) {
|
|
3148
|
+
async act(task, options = {}) {
|
|
3093
3149
|
this._ensureConnected();
|
|
3094
3150
|
|
|
3095
|
-
|
|
3151
|
+
const { tries = 7 } = options;
|
|
3152
|
+
|
|
3153
|
+
this.analytics.track("sdk.act", { task, tries });
|
|
3154
|
+
|
|
3155
|
+
const { events } = require("./agent/events.js");
|
|
3156
|
+
const startTime = Date.now();
|
|
3096
3157
|
|
|
3097
|
-
//
|
|
3098
|
-
|
|
3158
|
+
// Store original checkLimit and set custom one if provided
|
|
3159
|
+
const originalCheckLimit = this.agent.checkLimit;
|
|
3160
|
+
this.agent.checkLimit = tries;
|
|
3161
|
+
|
|
3162
|
+
// Reset check count for this act() call
|
|
3163
|
+
const originalCheckCount = this.agent.checkCount;
|
|
3164
|
+
this.agent.checkCount = 0;
|
|
3165
|
+
|
|
3166
|
+
// Emit scoped start marker for act()
|
|
3167
|
+
this.emitter.emit(events.log.log, formatter.formatActStart(task));
|
|
3168
|
+
|
|
3169
|
+
try {
|
|
3170
|
+
// Use the agent's exploratoryLoop method directly
|
|
3171
|
+
const response = await this.agent.exploratoryLoop(task, false, true, false);
|
|
3172
|
+
|
|
3173
|
+
const duration = Date.now() - startTime;
|
|
3174
|
+
const triesUsed = this.agent.checkCount;
|
|
3175
|
+
|
|
3176
|
+
this.emitter.emit(events.log.log, formatter.formatActComplete(duration, true));
|
|
3177
|
+
|
|
3178
|
+
// Restore original checkLimit
|
|
3179
|
+
this.agent.checkLimit = originalCheckLimit;
|
|
3180
|
+
this.agent.checkCount = originalCheckCount;
|
|
3181
|
+
|
|
3182
|
+
return {
|
|
3183
|
+
success: true,
|
|
3184
|
+
task,
|
|
3185
|
+
tries: triesUsed,
|
|
3186
|
+
maxTries: tries,
|
|
3187
|
+
duration,
|
|
3188
|
+
response: response || undefined,
|
|
3189
|
+
};
|
|
3190
|
+
} catch (error) {
|
|
3191
|
+
const duration = Date.now() - startTime;
|
|
3192
|
+
const triesUsed = this.agent.checkCount;
|
|
3193
|
+
|
|
3194
|
+
this.emitter.emit(events.log.log, formatter.formatActComplete(duration, false, error.message));
|
|
3195
|
+
|
|
3196
|
+
// Restore original checkLimit
|
|
3197
|
+
this.agent.checkLimit = originalCheckLimit;
|
|
3198
|
+
this.agent.checkCount = originalCheckCount;
|
|
3199
|
+
|
|
3200
|
+
// Create an enhanced error with additional context using ActError class
|
|
3201
|
+
throw new ActError(`Act failed: ${error.message}`, {
|
|
3202
|
+
task,
|
|
3203
|
+
tries: triesUsed,
|
|
3204
|
+
maxTries: tries,
|
|
3205
|
+
duration,
|
|
3206
|
+
cause: error,
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3099
3209
|
}
|
|
3100
3210
|
|
|
3101
3211
|
/**
|
|
@@ -3103,15 +3213,16 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
3103
3213
|
* Execute a natural language task using AI
|
|
3104
3214
|
*
|
|
3105
3215
|
* @param {string} task - Natural language description of what to do
|
|
3106
|
-
* @param {Object} options - Execution options
|
|
3107
|
-
* @param {
|
|
3108
|
-
* @returns {Promise<
|
|
3216
|
+
* @param {Object} [options] - Execution options
|
|
3217
|
+
* @param {number} [options.tries=7] - Maximum number of check/retry attempts
|
|
3218
|
+
* @returns {Promise<ActResult>} Result object with success status and details
|
|
3109
3219
|
*/
|
|
3110
|
-
async ai(task) {
|
|
3111
|
-
return await this.act(task);
|
|
3220
|
+
async ai(task, options) {
|
|
3221
|
+
return await this.act(task, options);
|
|
3112
3222
|
}
|
|
3113
3223
|
}
|
|
3114
3224
|
|
|
3115
3225
|
module.exports = TestDriverSDK;
|
|
3116
3226
|
module.exports.Element = Element;
|
|
3117
3227
|
module.exports.ElementNotFoundError = ElementNotFoundError;
|
|
3228
|
+
module.exports.ActError = ActError;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestDriver SDK - Act Test (Vitest)
|
|
3
|
+
* Tests the AI exploratory loop (act) functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { TestDriver } from "../../lib/vitest/hooks.mjs";
|
|
8
|
+
|
|
9
|
+
describe("Act Test", () => {
|
|
10
|
+
it("should use act to search for testdriver on Google", async (context) => {
|
|
11
|
+
const testdriver = TestDriver(context, { newSandbox: true });
|
|
12
|
+
|
|
13
|
+
// provision.chrome() automatically calls ready() and starts dashcam
|
|
14
|
+
await testdriver.provision.chrome({
|
|
15
|
+
url: 'https://www.google.com',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Use act to search for testdriver
|
|
19
|
+
let actRes = await testdriver.act("click on the empty search box, type 'testdriver', and hit enter. do not click the plus button in the search bar");
|
|
20
|
+
|
|
21
|
+
console.log("Act response:", actRes);
|
|
22
|
+
|
|
23
|
+
// Assert the search results are displayed
|
|
24
|
+
const result = await testdriver.assert(
|
|
25
|
+
"search results for testdriver are visible",
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(result).toBeTruthy();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -8,7 +8,7 @@ import { TestDriver } from "../../lib/vitest/hooks.mjs";
|
|
|
8
8
|
|
|
9
9
|
describe("Assert Test", () => {
|
|
10
10
|
it("should assert the testdriver login page shows", async (context) => {
|
|
11
|
-
const testdriver = TestDriver(context, { newSandbox: true });
|
|
11
|
+
const testdriver = TestDriver(context, { newSandbox: true, headless: false });
|
|
12
12
|
|
|
13
13
|
// provision.chrome() automatically calls ready() and starts dashcam
|
|
14
14
|
await testdriver.provision.chrome({
|
|
@@ -8,7 +8,7 @@ import { TestDriver } from "../../lib/vitest/hooks.mjs";
|
|
|
8
8
|
|
|
9
9
|
describe("Hover Text Test", () => {
|
|
10
10
|
it("should click Sign In and verify error message", async (context) => {
|
|
11
|
-
const testdriver = TestDriver(context, { headless:
|
|
11
|
+
const testdriver = TestDriver(context, { headless: false, newSandbox: true, cacheKey: 'hover-text-test' });
|
|
12
12
|
await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
|
|
13
13
|
|
|
14
14
|
// Click on Sign In button using new find() API
|
|
@@ -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