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
package/sdk.js
CHANGED
|
@@ -3,8 +3,69 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const os = require("os");
|
|
6
|
+
const crypto = require("crypto");
|
|
6
7
|
const { formatter } = require("./sdk-log-formatter");
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Get the file path of the caller (the file that called TestDriver)
|
|
11
|
+
* @returns {string|null} File path or null if not found
|
|
12
|
+
*/
|
|
13
|
+
function getCallerFilePath() {
|
|
14
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
15
|
+
try {
|
|
16
|
+
const err = new Error();
|
|
17
|
+
Error.prepareStackTrace = (_, stack) => stack;
|
|
18
|
+
const stack = err.stack;
|
|
19
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
20
|
+
|
|
21
|
+
// Look for the first file that's not sdk.js, hooks.mjs, or node internals
|
|
22
|
+
for (const callSite of stack) {
|
|
23
|
+
const fileName = callSite.getFileName();
|
|
24
|
+
if (fileName &&
|
|
25
|
+
!fileName.includes('sdk.js') &&
|
|
26
|
+
!fileName.includes('hooks.mjs') &&
|
|
27
|
+
!fileName.includes('hooks.js') &&
|
|
28
|
+
!fileName.includes('node_modules') &&
|
|
29
|
+
!fileName.includes('node:internal') &&
|
|
30
|
+
fileName !== 'evalmachine.<anonymous>') {
|
|
31
|
+
return fileName;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// Silently fail and return null
|
|
36
|
+
} finally {
|
|
37
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a hash of the caller file for use as a cache key
|
|
44
|
+
* @returns {string|null} Hash of the file or null if file not found
|
|
45
|
+
*/
|
|
46
|
+
function getCallerFileHash() {
|
|
47
|
+
const filePath = getCallerFilePath();
|
|
48
|
+
if (!filePath) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Handle file:// URLs by converting to file system path
|
|
54
|
+
let fsPath = filePath;
|
|
55
|
+
if (filePath.startsWith('file://')) {
|
|
56
|
+
fsPath = filePath.replace('file://', '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fileContent = fs.readFileSync(fsPath, 'utf-8');
|
|
60
|
+
const hash = crypto.createHash('sha256').update(fileContent).digest('hex');
|
|
61
|
+
// Return first 16 chars of hash for brevity
|
|
62
|
+
return hash.substring(0, 16);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
// If we can't read the file, return null
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
8
69
|
/**
|
|
9
70
|
* Custom error class for element operation failures
|
|
10
71
|
* Includes debugging information like screenshots and AI responses
|
|
@@ -13,8 +74,8 @@ class ElementNotFoundError extends Error {
|
|
|
13
74
|
constructor(message, debugInfo = {}) {
|
|
14
75
|
super(message);
|
|
15
76
|
this.name = "ElementNotFoundError";
|
|
16
|
-
|
|
17
|
-
this.aiResponse = debugInfo.aiResponse;
|
|
77
|
+
// Sanitize aiResponse to remove base64 images before storing
|
|
78
|
+
this.aiResponse = this._sanitizeAiResponse(debugInfo.aiResponse);
|
|
18
79
|
this.description = debugInfo.description;
|
|
19
80
|
this.timestamp = new Date().toISOString();
|
|
20
81
|
this.screenshotPath = null;
|
|
@@ -24,8 +85,9 @@ class ElementNotFoundError extends Error {
|
|
|
24
85
|
Error.captureStackTrace(this, ElementNotFoundError);
|
|
25
86
|
}
|
|
26
87
|
|
|
27
|
-
// Write screenshot to temp directory
|
|
28
|
-
|
|
88
|
+
// Write screenshot to temp directory immediately (don't store on error object)
|
|
89
|
+
// This prevents vitest from serializing huge base64 strings
|
|
90
|
+
if (debugInfo.screenshot) {
|
|
29
91
|
try {
|
|
30
92
|
const tempDir = path.join(os.tmpdir(), "testdriver-debug");
|
|
31
93
|
if (!fs.existsSync(tempDir)) {
|
|
@@ -36,7 +98,7 @@ class ElementNotFoundError extends Error {
|
|
|
36
98
|
this.screenshotPath = path.join(tempDir, filename);
|
|
37
99
|
|
|
38
100
|
// Remove data:image/png;base64, prefix if present
|
|
39
|
-
const base64Data =
|
|
101
|
+
const base64Data = debugInfo.screenshot.replace(
|
|
40
102
|
/^data:image\/\w+;base64,/,
|
|
41
103
|
"",
|
|
42
104
|
);
|
|
@@ -182,6 +244,25 @@ class ElementNotFoundError extends Error {
|
|
|
182
244
|
this.stack = filteredLines.join("\n");
|
|
183
245
|
}
|
|
184
246
|
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Sanitize AI response by removing large base64 data to prevent serialization issues
|
|
250
|
+
* @private
|
|
251
|
+
* @param {Object} response - AI response
|
|
252
|
+
* @returns {Object} Sanitized response
|
|
253
|
+
*/
|
|
254
|
+
_sanitizeAiResponse(response) {
|
|
255
|
+
if (!response) return null;
|
|
256
|
+
|
|
257
|
+
// Create shallow copy and remove large base64 fields
|
|
258
|
+
const sanitized = { ...response };
|
|
259
|
+
delete sanitized.croppedImage;
|
|
260
|
+
delete sanitized.screenshot;
|
|
261
|
+
delete sanitized.pixelDiffImage;
|
|
262
|
+
// Keep cachedImageUrl as it's just a URL string, not base64 data
|
|
263
|
+
|
|
264
|
+
return sanitized;
|
|
265
|
+
}
|
|
185
266
|
}
|
|
186
267
|
|
|
187
268
|
/**
|
|
@@ -213,16 +294,18 @@ class Element {
|
|
|
213
294
|
/**
|
|
214
295
|
* Find the element on screen
|
|
215
296
|
* @param {string} [newDescription] - Optional new description to search for
|
|
216
|
-
* @param {
|
|
297
|
+
* @param {Object} [options] - Optional options object with cacheThreshold and/or cacheKey
|
|
217
298
|
* @returns {Promise<Element>} This element instance
|
|
218
299
|
*/
|
|
219
|
-
async find(newDescription,
|
|
300
|
+
async find(newDescription, options) {
|
|
220
301
|
const description = newDescription || this.description;
|
|
221
302
|
if (newDescription) {
|
|
222
303
|
this.description = newDescription;
|
|
223
304
|
}
|
|
224
305
|
|
|
225
306
|
const startTime = Date.now();
|
|
307
|
+
let response = null;
|
|
308
|
+
let findError = null;
|
|
226
309
|
|
|
227
310
|
const debugMode =
|
|
228
311
|
process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
|
|
@@ -239,9 +322,38 @@ class Element {
|
|
|
239
322
|
this._screenshot = screenshot;
|
|
240
323
|
}
|
|
241
324
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
325
|
+
// Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold
|
|
326
|
+
let cacheKey = null;
|
|
327
|
+
let cacheThreshold = null;
|
|
328
|
+
|
|
329
|
+
if (typeof options === 'number') {
|
|
330
|
+
// Legacy: options is just a number threshold
|
|
331
|
+
cacheThreshold = options;
|
|
332
|
+
} else if (typeof options === 'object' && options !== null) {
|
|
333
|
+
// New: options is an object with cacheKey and/or cacheThreshold
|
|
334
|
+
cacheKey = options.cacheKey || null;
|
|
335
|
+
cacheThreshold = options.cacheThreshold ?? null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Use default cacheKey from SDK constructor if not provided in find() options
|
|
339
|
+
if (!cacheKey && this.sdk.options?.cacheKey) {
|
|
340
|
+
cacheKey = this.sdk.options.cacheKey;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Determine threshold:
|
|
344
|
+
// - If cacheKey is provided, enable cache (threshold = 0.05 or custom)
|
|
345
|
+
// - If no cacheKey, disable cache (threshold = -1) unless explicitly overridden
|
|
346
|
+
let threshold;
|
|
347
|
+
if (cacheKey) {
|
|
348
|
+
// cacheKey provided - enable cache with threshold
|
|
349
|
+
threshold = cacheThreshold ?? 0.05;
|
|
350
|
+
} else if (cacheThreshold !== null) {
|
|
351
|
+
// Explicit threshold provided without cacheKey
|
|
352
|
+
threshold = cacheThreshold;
|
|
353
|
+
} else {
|
|
354
|
+
// No cacheKey, no explicit threshold - use global default (which is -1 now)
|
|
355
|
+
threshold = this.sdk.cacheThresholds?.find ?? -1;
|
|
356
|
+
}
|
|
245
357
|
|
|
246
358
|
// Store the threshold for debugging
|
|
247
359
|
this._threshold = threshold;
|
|
@@ -249,16 +361,21 @@ class Element {
|
|
|
249
361
|
// Debug log threshold
|
|
250
362
|
if (debugMode) {
|
|
251
363
|
const { events } = require("./agent/events.js");
|
|
364
|
+
const autoGenMsg = (this.sdk._autoGeneratedCacheKey && cacheKey === this.sdk.options.cacheKey)
|
|
365
|
+
? ' (auto-generated from file hash)'
|
|
366
|
+
: '';
|
|
252
367
|
this.sdk.emitter.emit(
|
|
253
368
|
events.log.debug,
|
|
254
|
-
`🔍 find() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"})`,
|
|
369
|
+
`🔍 find() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
|
|
255
370
|
);
|
|
256
371
|
}
|
|
257
372
|
|
|
258
|
-
|
|
373
|
+
response = await this.sdk.apiClient.req("find", {
|
|
374
|
+
session: this.sdk.getSessionId(),
|
|
259
375
|
element: description,
|
|
260
376
|
image: screenshot,
|
|
261
377
|
threshold: threshold,
|
|
378
|
+
cacheKey: cacheKey,
|
|
262
379
|
os: this.sdk.os,
|
|
263
380
|
resolution: this.sdk.resolution,
|
|
264
381
|
});
|
|
@@ -278,12 +395,36 @@ class Element {
|
|
|
278
395
|
} else {
|
|
279
396
|
this._response = this._sanitizeResponse(response);
|
|
280
397
|
this._found = false;
|
|
398
|
+
findError = "Element not found";
|
|
281
399
|
}
|
|
282
400
|
} catch (error) {
|
|
283
401
|
this._response = error.response
|
|
284
402
|
? this._sanitizeResponse(error.response)
|
|
285
403
|
: null;
|
|
286
404
|
this._found = false;
|
|
405
|
+
findError = error.message;
|
|
406
|
+
response = error.response;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Track find interaction once at the end
|
|
410
|
+
const sessionId = this.sdk.getSessionId();
|
|
411
|
+
if (sessionId && this.sdk.sandbox?.send) {
|
|
412
|
+
try {
|
|
413
|
+
await this.sdk.sandbox.send({
|
|
414
|
+
type: "trackInteraction",
|
|
415
|
+
interactionType: "find",
|
|
416
|
+
session: sessionId,
|
|
417
|
+
prompt: description,
|
|
418
|
+
timestamp: startTime,
|
|
419
|
+
success: this._found,
|
|
420
|
+
error: findError,
|
|
421
|
+
cacheHit: response?.cacheHit || response?.cache_hit || response?.cached || false,
|
|
422
|
+
selector: response?.selector,
|
|
423
|
+
selectorUsed: !!response?.selector,
|
|
424
|
+
});
|
|
425
|
+
} catch (err) {
|
|
426
|
+
console.warn("Failed to track find interaction:", err.message);
|
|
427
|
+
}
|
|
287
428
|
}
|
|
288
429
|
|
|
289
430
|
return this;
|
|
@@ -544,11 +685,8 @@ class Element {
|
|
|
544
685
|
`Element "${this.description}" not found.`,
|
|
545
686
|
{
|
|
546
687
|
description: this.description,
|
|
547
|
-
screenshot: this._screenshot,
|
|
548
688
|
aiResponse: this._response,
|
|
549
689
|
threshold: this._threshold,
|
|
550
|
-
cachedImageUrl: this._response?.cachedImageUrl,
|
|
551
|
-
pixelDiffImage: this._response?.pixelDiffImage,
|
|
552
690
|
},
|
|
553
691
|
);
|
|
554
692
|
}
|
|
@@ -562,10 +700,22 @@ class Element {
|
|
|
562
700
|
);
|
|
563
701
|
this.sdk.emitter.emit(events.log.log, formattedMessage);
|
|
564
702
|
|
|
703
|
+
// Prepare element metadata for interaction tracking
|
|
704
|
+
const elementData = {
|
|
705
|
+
prompt: this.description,
|
|
706
|
+
elementType: this._response?.elementType,
|
|
707
|
+
elementBounds: this._response?.elementBounds,
|
|
708
|
+
croppedImageUrl: this._response?.savedImagePath,
|
|
709
|
+
edgeDetectedImageUrl: this._response?.edgeSavedImagePath || null,
|
|
710
|
+
cacheHit: this._response?.cacheHit,
|
|
711
|
+
selectorUsed: !!this._response?.selector,
|
|
712
|
+
selector: this._response?.selector
|
|
713
|
+
};
|
|
714
|
+
|
|
565
715
|
if (action === "hover") {
|
|
566
|
-
await this.commands.hover(this.coordinates.x, this.coordinates.y);
|
|
716
|
+
await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
|
|
567
717
|
} else {
|
|
568
|
-
await this.commands.click(this.coordinates.x, this.coordinates.y, action);
|
|
718
|
+
await this.commands.click(this.coordinates.x, this.coordinates.y, action, elementData);
|
|
569
719
|
}
|
|
570
720
|
}
|
|
571
721
|
|
|
@@ -579,11 +729,8 @@ class Element {
|
|
|
579
729
|
`Element "${this.description}" not found.`,
|
|
580
730
|
{
|
|
581
731
|
description: this.description,
|
|
582
|
-
screenshot: this._screenshot,
|
|
583
732
|
aiResponse: this._response,
|
|
584
733
|
threshold: this._threshold,
|
|
585
|
-
cachedImageUrl: this._response?.cachedImageUrl,
|
|
586
|
-
pixelDiffImage: this._response?.pixelDiffImage,
|
|
587
734
|
},
|
|
588
735
|
);
|
|
589
736
|
}
|
|
@@ -593,7 +740,19 @@ class Element {
|
|
|
593
740
|
const formattedMessage = formatter.formatAction("hover", this.description);
|
|
594
741
|
this.sdk.emitter.emit(events.log.log, formattedMessage);
|
|
595
742
|
|
|
596
|
-
|
|
743
|
+
// Prepare element metadata for interaction tracking
|
|
744
|
+
const elementData = {
|
|
745
|
+
prompt: this.description,
|
|
746
|
+
elementType: this._response?.elementType,
|
|
747
|
+
elementBounds: this._response?.elementBounds,
|
|
748
|
+
croppedImageUrl: this._response?.savedImagePath,
|
|
749
|
+
edgeDetectedImageUrl: this._response?.edgeSavedImagePath || null,
|
|
750
|
+
cacheHit: this._response?.cacheHit,
|
|
751
|
+
selectorUsed: !!this._response?.selector,
|
|
752
|
+
selector: this._response?.selector
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
|
|
597
756
|
}
|
|
598
757
|
|
|
599
758
|
/**
|
|
@@ -785,6 +944,60 @@ class Element {
|
|
|
785
944
|
}
|
|
786
945
|
}
|
|
787
946
|
|
|
947
|
+
/**
|
|
948
|
+
* Creates a chainable promise that allows method chaining on find() results
|
|
949
|
+
* This enables syntax like: await testdriver.find("button").click()
|
|
950
|
+
*
|
|
951
|
+
* @param {Promise<Element>} promise - The promise that resolves to an Element
|
|
952
|
+
* @returns {Promise<Element> & ChainableElement} A promise with chainable element methods
|
|
953
|
+
*/
|
|
954
|
+
function createChainablePromise(promise) {
|
|
955
|
+
// Define the chainable methods that should be available
|
|
956
|
+
const chainableMethods = ['click', 'hover', 'doubleClick', 'rightClick', 'mouseDown', 'mouseUp'];
|
|
957
|
+
|
|
958
|
+
// Create a new promise that wraps the original
|
|
959
|
+
const chainablePromise = promise.then(element => element);
|
|
960
|
+
|
|
961
|
+
// Add chainable methods to the promise
|
|
962
|
+
for (const method of chainableMethods) {
|
|
963
|
+
chainablePromise[method] = function(...args) {
|
|
964
|
+
// Return a promise that waits for the element, then calls the method
|
|
965
|
+
return promise.then(element => element[method](...args));
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Add getters for element properties (these return promises)
|
|
970
|
+
Object.defineProperty(chainablePromise, 'x', {
|
|
971
|
+
get() { return promise.then(el => el.x); }
|
|
972
|
+
});
|
|
973
|
+
Object.defineProperty(chainablePromise, 'y', {
|
|
974
|
+
get() { return promise.then(el => el.y); }
|
|
975
|
+
});
|
|
976
|
+
Object.defineProperty(chainablePromise, 'centerX', {
|
|
977
|
+
get() { return promise.then(el => el.centerX); }
|
|
978
|
+
});
|
|
979
|
+
Object.defineProperty(chainablePromise, 'centerY', {
|
|
980
|
+
get() { return promise.then(el => el.centerY); }
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
// Add found() method
|
|
984
|
+
chainablePromise.found = function() {
|
|
985
|
+
return promise.then(el => el.found());
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
// Add getCoordinates() method
|
|
989
|
+
chainablePromise.getCoordinates = function() {
|
|
990
|
+
return promise.then(el => el.getCoordinates());
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
// Add getResponse() method
|
|
994
|
+
chainablePromise.getResponse = function() {
|
|
995
|
+
return promise.then(el => el.getResponse());
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
return chainablePromise;
|
|
999
|
+
}
|
|
1000
|
+
|
|
788
1001
|
/**
|
|
789
1002
|
* TestDriver SDK
|
|
790
1003
|
*
|
|
@@ -838,6 +1051,17 @@ class TestDriverSDK {
|
|
|
838
1051
|
},
|
|
839
1052
|
});
|
|
840
1053
|
|
|
1054
|
+
// Auto-generate cache key from caller file hash if not explicitly provided
|
|
1055
|
+
// This allows caching to be tied to the specific test file
|
|
1056
|
+
if (!options.cacheKey) {
|
|
1057
|
+
const autoGeneratedKey = getCallerFileHash();
|
|
1058
|
+
if (autoGeneratedKey) {
|
|
1059
|
+
options.cacheKey = autoGeneratedKey;
|
|
1060
|
+
// Store flag to indicate this was auto-generated
|
|
1061
|
+
this._autoGeneratedCacheKey = true;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
841
1065
|
// Store options for later use
|
|
842
1066
|
this.options = options;
|
|
843
1067
|
|
|
@@ -847,7 +1071,7 @@ class TestDriverSDK {
|
|
|
847
1071
|
|
|
848
1072
|
// Store newSandbox preference from options
|
|
849
1073
|
this.newSandbox =
|
|
850
|
-
options.newSandbox !== undefined ? options.newSandbox :
|
|
1074
|
+
options.newSandbox !== undefined ? options.newSandbox : true;
|
|
851
1075
|
|
|
852
1076
|
// Store headless preference from options
|
|
853
1077
|
this.headless = options.headless !== undefined ? options.headless : false;
|
|
@@ -862,31 +1086,48 @@ class TestDriverSDK {
|
|
|
862
1086
|
|
|
863
1087
|
// Cache threshold configuration
|
|
864
1088
|
// threshold = pixel difference allowed (0.05 = 5% difference, 95% similarity)
|
|
865
|
-
// cache
|
|
866
|
-
//
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
if (!useCache) {
|
|
874
|
-
// If cache is disabled, use -1 to bypass cache entirely
|
|
1089
|
+
// By default, cache is DISABLED (threshold = -1) to avoid unnecessary AI costs
|
|
1090
|
+
// To enable cache, provide a cacheKey when calling find() or findAll()
|
|
1091
|
+
// Also support TD_NO_CACHE environment variable and cache: false option for backwards compatibility
|
|
1092
|
+
const cacheDisabled =
|
|
1093
|
+
options.cache === false || process.env.TD_NO_CACHE === "true";
|
|
1094
|
+
|
|
1095
|
+
if (cacheDisabled) {
|
|
1096
|
+
// Explicit cache disabled via option or env var
|
|
875
1097
|
this.cacheThresholds = {
|
|
876
1098
|
find: -1,
|
|
877
1099
|
findAll: -1,
|
|
878
1100
|
};
|
|
879
1101
|
} else {
|
|
880
|
-
//
|
|
1102
|
+
// Cache disabled by default, enabled only when cacheKey is provided
|
|
1103
|
+
// Note: The threshold value here is the fallback when cacheKey is NOT provided
|
|
881
1104
|
this.cacheThresholds = {
|
|
882
|
-
find: options.cacheThreshold?.find ??
|
|
883
|
-
findAll: options.cacheThreshold?.findAll ??
|
|
1105
|
+
find: options.cacheThreshold?.find ?? -1, // Default: cache disabled
|
|
1106
|
+
findAll: options.cacheThreshold?.findAll ?? -1, // Default: cache disabled
|
|
884
1107
|
};
|
|
885
1108
|
}
|
|
886
1109
|
|
|
887
|
-
// Redraw
|
|
888
|
-
//
|
|
889
|
-
|
|
1110
|
+
// Redraw configuration
|
|
1111
|
+
// Supports both:
|
|
1112
|
+
// - redraw: { enabled: true, diffThreshold: 0.1, screenRedraw: true, networkMonitor: true }
|
|
1113
|
+
// - redrawThreshold: 0.1 (legacy, sets diffThreshold)
|
|
1114
|
+
// The `redraw` option takes precedence and matches the per-command API
|
|
1115
|
+
if (options.redraw !== undefined) {
|
|
1116
|
+
// New unified API: redraw object (matches per-command options)
|
|
1117
|
+
this.redrawOptions = typeof options.redraw === 'object'
|
|
1118
|
+
? options.redraw
|
|
1119
|
+
: { enabled: options.redraw }; // Support redraw: false as shorthand
|
|
1120
|
+
} else if (options.redrawThreshold !== undefined) {
|
|
1121
|
+
// Legacy API: redrawThreshold number or object
|
|
1122
|
+
this.redrawOptions = typeof options.redrawThreshold === 'object'
|
|
1123
|
+
? options.redrawThreshold
|
|
1124
|
+
: { diffThreshold: options.redrawThreshold };
|
|
1125
|
+
} else {
|
|
1126
|
+
// Default: disabled
|
|
1127
|
+
this.redrawOptions = { enabled: false };
|
|
1128
|
+
}
|
|
1129
|
+
// Keep redrawThreshold for backwards compatibility in connect()
|
|
1130
|
+
this.redrawThreshold = this.redrawOptions;
|
|
890
1131
|
|
|
891
1132
|
// Track connection state
|
|
892
1133
|
this.connected = false;
|
|
@@ -910,6 +1151,242 @@ class TestDriverSDK {
|
|
|
910
1151
|
|
|
911
1152
|
// Set up event listeners once (they live for the lifetime of the SDK instance)
|
|
912
1153
|
this._setupLogging();
|
|
1154
|
+
|
|
1155
|
+
// Set up provision API
|
|
1156
|
+
this.provision = this._createProvisionAPI();
|
|
1157
|
+
|
|
1158
|
+
// Set up dashcam API lazily
|
|
1159
|
+
this._dashcam = null;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Wait for the sandbox connection to complete
|
|
1164
|
+
* @returns {Promise<void>}
|
|
1165
|
+
*/
|
|
1166
|
+
async ready() {
|
|
1167
|
+
if (this.__connectionPromise) {
|
|
1168
|
+
await this.__connectionPromise;
|
|
1169
|
+
}
|
|
1170
|
+
if (!this.connected) {
|
|
1171
|
+
throw new Error('Not connected to sandbox. Call connect() first or use autoConnect option.');
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Get or create the Dashcam instance
|
|
1177
|
+
* @returns {Dashcam} Dashcam instance
|
|
1178
|
+
*/
|
|
1179
|
+
get dashcam() {
|
|
1180
|
+
if (!this._dashcam) {
|
|
1181
|
+
const { Dashcam } = require("./src/core/index.js");
|
|
1182
|
+
// Don't pass apiKey - let Dashcam use its default key
|
|
1183
|
+
this._dashcam = new Dashcam(this);
|
|
1184
|
+
}
|
|
1185
|
+
return this._dashcam;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Get milliseconds elapsed since dashcam started recording
|
|
1190
|
+
* @returns {number|null} Milliseconds since dashcam start, or null if not recording
|
|
1191
|
+
*/
|
|
1192
|
+
getDashcamElapsedTime() {
|
|
1193
|
+
if (this._dashcam) {
|
|
1194
|
+
return this._dashcam.getElapsedTime();
|
|
1195
|
+
}
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Create the provision API with methods for launching applications
|
|
1201
|
+
* @private
|
|
1202
|
+
*/
|
|
1203
|
+
_createProvisionAPI() {
|
|
1204
|
+
return {
|
|
1205
|
+
/**
|
|
1206
|
+
* Launch Chrome browser
|
|
1207
|
+
* @param {Object} options - Chrome launch options
|
|
1208
|
+
* @param {string} [options.url='http://testdriver-sandbox.vercel.app/'] - URL to navigate to
|
|
1209
|
+
* @param {boolean} [options.maximized=true] - Start maximized
|
|
1210
|
+
* @param {boolean} [options.guest=false] - Use guest mode
|
|
1211
|
+
* @returns {Promise<void>}
|
|
1212
|
+
*/
|
|
1213
|
+
chrome: async (options = {}) => {
|
|
1214
|
+
// Automatically wait for connection to be ready
|
|
1215
|
+
await this.ready();
|
|
1216
|
+
|
|
1217
|
+
const {
|
|
1218
|
+
url = 'http://testdriver-sandbox.vercel.app/',
|
|
1219
|
+
maximized = true,
|
|
1220
|
+
guest = false,
|
|
1221
|
+
} = options;
|
|
1222
|
+
|
|
1223
|
+
// If dashcam is available and recording, add web logs for this domain
|
|
1224
|
+
if (this._dashcam) {
|
|
1225
|
+
console.log('[provision.chrome] Adding web logs to dashcam...');
|
|
1226
|
+
try {
|
|
1227
|
+
const urlObj = new URL(url);
|
|
1228
|
+
const domain = urlObj.hostname;
|
|
1229
|
+
const pattern = `*${domain}*`;
|
|
1230
|
+
await this._dashcam.addWebLog(pattern, 'Web Logs');
|
|
1231
|
+
console.log(`[provision.chrome] ✅ Web logs added to dashcam (pattern: ${pattern})`);
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
console.warn('[provision.chrome] ⚠️ Failed to add web logs:', error.message);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Automatically start dashcam if not already recording
|
|
1238
|
+
if (!this._dashcam || !this._dashcam.recording) {
|
|
1239
|
+
console.log('[provision.chrome] Starting dashcam...');
|
|
1240
|
+
await this.dashcam.start();
|
|
1241
|
+
console.log('[provision.chrome] ✅ Dashcam started');
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// Build Chrome launch command
|
|
1245
|
+
const chromeArgs = [];
|
|
1246
|
+
if (maximized) chromeArgs.push('--start-maximized');
|
|
1247
|
+
if (guest) chromeArgs.push('--guest');
|
|
1248
|
+
chromeArgs.push('--disable-fre', '--no-default-browser-check', '--no-first-run');
|
|
1249
|
+
|
|
1250
|
+
// Add dashcam-chrome extension on Linux
|
|
1251
|
+
if (this.os === 'linux') {
|
|
1252
|
+
chromeArgs.push('--load-extension=/usr/lib/node_modules/dashcam-chrome/build');
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Launch Chrome
|
|
1256
|
+
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1257
|
+
|
|
1258
|
+
if (this.os === 'windows') {
|
|
1259
|
+
const argsString = chromeArgs.map(arg => `"${arg}"`).join(', ');
|
|
1260
|
+
await this.exec(
|
|
1261
|
+
shell,
|
|
1262
|
+
`Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList ${argsString}, "${url}"`,
|
|
1263
|
+
30000
|
|
1264
|
+
);
|
|
1265
|
+
} else {
|
|
1266
|
+
const argsString = chromeArgs.join(' ');
|
|
1267
|
+
await this.exec(
|
|
1268
|
+
shell,
|
|
1269
|
+
`chrome-for-testing ${argsString} "${url}" >/dev/null 2>&1 &`,
|
|
1270
|
+
30000
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Wait for Chrome to be ready
|
|
1275
|
+
await this.focusApplication('Google Chrome');
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
// Wait for URL to load
|
|
1279
|
+
try {
|
|
1280
|
+
const urlObj = new URL(url);
|
|
1281
|
+
const domain = urlObj.hostname;
|
|
1282
|
+
|
|
1283
|
+
console.log(`[provision.chrome] Waiting for domain "${domain}" to appear in URL bar...`);
|
|
1284
|
+
|
|
1285
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
1286
|
+
try {
|
|
1287
|
+
const result = await this.find(`${domain}`);
|
|
1288
|
+
if (result.found()) {
|
|
1289
|
+
console.log(`[provision.chrome] ✅ Chrome ready at ${url}`);
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
} catch (e) {
|
|
1293
|
+
// Not found yet, continue polling
|
|
1294
|
+
}
|
|
1295
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
await this.focusApplication('Google Chrome');
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
console.warn(`[provision.chrome] ⚠️ Could not parse URL "${url}":`, e.message);
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Launch VS Code
|
|
1306
|
+
* @param {Object} options - VS Code launch options
|
|
1307
|
+
* @param {string} [options.workspace] - Workspace/folder to open
|
|
1308
|
+
* @param {string[]} [options.extensions=[]] - Extensions to install
|
|
1309
|
+
* @returns {Promise<void>}
|
|
1310
|
+
*/
|
|
1311
|
+
vscode: async (options = {}) => {
|
|
1312
|
+
this._ensureConnected();
|
|
1313
|
+
|
|
1314
|
+
const {
|
|
1315
|
+
workspace = null,
|
|
1316
|
+
extensions = [],
|
|
1317
|
+
} = options;
|
|
1318
|
+
|
|
1319
|
+
// Install extensions if provided
|
|
1320
|
+
for (const extension of extensions) {
|
|
1321
|
+
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1322
|
+
await this.exec(
|
|
1323
|
+
shell,
|
|
1324
|
+
`code --install-extension ${extension}`,
|
|
1325
|
+
60000,
|
|
1326
|
+
true
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Launch VS Code
|
|
1331
|
+
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1332
|
+
const workspaceArg = workspace ? `"${workspace}"` : '';
|
|
1333
|
+
|
|
1334
|
+
if (this.os === 'windows') {
|
|
1335
|
+
await this.exec(
|
|
1336
|
+
shell,
|
|
1337
|
+
`Start-Process code -ArgumentList ${workspaceArg}`,
|
|
1338
|
+
30000
|
|
1339
|
+
);
|
|
1340
|
+
} else {
|
|
1341
|
+
await this.exec(
|
|
1342
|
+
shell,
|
|
1343
|
+
`code ${workspaceArg} >/dev/null 2>&1 &`,
|
|
1344
|
+
30000
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Wait for VS Code to be ready
|
|
1349
|
+
await this.focusApplication('Visual Studio Code');
|
|
1350
|
+
console.log('[provision.vscode] ✅ VS Code ready');
|
|
1351
|
+
},
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Launch Electron app
|
|
1355
|
+
* @param {Object} options - Electron launch options
|
|
1356
|
+
* @param {string} options.appPath - Path to Electron app (required)
|
|
1357
|
+
* @param {string[]} [options.args=[]] - Additional electron args
|
|
1358
|
+
* @returns {Promise<void>}
|
|
1359
|
+
*/
|
|
1360
|
+
electron: async (options = {}) => {
|
|
1361
|
+
this._ensureConnected();
|
|
1362
|
+
|
|
1363
|
+
const { appPath, args = [] } = options;
|
|
1364
|
+
|
|
1365
|
+
if (!appPath) {
|
|
1366
|
+
throw new Error('provision.electron requires appPath option');
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1370
|
+
const argsString = args.join(' ');
|
|
1371
|
+
|
|
1372
|
+
if (this.os === 'windows') {
|
|
1373
|
+
await this.exec(
|
|
1374
|
+
shell,
|
|
1375
|
+
`Start-Process electron -ArgumentList "${appPath}", ${argsString}`,
|
|
1376
|
+
30000
|
|
1377
|
+
);
|
|
1378
|
+
} else {
|
|
1379
|
+
await this.exec(
|
|
1380
|
+
shell,
|
|
1381
|
+
`electron "${appPath}" ${argsString} >/dev/null 2>&1 &`,
|
|
1382
|
+
30000
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
await this.focusApplication('Electron');
|
|
1387
|
+
console.log('[provision.electron] ✅ Electron app ready');
|
|
1388
|
+
},
|
|
1389
|
+
};
|
|
913
1390
|
}
|
|
914
1391
|
|
|
915
1392
|
/**
|
|
@@ -1009,12 +1486,33 @@ class TestDriverSDK {
|
|
|
1009
1486
|
// Expose the agent's commands, parser, and commander
|
|
1010
1487
|
this.commands = this.agent.commands;
|
|
1011
1488
|
|
|
1489
|
+
// Recreate commands with dashcam elapsed time support
|
|
1490
|
+
const { createCommands } = require("./agent/lib/commands.js");
|
|
1491
|
+
const commandsResult = createCommands(
|
|
1492
|
+
this.agent.emitter,
|
|
1493
|
+
this.agent.system,
|
|
1494
|
+
this.agent.sandbox,
|
|
1495
|
+
this.agent.config,
|
|
1496
|
+
this.agent.session,
|
|
1497
|
+
() => this.agent.sourceMapper?.currentFilePath || this.agent.thisFile,
|
|
1498
|
+
this.agent.cliArgs.options.redrawThreshold,
|
|
1499
|
+
() => this.getDashcamElapsedTime(), // Pass dashcam elapsed time function
|
|
1500
|
+
);
|
|
1501
|
+
this.commands = commandsResult.commands;
|
|
1502
|
+
this.agent.commands = commandsResult.commands;
|
|
1503
|
+
this.agent.redraw = commandsResult.redraw;
|
|
1504
|
+
|
|
1012
1505
|
// Dynamically create command methods based on available commands
|
|
1013
1506
|
this._setupCommandMethods();
|
|
1014
1507
|
|
|
1015
1508
|
this.connected = true;
|
|
1509
|
+
|
|
1510
|
+
// Expose whether we reconnected to an existing sandbox or created a new one
|
|
1511
|
+
this.isReconnected = this.agent.isReconnected || false;
|
|
1512
|
+
|
|
1016
1513
|
this.analytics.track("sdk.connect", {
|
|
1017
1514
|
sandboxId: this.instance?.instanceId,
|
|
1515
|
+
isReconnected: this.isReconnected,
|
|
1018
1516
|
});
|
|
1019
1517
|
|
|
1020
1518
|
return this.instance;
|
|
@@ -1054,16 +1552,24 @@ class TestDriverSDK {
|
|
|
1054
1552
|
* Automatically locates the element and returns it
|
|
1055
1553
|
*
|
|
1056
1554
|
* @param {string} description - Description of the element to find
|
|
1057
|
-
* @param {number} [
|
|
1058
|
-
* @returns {Promise<Element>} Element instance that has been located
|
|
1555
|
+
* @param {number | Object} [options] - Cache options: number for threshold, or object with {cacheKey, cacheThreshold}
|
|
1556
|
+
* @returns {Promise<Element> & ChainableElement} Element instance that has been located, with chainable methods
|
|
1059
1557
|
*
|
|
1060
1558
|
* @example
|
|
1061
|
-
* // Find and click immediately
|
|
1559
|
+
* // Find and click immediately (chainable)
|
|
1560
|
+
* await client.find('the sign in button').click();
|
|
1561
|
+
*
|
|
1562
|
+
* @example
|
|
1563
|
+
* // Find and click (traditional)
|
|
1062
1564
|
* const element = await client.find('the sign in button');
|
|
1063
1565
|
* await element.click();
|
|
1064
1566
|
*
|
|
1065
1567
|
* @example
|
|
1066
|
-
* // Find with
|
|
1568
|
+
* // Find with cache key to enable caching
|
|
1569
|
+
* const element = await client.find('login button', { cacheKey: 'my-test-run' });
|
|
1570
|
+
*
|
|
1571
|
+
* @example
|
|
1572
|
+
* // Find with custom cache threshold (legacy)
|
|
1067
1573
|
* const element = await client.find('login button', 0.01);
|
|
1068
1574
|
*
|
|
1069
1575
|
* @example
|
|
@@ -1077,10 +1583,14 @@ class TestDriverSDK {
|
|
|
1077
1583
|
* }
|
|
1078
1584
|
* await element.click();
|
|
1079
1585
|
*/
|
|
1080
|
-
|
|
1586
|
+
find(description, options) {
|
|
1081
1587
|
this._ensureConnected();
|
|
1082
1588
|
const element = new Element(description, this, this.system, this.commands);
|
|
1083
|
-
|
|
1589
|
+
const findPromise = element.find(null, options);
|
|
1590
|
+
|
|
1591
|
+
// Create a chainable promise that allows direct method chaining
|
|
1592
|
+
// e.g., await testdriver.find("button").click()
|
|
1593
|
+
return createChainablePromise(findPromise);
|
|
1084
1594
|
}
|
|
1085
1595
|
|
|
1086
1596
|
/**
|
|
@@ -1088,7 +1598,7 @@ class TestDriverSDK {
|
|
|
1088
1598
|
* Automatically locates all matching elements and returns them as an array
|
|
1089
1599
|
*
|
|
1090
1600
|
* @param {string} description - Description of the elements to find
|
|
1091
|
-
* @param {number} [
|
|
1601
|
+
* @param {number | Object} [options] - Cache options: number for threshold, or object with {cacheKey, cacheThreshold}
|
|
1092
1602
|
* @returns {Promise<Element[]>} Array of Element instances that have been located
|
|
1093
1603
|
*
|
|
1094
1604
|
* @example
|
|
@@ -1099,13 +1609,13 @@ class TestDriverSDK {
|
|
|
1099
1609
|
* }
|
|
1100
1610
|
*
|
|
1101
1611
|
* @example
|
|
1102
|
-
* // Find all list items with
|
|
1103
|
-
* const items = await client.findAll('list item',
|
|
1612
|
+
* // Find all list items with cache key to enable caching
|
|
1613
|
+
* const items = await client.findAll('list item', { cacheKey: 'my-test-run' });
|
|
1104
1614
|
* for (const item of items) {
|
|
1105
1615
|
* console.log(`Found item at (${item.x}, ${item.y})`);
|
|
1106
1616
|
* }
|
|
1107
1617
|
*/
|
|
1108
|
-
async findAll(description,
|
|
1618
|
+
async findAll(description, options) {
|
|
1109
1619
|
this._ensureConnected();
|
|
1110
1620
|
|
|
1111
1621
|
const startTime = Date.now();
|
|
@@ -1118,15 +1628,59 @@ class TestDriverSDK {
|
|
|
1118
1628
|
try {
|
|
1119
1629
|
const screenshot = await this.system.captureScreenBase64();
|
|
1120
1630
|
|
|
1121
|
-
//
|
|
1122
|
-
|
|
1631
|
+
// Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold
|
|
1632
|
+
let cacheKey = null;
|
|
1633
|
+
let cacheThreshold = null;
|
|
1634
|
+
|
|
1635
|
+
if (typeof options === 'number') {
|
|
1636
|
+
// Legacy: options is just a number threshold
|
|
1637
|
+
cacheThreshold = options;
|
|
1638
|
+
} else if (typeof options === 'object' && options !== null) {
|
|
1639
|
+
// New: options is an object with cacheKey and/or cacheThreshold
|
|
1640
|
+
cacheKey = options.cacheKey || null;
|
|
1641
|
+
cacheThreshold = options.cacheThreshold ?? null;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Use default cacheKey from SDK constructor if not provided in findAll() options
|
|
1645
|
+
if (!cacheKey && this.options?.cacheKey) {
|
|
1646
|
+
cacheKey = this.options.cacheKey;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// Determine threshold:
|
|
1650
|
+
// - If cacheKey is provided, enable cache (threshold = 0.05 or custom)
|
|
1651
|
+
// - If no cacheKey, disable cache (threshold = -1) unless explicitly overridden
|
|
1652
|
+
let threshold;
|
|
1653
|
+
if (cacheKey) {
|
|
1654
|
+
// cacheKey provided - enable cache with threshold
|
|
1655
|
+
threshold = cacheThreshold ?? 0.05;
|
|
1656
|
+
} else if (cacheThreshold !== null) {
|
|
1657
|
+
// Explicit threshold provided without cacheKey
|
|
1658
|
+
threshold = cacheThreshold;
|
|
1659
|
+
} else {
|
|
1660
|
+
// No cacheKey, no explicit threshold - use global default (which is -1 now)
|
|
1661
|
+
threshold = this.cacheThresholds?.findAll ?? -1;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Debug log threshold
|
|
1665
|
+
const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
|
|
1666
|
+
if (debugMode) {
|
|
1667
|
+
const autoGenMsg = (this._autoGeneratedCacheKey && cacheKey === this.options.cacheKey)
|
|
1668
|
+
? ' (auto-generated from file hash)'
|
|
1669
|
+
: '';
|
|
1670
|
+
this.emitter.emit(
|
|
1671
|
+
events.log.debug,
|
|
1672
|
+
`🔍 findAll() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1123
1675
|
|
|
1124
1676
|
const response = await this.apiClient.req(
|
|
1125
1677
|
"/api/v7.0.0/testdriver-agent/testdriver-find-all",
|
|
1126
1678
|
{
|
|
1679
|
+
session: this.getSessionId(),
|
|
1127
1680
|
element: description,
|
|
1128
1681
|
image: screenshot,
|
|
1129
1682
|
threshold: threshold,
|
|
1683
|
+
cacheKey: cacheKey,
|
|
1130
1684
|
os: this.os,
|
|
1131
1685
|
resolution: this.resolution,
|
|
1132
1686
|
},
|
|
@@ -1173,6 +1727,27 @@ class TestDriverSDK {
|
|
|
1173
1727
|
return element;
|
|
1174
1728
|
});
|
|
1175
1729
|
|
|
1730
|
+
// Track successful findAll interaction
|
|
1731
|
+
const sessionId = this.getSessionId();
|
|
1732
|
+
if (sessionId && this.sandbox?.send) {
|
|
1733
|
+
try {
|
|
1734
|
+
await this.sandbox.send({
|
|
1735
|
+
type: "trackInteraction",
|
|
1736
|
+
interactionType: "findAll",
|
|
1737
|
+
session: sessionId,
|
|
1738
|
+
prompt: description,
|
|
1739
|
+
timestamp: startTime,
|
|
1740
|
+
success: true,
|
|
1741
|
+
input: { count: elements.length },
|
|
1742
|
+
cacheHit: response.cached || false,
|
|
1743
|
+
selector: response.selector,
|
|
1744
|
+
selectorUsed: !!response.selector,
|
|
1745
|
+
});
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
console.warn("Failed to track findAll interaction:", err.message);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1176
1751
|
// Log debug information when elements are found
|
|
1177
1752
|
if (process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG) {
|
|
1178
1753
|
const { events } = require("./agent/events.js");
|
|
@@ -1189,10 +1764,51 @@ class TestDriverSDK {
|
|
|
1189
1764
|
|
|
1190
1765
|
return elements;
|
|
1191
1766
|
} else {
|
|
1767
|
+
// No elements found - track interaction
|
|
1768
|
+
const sessionId = this.getSessionId();
|
|
1769
|
+
if (sessionId && this.sandbox?.send) {
|
|
1770
|
+
try {
|
|
1771
|
+
await this.sandbox.send({
|
|
1772
|
+
type: "trackInteraction",
|
|
1773
|
+
interactionType: "findAll",
|
|
1774
|
+
session: sessionId,
|
|
1775
|
+
prompt: description,
|
|
1776
|
+
timestamp: startTime,
|
|
1777
|
+
success: false,
|
|
1778
|
+
error: "No elements found",
|
|
1779
|
+
input: { count: 0 },
|
|
1780
|
+
cacheHit: response?.cached || false,
|
|
1781
|
+
selector: response?.selector,
|
|
1782
|
+
selectorUsed: !!response?.selector,
|
|
1783
|
+
});
|
|
1784
|
+
} catch (err) {
|
|
1785
|
+
console.warn("Failed to track findAll interaction:", err.message);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1192
1789
|
// No elements found - return empty array
|
|
1193
1790
|
return [];
|
|
1194
1791
|
}
|
|
1195
1792
|
} catch (error) {
|
|
1793
|
+
// Track findAll error interaction
|
|
1794
|
+
const sessionId = this.getSessionId();
|
|
1795
|
+
if (sessionId && this.sandbox?.send) {
|
|
1796
|
+
try {
|
|
1797
|
+
await this.sandbox.send({
|
|
1798
|
+
type: "trackInteraction",
|
|
1799
|
+
interactionType: "findAll",
|
|
1800
|
+
session: sessionId,
|
|
1801
|
+
prompt: description,
|
|
1802
|
+
timestamp: startTime,
|
|
1803
|
+
success: false,
|
|
1804
|
+
error: error.message,
|
|
1805
|
+
input: { count: 0 },
|
|
1806
|
+
});
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
console.warn("Failed to track findAll interaction:", err.message);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1196
1812
|
const { events } = require("./agent/events.js");
|
|
1197
1813
|
this.emitter.emit(events.log.log, `Error in findAll: ${error.message}`);
|
|
1198
1814
|
return [];
|
|
@@ -1245,17 +1861,18 @@ class TestDriverSDK {
|
|
|
1245
1861
|
*/
|
|
1246
1862
|
_setupCommandMethods() {
|
|
1247
1863
|
// Mapping from command names to SDK method names with type definitions
|
|
1864
|
+
// Each command supports both positional args (legacy) and object args (new)
|
|
1248
1865
|
const commandMapping = {
|
|
1249
1866
|
"hover-text": {
|
|
1250
1867
|
name: "hoverText",
|
|
1251
1868
|
/**
|
|
1252
1869
|
* Hover over text on screen
|
|
1253
1870
|
* @deprecated Use find() and element.click() instead
|
|
1254
|
-
* @param {string}
|
|
1255
|
-
* @param {string
|
|
1256
|
-
* @param {
|
|
1257
|
-
* @param {
|
|
1258
|
-
* @param {number} [timeout=5000] - Timeout in milliseconds
|
|
1871
|
+
* @param {Object|string} options - Options object or text (legacy positional)
|
|
1872
|
+
* @param {string} options.text - Text to find and hover over
|
|
1873
|
+
* @param {string|null} [options.description] - Optional description of the element
|
|
1874
|
+
* @param {ClickAction} [options.action='click'] - Action to perform
|
|
1875
|
+
* @param {number} [options.timeout=5000] - Timeout in milliseconds
|
|
1259
1876
|
* @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
|
|
1260
1877
|
*/
|
|
1261
1878
|
doc: "Hover over text on screen (deprecated - use find() instead)",
|
|
@@ -1265,8 +1882,9 @@ class TestDriverSDK {
|
|
|
1265
1882
|
/**
|
|
1266
1883
|
* Hover over an image on screen
|
|
1267
1884
|
* @deprecated Use find() and element.click() instead
|
|
1268
|
-
* @param {string}
|
|
1269
|
-
* @param {
|
|
1885
|
+
* @param {Object|string} options - Options object or description (legacy positional)
|
|
1886
|
+
* @param {string} options.description - Description of the image to find
|
|
1887
|
+
* @param {ClickAction} [options.action='click'] - Action to perform
|
|
1270
1888
|
* @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
|
|
1271
1889
|
*/
|
|
1272
1890
|
doc: "Hover over an image on screen (deprecated - use find() instead)",
|
|
@@ -1275,9 +1893,10 @@ class TestDriverSDK {
|
|
|
1275
1893
|
name: "matchImage",
|
|
1276
1894
|
/**
|
|
1277
1895
|
* Match and interact with an image template
|
|
1278
|
-
* @param {string}
|
|
1279
|
-
* @param {
|
|
1280
|
-
* @param {
|
|
1896
|
+
* @param {Object|string} options - Options object or path (legacy positional)
|
|
1897
|
+
* @param {string} options.path - Path to the image template
|
|
1898
|
+
* @param {ClickAction} [options.action='click'] - Action to perform
|
|
1899
|
+
* @param {boolean} [options.invert=false] - Invert the match
|
|
1281
1900
|
* @returns {Promise<boolean>}
|
|
1282
1901
|
*/
|
|
1283
1902
|
doc: "Match and interact with an image template",
|
|
@@ -1286,17 +1905,20 @@ class TestDriverSDK {
|
|
|
1286
1905
|
name: "type",
|
|
1287
1906
|
/**
|
|
1288
1907
|
* Type text
|
|
1289
|
-
* @param {string
|
|
1290
|
-
* @param {
|
|
1908
|
+
* @param {string|number} text - Text to type
|
|
1909
|
+
* @param {Object} [options] - Additional options
|
|
1910
|
+
* @param {number} [options.delay=250] - Delay between keystrokes in milliseconds
|
|
1911
|
+
* @param {boolean} [options.secret=false] - If true, text is treated as sensitive (not logged or stored)
|
|
1291
1912
|
* @returns {Promise<void>}
|
|
1292
1913
|
*/
|
|
1293
|
-
doc: "Type text",
|
|
1914
|
+
doc: "Type text (use { secret: true } for passwords)",
|
|
1294
1915
|
},
|
|
1295
1916
|
"press-keys": {
|
|
1296
1917
|
name: "pressKeys",
|
|
1297
1918
|
/**
|
|
1298
1919
|
* Press keyboard keys
|
|
1299
1920
|
* @param {KeyboardKey[]} keys - Array of keys to press
|
|
1921
|
+
* @param {Object} [options] - Additional options (reserved for future use)
|
|
1300
1922
|
* @returns {Promise<void>}
|
|
1301
1923
|
*/
|
|
1302
1924
|
doc: "Press keyboard keys",
|
|
@@ -1305,9 +1927,10 @@ class TestDriverSDK {
|
|
|
1305
1927
|
name: "click",
|
|
1306
1928
|
/**
|
|
1307
1929
|
* Click at coordinates
|
|
1308
|
-
* @param {number}
|
|
1309
|
-
* @param {number}
|
|
1310
|
-
* @param {
|
|
1930
|
+
* @param {Object|number} options - Options object or x coordinate (legacy positional)
|
|
1931
|
+
* @param {number} options.x - X coordinate
|
|
1932
|
+
* @param {number} options.y - Y coordinate
|
|
1933
|
+
* @param {ClickAction} [options.action='click'] - Type of click action
|
|
1311
1934
|
* @returns {Promise<void>}
|
|
1312
1935
|
*/
|
|
1313
1936
|
doc: "Click at coordinates",
|
|
@@ -1316,8 +1939,9 @@ class TestDriverSDK {
|
|
|
1316
1939
|
name: "hover",
|
|
1317
1940
|
/**
|
|
1318
1941
|
* Hover at coordinates
|
|
1319
|
-
* @param {number}
|
|
1320
|
-
* @param {number}
|
|
1942
|
+
* @param {Object|number} options - Options object or x coordinate (legacy positional)
|
|
1943
|
+
* @param {number} options.x - X coordinate
|
|
1944
|
+
* @param {number} options.y - Y coordinate
|
|
1321
1945
|
* @returns {Promise<void>}
|
|
1322
1946
|
*/
|
|
1323
1947
|
doc: "Hover at coordinates",
|
|
@@ -1327,7 +1951,8 @@ class TestDriverSDK {
|
|
|
1327
1951
|
/**
|
|
1328
1952
|
* Scroll the page
|
|
1329
1953
|
* @param {ScrollDirection} [direction='down'] - Direction to scroll
|
|
1330
|
-
* @param {
|
|
1954
|
+
* @param {Object} [options] - Additional options
|
|
1955
|
+
* @param {number} [options.amount=300] - Amount to scroll in pixels
|
|
1331
1956
|
* @returns {Promise<void>}
|
|
1332
1957
|
*/
|
|
1333
1958
|
doc: "Scroll the page",
|
|
@@ -1338,6 +1963,7 @@ class TestDriverSDK {
|
|
|
1338
1963
|
* Wait for specified time
|
|
1339
1964
|
* @deprecated Consider using element polling with find() instead of arbitrary waits
|
|
1340
1965
|
* @param {number} [timeout=3000] - Time to wait in milliseconds
|
|
1966
|
+
* @param {Object} [options] - Additional options (reserved for future use)
|
|
1341
1967
|
* @returns {Promise<void>}
|
|
1342
1968
|
*/
|
|
1343
1969
|
doc: "Wait for specified time (deprecated - consider element polling instead)",
|
|
@@ -1347,10 +1973,9 @@ class TestDriverSDK {
|
|
|
1347
1973
|
/**
|
|
1348
1974
|
* Wait for text to appear on screen
|
|
1349
1975
|
* @deprecated Use find() in a polling loop instead
|
|
1350
|
-
* @param {string}
|
|
1351
|
-
* @param {
|
|
1352
|
-
* @param {
|
|
1353
|
-
* @param {boolean} [invert=false] - Invert the match (wait for text to disappear)
|
|
1976
|
+
* @param {Object|string} options - Options object or text (legacy positional)
|
|
1977
|
+
* @param {string} options.text - Text to wait for
|
|
1978
|
+
* @param {number} [options.timeout=5000] - Timeout in milliseconds
|
|
1354
1979
|
* @returns {Promise<void>}
|
|
1355
1980
|
*/
|
|
1356
1981
|
doc: "Wait for text to appear on screen (deprecated - use find() in a loop instead)",
|
|
@@ -1360,9 +1985,9 @@ class TestDriverSDK {
|
|
|
1360
1985
|
/**
|
|
1361
1986
|
* Wait for image to appear on screen
|
|
1362
1987
|
* @deprecated Use find() in a polling loop instead
|
|
1363
|
-
* @param {string}
|
|
1364
|
-
* @param {
|
|
1365
|
-
* @param {
|
|
1988
|
+
* @param {Object|string} options - Options object or description (legacy positional)
|
|
1989
|
+
* @param {string} options.description - Description of the image
|
|
1990
|
+
* @param {number} [options.timeout=10000] - Timeout in milliseconds
|
|
1366
1991
|
* @returns {Promise<void>}
|
|
1367
1992
|
*/
|
|
1368
1993
|
doc: "Wait for image to appear on screen (deprecated - use find() in a loop instead)",
|
|
@@ -1371,12 +1996,11 @@ class TestDriverSDK {
|
|
|
1371
1996
|
name: "scrollUntilText",
|
|
1372
1997
|
/**
|
|
1373
1998
|
* Scroll until text is found
|
|
1374
|
-
* @param {string}
|
|
1375
|
-
* @param {
|
|
1376
|
-
* @param {
|
|
1377
|
-
* @param {
|
|
1378
|
-
* @param {
|
|
1379
|
-
* @param {boolean} [invert=false] - Invert the match
|
|
1999
|
+
* @param {Object|string} options - Options object or text (legacy positional)
|
|
2000
|
+
* @param {string} options.text - Text to find
|
|
2001
|
+
* @param {ScrollDirection} [options.direction='down'] - Scroll direction
|
|
2002
|
+
* @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
|
|
2003
|
+
* @param {boolean} [options.invert=false] - Invert the match
|
|
1380
2004
|
* @returns {Promise<void>}
|
|
1381
2005
|
*/
|
|
1382
2006
|
doc: "Scroll until text is found",
|
|
@@ -1385,12 +2009,13 @@ class TestDriverSDK {
|
|
|
1385
2009
|
name: "scrollUntilImage",
|
|
1386
2010
|
/**
|
|
1387
2011
|
* Scroll until image is found
|
|
1388
|
-
* @param {string}
|
|
1389
|
-
* @param {
|
|
1390
|
-
* @param {
|
|
1391
|
-
* @param {
|
|
1392
|
-
* @param {string
|
|
1393
|
-
* @param {
|
|
2012
|
+
* @param {Object|string} [options] - Options object or description (legacy positional)
|
|
2013
|
+
* @param {string} [options.description] - Description of the image
|
|
2014
|
+
* @param {ScrollDirection} [options.direction='down'] - Scroll direction
|
|
2015
|
+
* @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
|
|
2016
|
+
* @param {string} [options.method='mouse'] - Scroll method
|
|
2017
|
+
* @param {string} [options.path] - Path to image template
|
|
2018
|
+
* @param {boolean} [options.invert=false] - Invert the match
|
|
1394
2019
|
* @returns {Promise<void>}
|
|
1395
2020
|
*/
|
|
1396
2021
|
doc: "Scroll until image is found",
|
|
@@ -1400,6 +2025,7 @@ class TestDriverSDK {
|
|
|
1400
2025
|
/**
|
|
1401
2026
|
* Focus an application by name
|
|
1402
2027
|
* @param {string} name - Application name
|
|
2028
|
+
* @param {Object} [options] - Additional options (reserved for future use)
|
|
1403
2029
|
* @returns {Promise<string>}
|
|
1404
2030
|
*/
|
|
1405
2031
|
doc: "Focus an application by name",
|
|
@@ -1408,7 +2034,8 @@ class TestDriverSDK {
|
|
|
1408
2034
|
name: "remember",
|
|
1409
2035
|
/**
|
|
1410
2036
|
* Extract and remember information from the screen using AI
|
|
1411
|
-
* @param {string}
|
|
2037
|
+
* @param {Object|string} options - Options object or description (legacy positional)
|
|
2038
|
+
* @param {string} options.description - What to remember
|
|
1412
2039
|
* @returns {Promise<string>}
|
|
1413
2040
|
*/
|
|
1414
2041
|
doc: "Extract and remember information from the screen",
|
|
@@ -1418,6 +2045,7 @@ class TestDriverSDK {
|
|
|
1418
2045
|
/**
|
|
1419
2046
|
* Make an AI-powered assertion
|
|
1420
2047
|
* @param {string} assertion - Assertion to check
|
|
2048
|
+
* @param {Object} [options] - Additional options (reserved for future use)
|
|
1421
2049
|
* @returns {Promise<boolean>}
|
|
1422
2050
|
*/
|
|
1423
2051
|
doc: "Make an AI-powered assertion",
|
|
@@ -1426,10 +2054,11 @@ class TestDriverSDK {
|
|
|
1426
2054
|
name: "exec",
|
|
1427
2055
|
/**
|
|
1428
2056
|
* Execute code in the sandbox
|
|
1429
|
-
* @param {ExecLanguage}
|
|
1430
|
-
* @param {
|
|
1431
|
-
* @param {
|
|
1432
|
-
* @param {
|
|
2057
|
+
* @param {Object|ExecLanguage} options - Options object or language (legacy positional)
|
|
2058
|
+
* @param {ExecLanguage} [options.language='pwsh'] - Language ('js', 'pwsh', or 'sh')
|
|
2059
|
+
* @param {string} options.code - Code to execute
|
|
2060
|
+
* @param {number} [options.timeout] - Timeout in milliseconds
|
|
2061
|
+
* @param {boolean} [options.silent=false] - Suppress output
|
|
1433
2062
|
* @returns {Promise<string>}
|
|
1434
2063
|
*/
|
|
1435
2064
|
doc: "Execute code in the sandbox",
|
|
@@ -1580,6 +2209,9 @@ class TestDriverSDK {
|
|
|
1580
2209
|
? `[${this.testContext}] ${message}`
|
|
1581
2210
|
: message;
|
|
1582
2211
|
console.log(prefixedMessage);
|
|
2212
|
+
|
|
2213
|
+
// Also forward to sandbox for dashcam
|
|
2214
|
+
this._forwardLogToSandbox(prefixedMessage);
|
|
1583
2215
|
}
|
|
1584
2216
|
});
|
|
1585
2217
|
|
|
@@ -1753,8 +2385,37 @@ class TestDriverSDK {
|
|
|
1753
2385
|
// Auto-detect sandbox ID from the active sandbox if not provided
|
|
1754
2386
|
const sandboxId = options.sandboxId || this.agent?.sandbox?.id || null;
|
|
1755
2387
|
|
|
1756
|
-
// Get session ID
|
|
1757
|
-
|
|
2388
|
+
// Get or create session ID using the agent's newSession method
|
|
2389
|
+
let sessionId = this.agent?.sessionInstance?.get() || null;
|
|
2390
|
+
|
|
2391
|
+
// If no session exists, create one using the agent's method
|
|
2392
|
+
if (!sessionId && this.agent?.newSession) {
|
|
2393
|
+
try {
|
|
2394
|
+
await this.agent.newSession();
|
|
2395
|
+
sessionId = this.agent.sessionInstance.get();
|
|
2396
|
+
|
|
2397
|
+
// Save session ID to file for reuse across test runs
|
|
2398
|
+
if (sessionId) {
|
|
2399
|
+
const sessionFile = path.join(os.homedir(), '.testdriverai-session');
|
|
2400
|
+
fs.writeFileSync(sessionFile, sessionId, { encoding: 'utf-8' });
|
|
2401
|
+
}
|
|
2402
|
+
} catch (error) {
|
|
2403
|
+
// Log but don't fail - tests can run without a session
|
|
2404
|
+
console.warn('Failed to create session:', error.message);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// If still no session, try reading from file (for reporter/separate processes)
|
|
2409
|
+
if (!sessionId) {
|
|
2410
|
+
try {
|
|
2411
|
+
const sessionFile = path.join(os.homedir(), '.testdriverai-session');
|
|
2412
|
+
if (fs.existsSync(sessionFile)) {
|
|
2413
|
+
sessionId = fs.readFileSync(sessionFile, 'utf-8').trim();
|
|
2414
|
+
}
|
|
2415
|
+
} catch (error) {
|
|
2416
|
+
// Ignore file read errors
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
1758
2419
|
|
|
1759
2420
|
const data = {
|
|
1760
2421
|
runId: options.runId,
|