testdriverai 7.2.3 → 7.2.10
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 +163 -0
- package/.testdriver/last-sandbox +7 -0
- package/agent/events.js +1 -0
- package/agent/index.js +99 -163
- package/agent/lib/sandbox.js +11 -1
- package/agents.md +393 -0
- package/bin/testdriverai.js +8 -0
- package/debug/01-table-initial.png +0 -0
- package/debug/02-after-ai-explore.png +0 -0
- package/debug/02-after-scroll.png +0 -0
- package/debugger/index.html +37 -0
- package/docs/docs.json +93 -125
- package/docs/v7/_drafts/architecture.mdx +1 -26
- package/docs/v7/_drafts/caching.mdx +2 -2
- package/docs/v7/{getting-started → _drafts}/installation.mdx +0 -66
- package/docs/v7/{features/coverage.mdx → _drafts/powerful.mdx} +1 -90
- package/docs/v7/_drafts/quick-start-test-recording.mdx +0 -1
- package/docs/v7/{features → _drafts}/scalable.mdx +126 -4
- package/docs/v7/_drafts/screenshot.mdx +155 -0
- package/docs/v7/_drafts/test-recording.mdx +0 -6
- package/docs/v7/_drafts/writing-tests.mdx +25 -0
- package/docs/v7/{api/act.mdx → ai.mdx} +28 -27
- package/docs/v7/{api/assert.mdx → assert.mdx} +3 -3
- package/docs/v7/aws-setup.mdx +338 -0
- package/docs/v7/caching.mdx +128 -0
- package/docs/v7/ci-cd.mdx +605 -0
- package/docs/v7/{api/click.mdx → click.mdx} +4 -4
- package/docs/v7/cloud.mdx +120 -0
- package/docs/v7/customizing-devices.mdx +129 -0
- package/docs/v7/{api/doubleClick.mdx → double-click.mdx} +5 -5
- package/docs/v7/enterprise.mdx +135 -0
- package/docs/v7/examples.mdx +5 -0
- package/docs/v7/{api/exec.mdx → exec.mdx} +3 -3
- package/docs/v7/{api/find.mdx → find.mdx} +17 -21
- package/docs/v7/{api/focusApplication.mdx → focus-application.mdx} +3 -3
- package/docs/v7/generating-tests.mdx +32 -0
- package/docs/v7/{api/hover.mdx → hover.mdx} +3 -3
- package/docs/v7/locating-elements.mdx +71 -0
- package/docs/v7/making-assertions.mdx +32 -0
- package/docs/v7/{api/mouseDown.mdx → mouse-down.mdx} +7 -7
- package/docs/v7/{api/mouseUp.mdx → mouse-up.mdx} +8 -8
- package/docs/v7/performing-actions.mdx +51 -0
- package/docs/v7/{api/pressKeys.mdx → press-keys.mdx} +3 -3
- package/docs/v7/quickstart.mdx +162 -0
- package/docs/v7/reusable-code.mdx +240 -0
- package/docs/v7/{api/rightClick.mdx → right-click.mdx} +5 -5
- package/docs/v7/running-tests.mdx +181 -0
- package/docs/v7/{api/scroll.mdx → scroll.mdx} +3 -3
- package/docs/v7/secrets.mdx +115 -0
- package/docs/v7/self-hosted.mdx +66 -0
- package/docs/v7/{api/type.mdx → type.mdx} +3 -3
- package/docs/v7/variables.mdx +111 -0
- package/docs/v7/waiting-for-elements.mdx +66 -0
- package/docs/v7/what-is-testdriver.mdx +54 -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 +81 -81
- package/package.json +4 -3
- package/sdk-log-formatter.js +41 -0
- package/sdk.d.ts +22 -9
- package/sdk.js +344 -100
- package/test/manual/reconnect-provision.test.mjs +49 -0
- package/test/manual/reconnect-signin.test.mjs +41 -0
- package/test/testdriver/act.test.mjs +30 -0
- package/test/testdriver/ai.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 -119
- package/test/testdriver/windows-installer.test.mjs +61 -0
- package/tests/example.test.js +33 -0
- package/tests/login.js +28 -0
- package/tests/table-sort-enrollments.test.mjs +72 -0
- package/tests/table-sort-experiment.test.mjs +42 -0
- package/tests/table-sort-setup.test.mjs +59 -0
- package/vitest.config.mjs +3 -1
- package/agent/lib/cache.js +0 -142
- package/docs/v7/api/assertions.mdx +0 -403
- package/docs/v7/features/ai-native.mdx +0 -413
- package/docs/v7/features/application-logs.mdx +0 -353
- package/docs/v7/features/browser-logs.mdx +0 -414
- package/docs/v7/features/cache-management.mdx +0 -402
- package/docs/v7/features/continuous-testing.mdx +0 -346
- package/docs/v7/features/data-driven-testing.mdx +0 -441
- package/docs/v7/features/easy-to-write.mdx +0 -280
- package/docs/v7/features/enterprise.mdx +0 -656
- package/docs/v7/features/fast.mdx +0 -406
- package/docs/v7/features/managed-sandboxes.mdx +0 -384
- package/docs/v7/features/network-monitoring.mdx +0 -568
- package/docs/v7/features/parallel-execution.mdx +0 -381
- package/docs/v7/features/powerful.mdx +0 -531
- package/docs/v7/features/sandbox-customization.mdx +0 -229
- package/docs/v7/features/stable.mdx +0 -473
- package/docs/v7/features/system-performance.mdx +0 -616
- package/docs/v7/features/test-analytics.mdx +0 -373
- package/docs/v7/features/test-cases.mdx +0 -393
- package/docs/v7/features/test-replays.mdx +0 -408
- package/docs/v7/features/test-reports.mdx +0 -308
- package/docs/v7/getting-started/debugging-tests.mdx +0 -382
- package/docs/v7/getting-started/quickstart.mdx +0 -90
- package/docs/v7/getting-started/running-tests.mdx +0 -173
- package/docs/v7/getting-started/setting-up-in-ci.mdx +0 -612
- package/docs/v7/getting-started/writing-tests.mdx +0 -534
- package/docs/v7/overview/what-is-testdriver.mdx +0 -386
- package/docs/v7/presets/chrome-extension.mdx +0 -248
- package/docs/v7/presets/chrome.mdx +0 -300
- package/docs/v7/presets/electron.mdx +0 -460
- package/docs/v7/presets/vscode.mdx +0 -417
- package/docs/v7/presets/webapp.mdx +0 -393
- /package/docs/v7/{commands → _drafts/commands}/assert.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/exec.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/focus-application.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/hover-image.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/hover-text.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/if.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/match-image.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/press-keys.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/remember.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/run.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/scroll-until-image.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/scroll-until-text.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/scroll.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/type.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/wait-for-image.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/wait-for-text.mdx +0 -0
- /package/docs/v7/{commands → _drafts/commands}/wait.mdx +0 -0
- /package/docs/v7/{getting-started → _drafts}/configuration.mdx +0 -0
- /package/docs/v7/{features → _drafts}/observable.mdx +0 -0
- /package/docs/v7/{platforms → _drafts/platforms}/linux.mdx +0 -0
- /package/docs/v7/{platforms → _drafts/platforms}/macos.mdx +0 -0
- /package/docs/v7/{platforms → _drafts/platforms}/windows.mdx +0 -0
- /package/docs/v7/{playwright.mdx → _drafts/playwright.mdx} +0 -0
- /package/docs/v7/{overview → _drafts}/readme.mdx +0 -0
- /package/docs/v7/{features → _drafts}/reports.mdx +0 -0
- /package/docs/v7/{api/client.mdx → client.mdx} +0 -0
- /package/docs/v7/{api/dashcam.mdx → dashcam.mdx} +0 -0
- /package/docs/v7/{api/elements.mdx → elements.mdx} +0 -0
- /package/docs/v7/{api/sandbox.mdx → sandbox.mdx} +0 -0
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
|
*/
|
|
@@ -330,10 +372,17 @@ class Element {
|
|
|
330
372
|
/**
|
|
331
373
|
* Find the element on screen
|
|
332
374
|
* @param {string} [newDescription] - Optional new description to search for
|
|
333
|
-
* @param {Object} [options] - Optional options object with cacheThreshold and/or
|
|
375
|
+
* @param {Object} [options] - Optional options object with cacheThreshold, cacheKey, and/or timeout
|
|
376
|
+
* @param {number} [options.timeout] - Max time in ms to poll for element (polls every 5 seconds)
|
|
334
377
|
* @returns {Promise<Element>} This element instance
|
|
335
378
|
*/
|
|
336
379
|
async find(newDescription, options) {
|
|
380
|
+
// Handle timeout/polling option
|
|
381
|
+
const timeout = typeof options === 'object' ? options?.timeout : null;
|
|
382
|
+
if (timeout && timeout > 0) {
|
|
383
|
+
return this._findWithTimeout(newDescription, options, timeout);
|
|
384
|
+
}
|
|
385
|
+
|
|
337
386
|
const description = newDescription || this.description;
|
|
338
387
|
if (newDescription) {
|
|
339
388
|
this.description = newDescription;
|
|
@@ -484,6 +533,61 @@ class Element {
|
|
|
484
533
|
return this;
|
|
485
534
|
}
|
|
486
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Find element with polling/timeout support
|
|
538
|
+
* @private
|
|
539
|
+
* @param {string} [newDescription] - Optional new description to search for
|
|
540
|
+
* @param {Object} options - Options object
|
|
541
|
+
* @param {number} timeout - Max time in ms to poll for element
|
|
542
|
+
* @returns {Promise<Element>} This element instance
|
|
543
|
+
*/
|
|
544
|
+
async _findWithTimeout(newDescription, options, timeout) {
|
|
545
|
+
const POLL_INTERVAL = 5000; // 5 seconds between attempts
|
|
546
|
+
const startTime = Date.now();
|
|
547
|
+
const description = newDescription || this.description;
|
|
548
|
+
|
|
549
|
+
// Log that we're starting a polling find
|
|
550
|
+
const { events } = require("./agent/events.js");
|
|
551
|
+
this.sdk.emitter.emit(events.log.log, `🔄 Polling for "${description}" (timeout: ${timeout}ms)`);
|
|
552
|
+
|
|
553
|
+
// Create options without timeout to avoid infinite recursion
|
|
554
|
+
const findOptions = typeof options === 'object' ? { ...options } : {};
|
|
555
|
+
delete findOptions.timeout;
|
|
556
|
+
|
|
557
|
+
let attempts = 0;
|
|
558
|
+
while (Date.now() - startTime < timeout) {
|
|
559
|
+
attempts++;
|
|
560
|
+
|
|
561
|
+
// Call the regular find (without timeout option)
|
|
562
|
+
await this.find(newDescription, findOptions);
|
|
563
|
+
|
|
564
|
+
if (this._found) {
|
|
565
|
+
this.sdk.emitter.emit(events.log.log, `✅ Found "${description}" after ${attempts} attempt(s)`);
|
|
566
|
+
return this;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const elapsed = Date.now() - startTime;
|
|
570
|
+
const remaining = timeout - elapsed;
|
|
571
|
+
|
|
572
|
+
if (remaining > POLL_INTERVAL) {
|
|
573
|
+
this.sdk.emitter.emit(events.log.log, `⏳ Element not found, retrying in 5s... (${Math.round(remaining / 1000)}s remaining)`);
|
|
574
|
+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
|
|
575
|
+
} else if (remaining > 0) {
|
|
576
|
+
// Less than 5s remaining, wait the remaining time and try once more
|
|
577
|
+
await new Promise(resolve => setTimeout(resolve, remaining));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Final attempt after timeout
|
|
582
|
+
await this.find(newDescription, findOptions);
|
|
583
|
+
|
|
584
|
+
if (!this._found) {
|
|
585
|
+
this.sdk.emitter.emit(events.log.log, `❌ Element "${description}" not found after ${timeout}ms (${attempts} attempts)`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return this;
|
|
589
|
+
}
|
|
590
|
+
|
|
487
591
|
/**
|
|
488
592
|
* Sanitize response by removing large base64 data to prevent memory leaks
|
|
489
593
|
* @private
|
|
@@ -1141,6 +1245,9 @@ class TestDriverSDK {
|
|
|
1141
1245
|
this.sandboxAmi = options.sandboxAmi || null;
|
|
1142
1246
|
this.sandboxInstance = options.sandboxInstance || null;
|
|
1143
1247
|
|
|
1248
|
+
// Store reconnect preference from options
|
|
1249
|
+
this.reconnect = options.reconnect !== undefined ? options.reconnect : false;
|
|
1250
|
+
|
|
1144
1251
|
// Cache threshold configuration
|
|
1145
1252
|
// threshold = pixel difference allowed (0.05 = 5% difference, 95% similarity)
|
|
1146
1253
|
// By default, cache is DISABLED (threshold = -1) to avoid unnecessary AI costs
|
|
@@ -1418,22 +1525,19 @@ class TestDriverSDK {
|
|
|
1418
1525
|
* @param {Object} options - Chrome extension launch options
|
|
1419
1526
|
* @param {string} [options.extensionPath] - Local filesystem path to the unpacked extension directory
|
|
1420
1527
|
* @param {string} [options.extensionId] - Chrome Web Store extension ID (e.g., "cjpalhdlnbpafiamejdnhcphjbkeiagm" for uBlock Origin)
|
|
1421
|
-
* @param {string} [options.url='http://testdriver-sandbox.vercel.app/'] - URL to navigate to
|
|
1422
1528
|
* @param {boolean} [options.maximized=true] - Start maximized
|
|
1423
1529
|
* @returns {Promise<void>}
|
|
1424
1530
|
* @example
|
|
1425
1531
|
* // Load extension from local path
|
|
1426
1532
|
* await testdriver.exec('sh', 'git clone https://github.com/user/extension.git /tmp/extension');
|
|
1427
1533
|
* await testdriver.provision.chromeExtension({
|
|
1428
|
-
* extensionPath: '/tmp/extension'
|
|
1429
|
-
* url: 'https://example.com'
|
|
1534
|
+
* extensionPath: '/tmp/extension'
|
|
1430
1535
|
* });
|
|
1431
1536
|
*
|
|
1432
1537
|
* @example
|
|
1433
1538
|
* // Load extension by Chrome Web Store ID
|
|
1434
1539
|
* await testdriver.provision.chromeExtension({
|
|
1435
|
-
* extensionId: 'cjpalhdlnbpafiamejdnhcphjbkeiagm'
|
|
1436
|
-
* url: 'https://example.com'
|
|
1540
|
+
* extensionId: 'cjpalhdlnbpafiamejdnhcphjbkeiagm' // uBlock Origin
|
|
1437
1541
|
* });
|
|
1438
1542
|
*/
|
|
1439
1543
|
chromeExtension: async (options = {}) => {
|
|
@@ -1443,7 +1547,6 @@ class TestDriverSDK {
|
|
|
1443
1547
|
const {
|
|
1444
1548
|
extensionPath: providedExtensionPath,
|
|
1445
1549
|
extensionId,
|
|
1446
|
-
url = 'http://testdriver-sandbox.vercel.app/',
|
|
1447
1550
|
maximized = true,
|
|
1448
1551
|
} = options;
|
|
1449
1552
|
|
|
@@ -1561,7 +1664,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1561
1664
|
console.log(`[provision.chromeExtension] Extension ${extensionId} extracted to ${extensionPath}`);
|
|
1562
1665
|
}
|
|
1563
1666
|
|
|
1564
|
-
// If dashcam is available
|
|
1667
|
+
// If dashcam is available, set up file logging
|
|
1565
1668
|
if (this._dashcam) {
|
|
1566
1669
|
// Create the log file on the remote machine
|
|
1567
1670
|
const logPath = this.os === "windows"
|
|
@@ -1573,11 +1676,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1573
1676
|
: `touch ${logPath}`;
|
|
1574
1677
|
|
|
1575
1678
|
await this.exec(shell, createLogCmd, 10000, true);
|
|
1576
|
-
|
|
1577
|
-
const urlObj = new URL(url);
|
|
1578
|
-
const domain = urlObj.hostname;
|
|
1579
|
-
const pattern = `*${domain}*`;
|
|
1580
|
-
await this._dashcam.addWebLog(pattern, 'Web Logs');
|
|
1581
1679
|
await this._dashcam.addFileLog(logPath, "TestDriver Log");
|
|
1582
1680
|
}
|
|
1583
1681
|
|
|
@@ -1653,19 +1751,19 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1653
1751
|
chromeArgs.push(`--load-extension=${extensionPath}`);
|
|
1654
1752
|
}
|
|
1655
1753
|
|
|
1656
|
-
// Launch Chrome
|
|
1754
|
+
// Launch Chrome (opens to New Tab by default)
|
|
1657
1755
|
if (this.os === 'windows') {
|
|
1658
1756
|
const argsString = chromeArgs.map(arg => `"${arg}"`).join(', ');
|
|
1659
1757
|
await this.exec(
|
|
1660
1758
|
shell,
|
|
1661
|
-
`Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList ${argsString}
|
|
1759
|
+
`Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList ${argsString}`,
|
|
1662
1760
|
30000
|
|
1663
1761
|
);
|
|
1664
1762
|
} else {
|
|
1665
1763
|
const argsString = chromeArgs.join(' ');
|
|
1666
1764
|
await this.exec(
|
|
1667
1765
|
shell,
|
|
1668
|
-
`chrome-for-testing ${argsString}
|
|
1766
|
+
`chrome-for-testing ${argsString} >/dev/null 2>&1 &`,
|
|
1669
1767
|
30000
|
|
1670
1768
|
);
|
|
1671
1769
|
}
|
|
@@ -1673,25 +1771,18 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1673
1771
|
// Wait for Chrome to be ready
|
|
1674
1772
|
await this.focusApplication('Google Chrome');
|
|
1675
1773
|
|
|
1676
|
-
// Wait for
|
|
1677
|
-
|
|
1678
|
-
const
|
|
1679
|
-
const domain = urlObj.hostname;
|
|
1680
|
-
|
|
1681
|
-
for (let attempt = 0; attempt < 30; attempt++) {
|
|
1682
|
-
const result = await this.find(`${domain}`);
|
|
1774
|
+
// Wait for New Tab to appear
|
|
1775
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
1776
|
+
const result = await this.find('New Tab');
|
|
1683
1777
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
}
|
|
1778
|
+
if (result.found()) {
|
|
1779
|
+
break;
|
|
1780
|
+
} else {
|
|
1781
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1689
1782
|
}
|
|
1690
|
-
|
|
1691
|
-
await this.focusApplication('Google Chrome');
|
|
1692
|
-
} catch (e) {
|
|
1693
|
-
console.warn(`[provision.chromeExtension] ⚠️ Could not parse URL "${url}":`, e.message);
|
|
1694
1783
|
}
|
|
1784
|
+
|
|
1785
|
+
await this.focusApplication('Google Chrome');
|
|
1695
1786
|
},
|
|
1696
1787
|
|
|
1697
1788
|
/**
|
|
@@ -1858,33 +1949,82 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1858
1949
|
);
|
|
1859
1950
|
}
|
|
1860
1951
|
|
|
1861
|
-
|
|
1952
|
+
// Check if the downloaded file has a proper extension, if not scan the download directory
|
|
1953
|
+
let actualFilePath = filePath;
|
|
1954
|
+
const hasValidExtension = /\.(msi|exe|deb|rpm|appimage|sh|dmg|pkg)$/i.test(detectedFilename);
|
|
1955
|
+
|
|
1956
|
+
if (!hasValidExtension && this.os === 'windows') {
|
|
1957
|
+
// On Windows, scan the download directory for .msi or .exe files
|
|
1958
|
+
console.log(`[provision.installer] Downloaded file has no extension, scanning for .msi or .exe files...`);
|
|
1959
|
+
const scanResult = await this.exec(
|
|
1960
|
+
shell,
|
|
1961
|
+
`Get-ChildItem -Path "${downloadDir}" -File | Where-Object { $_.Extension -match '\\.(msi|exe)$' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName`,
|
|
1962
|
+
30000,
|
|
1963
|
+
true
|
|
1964
|
+
);
|
|
1965
|
+
|
|
1966
|
+
if (scanResult && scanResult.trim()) {
|
|
1967
|
+
actualFilePath = scanResult.trim();
|
|
1968
|
+
console.log(`[provision.installer] Found installer: ${actualFilePath}`);
|
|
1969
|
+
}
|
|
1970
|
+
} else if (!hasValidExtension && this.os === 'linux') {
|
|
1971
|
+
// On Linux, scan for common installer extensions
|
|
1972
|
+
console.log(`[provision.installer] Downloaded file has no extension, scanning for installer files...`);
|
|
1973
|
+
const scanResult = await this.exec(
|
|
1974
|
+
shell,
|
|
1975
|
+
`find "${downloadDir}" -maxdepth 1 -type f \\( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" -o -name "*.sh" \\) -printf '%T@ %p\\n' | sort -rn | head -1 | cut -d' ' -f2-`,
|
|
1976
|
+
30000,
|
|
1977
|
+
true
|
|
1978
|
+
);
|
|
1979
|
+
|
|
1980
|
+
if (scanResult && scanResult.trim()) {
|
|
1981
|
+
actualFilePath = scanResult.trim();
|
|
1982
|
+
console.log(`[provision.installer] Found installer: ${actualFilePath}`);
|
|
1983
|
+
}
|
|
1984
|
+
} else if (!hasValidExtension && this.os === 'darwin') {
|
|
1985
|
+
// On macOS, scan for common installer extensions
|
|
1986
|
+
console.log(`[provision.installer] Downloaded file has no extension, scanning for installer files...`);
|
|
1987
|
+
const scanResult = await this.exec(
|
|
1988
|
+
shell,
|
|
1989
|
+
`find "${downloadDir}" -maxdepth 1 -type f \\( -name "*.dmg" -o -name "*.pkg" \\) -print0 | xargs -0 ls -t | head -1`,
|
|
1990
|
+
30000,
|
|
1991
|
+
true
|
|
1992
|
+
);
|
|
1993
|
+
|
|
1994
|
+
if (scanResult && scanResult.trim()) {
|
|
1995
|
+
actualFilePath = scanResult.trim();
|
|
1996
|
+
console.log(`[provision.installer] Found installer: ${actualFilePath}`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
console.log(`[provision.installer] ✅ Downloaded to ${actualFilePath}`);
|
|
1862
2001
|
|
|
1863
|
-
// Auto-detect install command based on file extension
|
|
1864
|
-
const
|
|
2002
|
+
// Auto-detect install command based on file extension (use actualFilePath for extension detection)
|
|
2003
|
+
const actualFilename = actualFilePath.split(/[/\\]/).pop() || '';
|
|
2004
|
+
const ext = actualFilename.split('.').pop()?.toLowerCase();
|
|
1865
2005
|
let installCommand = null;
|
|
1866
2006
|
|
|
1867
2007
|
if (this.os === 'windows') {
|
|
1868
2008
|
if (ext === 'msi') {
|
|
1869
|
-
installCommand = `Start-Process msiexec -ArgumentList '/i', '"${
|
|
2009
|
+
installCommand = `Start-Process msiexec -ArgumentList '/i', '"${actualFilePath}"', '/quiet', '/norestart' -Wait`;
|
|
1870
2010
|
} else if (ext === 'exe') {
|
|
1871
|
-
installCommand = `Start-Process "${
|
|
2011
|
+
installCommand = `Start-Process "${actualFilePath}" -ArgumentList '/S' -Wait`;
|
|
1872
2012
|
}
|
|
1873
2013
|
} else if (this.os === 'linux') {
|
|
1874
2014
|
if (ext === 'deb') {
|
|
1875
|
-
installCommand = `sudo dpkg -i "${
|
|
2015
|
+
installCommand = `sudo dpkg -i "${actualFilePath}" && sudo apt-get install -f -y`;
|
|
1876
2016
|
} else if (ext === 'rpm') {
|
|
1877
|
-
installCommand = `sudo rpm -i "${
|
|
2017
|
+
installCommand = `sudo rpm -i "${actualFilePath}"`;
|
|
1878
2018
|
} else if (ext === 'appimage') {
|
|
1879
|
-
installCommand = `chmod +x "${
|
|
2019
|
+
installCommand = `chmod +x "${actualFilePath}"`;
|
|
1880
2020
|
} else if (ext === 'sh') {
|
|
1881
|
-
installCommand = `chmod +x "${
|
|
2021
|
+
installCommand = `chmod +x "${actualFilePath}" && "${actualFilePath}"`;
|
|
1882
2022
|
}
|
|
1883
2023
|
} else if (this.os === 'darwin') {
|
|
1884
2024
|
if (ext === 'dmg') {
|
|
1885
|
-
installCommand = `hdiutil attach "${
|
|
2025
|
+
installCommand = `hdiutil attach "${actualFilePath}" -mountpoint /Volumes/installer && cp -R /Volumes/installer/*.app /Applications/ && hdiutil detach /Volumes/installer`;
|
|
1886
2026
|
} else if (ext === 'pkg') {
|
|
1887
|
-
installCommand = `sudo installer -pkg "${
|
|
2027
|
+
installCommand = `sudo installer -pkg "${actualFilePath}" -target /`;
|
|
1888
2028
|
}
|
|
1889
2029
|
}
|
|
1890
2030
|
|
|
@@ -1900,7 +2040,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1900
2040
|
await this.focusApplication(appName);
|
|
1901
2041
|
}
|
|
1902
2042
|
|
|
1903
|
-
return
|
|
2043
|
+
return actualFilePath;
|
|
1904
2044
|
},
|
|
1905
2045
|
|
|
1906
2046
|
/**
|
|
@@ -1997,6 +2137,29 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
1997
2137
|
: this.newSandbox,
|
|
1998
2138
|
};
|
|
1999
2139
|
|
|
2140
|
+
// Handle reconnect option - use last sandbox file
|
|
2141
|
+
// Check both connectOptions and constructor options
|
|
2142
|
+
const shouldReconnect = connectOptions.reconnect !== undefined
|
|
2143
|
+
? connectOptions.reconnect
|
|
2144
|
+
: this.reconnect;
|
|
2145
|
+
|
|
2146
|
+
if (shouldReconnect) {
|
|
2147
|
+
const lastSandbox = this.agent.getLastSandboxId();
|
|
2148
|
+
if (!lastSandbox || !lastSandbox.sandboxId) {
|
|
2149
|
+
throw new Error(
|
|
2150
|
+
"Cannot reconnect: No previous sandbox found. Run a test first to create a sandbox, or remove the reconnect option."
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
this.agent.sandboxId = lastSandbox.sandboxId;
|
|
2154
|
+
buildEnvOptions.new = false;
|
|
2155
|
+
|
|
2156
|
+
// Use OS from last sandbox if not explicitly specified
|
|
2157
|
+
if (!connectOptions.os && lastSandbox.os) {
|
|
2158
|
+
this.agent.sandboxOs = lastSandbox.os;
|
|
2159
|
+
this.os = lastSandbox.os;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2000
2163
|
// Set agent properties for buildEnv to use
|
|
2001
2164
|
if (connectOptions.sandboxId) {
|
|
2002
2165
|
this.agent.sandboxId = connectOptions.sandboxId;
|
|
@@ -2026,6 +2189,10 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2026
2189
|
} else {
|
|
2027
2190
|
this.agent.sandboxOs = this.os;
|
|
2028
2191
|
}
|
|
2192
|
+
// Use keepAlive from connectOptions if provided
|
|
2193
|
+
if (connectOptions.keepAlive !== undefined) {
|
|
2194
|
+
this.agent.keepAlive = connectOptions.keepAlive;
|
|
2195
|
+
}
|
|
2029
2196
|
|
|
2030
2197
|
// Set redrawThreshold on agent's cliArgs.options
|
|
2031
2198
|
this.agent.cliArgs.options.redrawThreshold = this.redrawThreshold;
|
|
@@ -2108,6 +2275,14 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2108
2275
|
return this.session?.get() || null;
|
|
2109
2276
|
}
|
|
2110
2277
|
|
|
2278
|
+
/**
|
|
2279
|
+
* Get the last sandbox info from the stored file
|
|
2280
|
+
* @returns {Object|null} Last sandbox info including sandboxId, os, ami, instanceType, timestamp, or null if not found
|
|
2281
|
+
*/
|
|
2282
|
+
getLastSandboxId() {
|
|
2283
|
+
return this.agent.getLastSandboxId();
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2111
2286
|
// ====================================
|
|
2112
2287
|
// Element Finding API
|
|
2113
2288
|
// ====================================
|
|
@@ -2188,10 +2363,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2188
2363
|
const absoluteTimestamp = Date.now();
|
|
2189
2364
|
const startTime = absoluteTimestamp;
|
|
2190
2365
|
|
|
2191
|
-
// Log finding all action
|
|
2192
2366
|
const { events } = require("./agent/events.js");
|
|
2193
|
-
const findingMessage = formatter.formatElementsFinding(description);
|
|
2194
|
-
this.emitter.emit(events.log.log, findingMessage);
|
|
2195
2367
|
|
|
2196
2368
|
try {
|
|
2197
2369
|
const screenshot = await this.system.captureScreenBase64();
|
|
@@ -2257,16 +2429,16 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2257
2429
|
const duration = Date.now() - startTime;
|
|
2258
2430
|
|
|
2259
2431
|
if (response && response.elements && response.elements.length > 0) {
|
|
2260
|
-
//
|
|
2261
|
-
const
|
|
2432
|
+
// Single log at the end - found elements
|
|
2433
|
+
const formattedMessage = formatter.formatFindAllSingleLine(
|
|
2262
2434
|
description,
|
|
2263
2435
|
response.elements.length,
|
|
2264
2436
|
{
|
|
2265
|
-
duration:
|
|
2437
|
+
duration: duration,
|
|
2266
2438
|
cacheHit: response.cached || false,
|
|
2267
2439
|
},
|
|
2268
2440
|
);
|
|
2269
|
-
this.emitter.emit(events.log.
|
|
2441
|
+
this.emitter.emit(events.log.narration, formattedMessage, true);
|
|
2270
2442
|
|
|
2271
2443
|
// Create Element instances for each found element
|
|
2272
2444
|
const elements = response.elements.map((elementData) => {
|
|
@@ -2316,7 +2488,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2316
2488
|
|
|
2317
2489
|
// Log debug information when elements are found
|
|
2318
2490
|
if (process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG) {
|
|
2319
|
-
const { events } = require("./agent/events.js");
|
|
2320
2491
|
this.emitter.emit(
|
|
2321
2492
|
events.log.debug,
|
|
2322
2493
|
`✓ Found ${elements.length} element(s): "${description}"`,
|
|
@@ -2330,6 +2501,19 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2330
2501
|
|
|
2331
2502
|
return elements;
|
|
2332
2503
|
} else {
|
|
2504
|
+
const duration = Date.now() - startTime;
|
|
2505
|
+
|
|
2506
|
+
// Single log at the end - no elements found
|
|
2507
|
+
const formattedMessage = formatter.formatFindAllSingleLine(
|
|
2508
|
+
description,
|
|
2509
|
+
0,
|
|
2510
|
+
{
|
|
2511
|
+
duration: duration,
|
|
2512
|
+
cacheHit: response?.cached || false,
|
|
2513
|
+
},
|
|
2514
|
+
);
|
|
2515
|
+
this.emitter.emit(events.log.narration, formattedMessage, true);
|
|
2516
|
+
|
|
2333
2517
|
// No elements found - track interaction (fire-and-forget, don't block)
|
|
2334
2518
|
const sessionId = this.getSessionId();
|
|
2335
2519
|
if (sessionId && this.sandbox?.send) {
|
|
@@ -2354,6 +2538,18 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2354
2538
|
return [];
|
|
2355
2539
|
}
|
|
2356
2540
|
} catch (error) {
|
|
2541
|
+
const duration = Date.now() - startTime;
|
|
2542
|
+
|
|
2543
|
+
// Single log at the end - error
|
|
2544
|
+
const formattedMessage = formatter.formatFindAllSingleLine(
|
|
2545
|
+
description,
|
|
2546
|
+
0,
|
|
2547
|
+
{
|
|
2548
|
+
duration: duration,
|
|
2549
|
+
},
|
|
2550
|
+
);
|
|
2551
|
+
this.emitter.emit(events.log.narration, formattedMessage, true);
|
|
2552
|
+
|
|
2357
2553
|
// Track findAll error interaction (fire-and-forget, don't block)
|
|
2358
2554
|
const sessionId = this.getSessionId();
|
|
2359
2555
|
if (sessionId && this.sandbox?.send) {
|
|
@@ -2371,8 +2567,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2371
2567
|
});
|
|
2372
2568
|
}
|
|
2373
2569
|
|
|
2374
|
-
const { events } = require("./agent/events.js");
|
|
2375
|
-
this.emitter.emit(events.log.log, `Error in findAll: ${error.message}`);
|
|
2376
2570
|
return [];
|
|
2377
2571
|
}
|
|
2378
2572
|
}
|
|
@@ -2771,6 +2965,11 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2771
2965
|
// handles forwarding to sandbox. This prevents duplicate output to server.
|
|
2772
2966
|
this.emitter.on("log:**", (message) => {
|
|
2773
2967
|
const event = this.emitter.event;
|
|
2968
|
+
|
|
2969
|
+
if (event.includes("markdown")) {
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2774
2973
|
if (event === events.log.debug && !debugMode) return;
|
|
2775
2974
|
if (this.loggingEnabled && message) {
|
|
2776
2975
|
const prefixedMessage = this.testContext
|
|
@@ -2920,39 +3119,11 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
2920
3119
|
const platform = options.platform || this.config.TD_PLATFORM || "linux";
|
|
2921
3120
|
|
|
2922
3121
|
// Auto-detect sandbox ID from the active sandbox if not provided
|
|
2923
|
-
|
|
3122
|
+
// For E2B (Linux), the instance has sandboxId; for AWS (Windows), it has instanceId
|
|
3123
|
+
const sandboxId = options.sandboxId || this.instance?.sandboxId || this.instance?.instanceId || this.agent?.sandboxId || null;
|
|
2924
3124
|
|
|
2925
3125
|
// Get or create session ID using the agent's newSession method
|
|
2926
3126
|
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
3127
|
|
|
2957
3128
|
const data = {
|
|
2958
3129
|
runId: options.runId,
|
|
@@ -3076,26 +3247,98 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
3076
3247
|
* This is the SDK equivalent of the CLI's exploratory loop
|
|
3077
3248
|
*
|
|
3078
3249
|
* @param {string} task - Natural language description of what to do
|
|
3079
|
-
* @param {Object} options - Execution options
|
|
3080
|
-
* @param {
|
|
3081
|
-
* @returns {Promise<
|
|
3250
|
+
* @param {Object} [options] - Execution options
|
|
3251
|
+
* @param {number} [options.tries=7] - Maximum number of check/retry attempts before giving up
|
|
3252
|
+
* @returns {Promise<ActResult>} Result object with success status and details
|
|
3253
|
+
* @throws {ActError} When the task fails after all tries are exhausted
|
|
3254
|
+
*
|
|
3255
|
+
* @typedef {Object} ActResult
|
|
3256
|
+
* @property {boolean} success - Whether the task completed successfully
|
|
3257
|
+
* @property {string} task - The original task that was executed
|
|
3258
|
+
* @property {number} tries - Number of check attempts made
|
|
3259
|
+
* @property {number} maxTries - Maximum tries that were allowed
|
|
3260
|
+
* @property {number} duration - Total execution time in milliseconds
|
|
3261
|
+
* @property {string} [response] - AI's final response if available
|
|
3082
3262
|
*
|
|
3083
3263
|
* @example
|
|
3084
3264
|
* // Simple execution
|
|
3085
|
-
* await client.act('Click the submit button');
|
|
3265
|
+
* const result = await client.act('Click the submit button');
|
|
3266
|
+
* console.log(result.success); // true
|
|
3086
3267
|
*
|
|
3087
3268
|
* @example
|
|
3088
|
-
* // With
|
|
3089
|
-
* const result = await client.act('Fill out the contact form', {
|
|
3090
|
-
* console.log(result);
|
|
3269
|
+
* // With custom retry limit
|
|
3270
|
+
* const result = await client.act('Fill out the contact form', { tries: 10 });
|
|
3271
|
+
* console.log(`Completed in ${result.tries} tries`);
|
|
3272
|
+
*
|
|
3273
|
+
* @example
|
|
3274
|
+
* // Handle failures
|
|
3275
|
+
* try {
|
|
3276
|
+
* await client.act('Complete the checkout process', { tries: 3 });
|
|
3277
|
+
* } catch (error) {
|
|
3278
|
+
* console.log(`Failed after ${error.tries} tries: ${error.message}`);
|
|
3279
|
+
* }
|
|
3091
3280
|
*/
|
|
3092
|
-
async act(task) {
|
|
3281
|
+
async act(task, options = {}) {
|
|
3093
3282
|
this._ensureConnected();
|
|
3094
3283
|
|
|
3095
|
-
|
|
3284
|
+
const { tries = 7 } = options;
|
|
3285
|
+
|
|
3286
|
+
this.analytics.track("sdk.act", { task, tries });
|
|
3096
3287
|
|
|
3097
|
-
|
|
3098
|
-
|
|
3288
|
+
const { events } = require("./agent/events.js");
|
|
3289
|
+
const startTime = Date.now();
|
|
3290
|
+
|
|
3291
|
+
// Store original checkLimit and set custom one if provided
|
|
3292
|
+
const originalCheckLimit = this.agent.checkLimit;
|
|
3293
|
+
this.agent.checkLimit = tries;
|
|
3294
|
+
|
|
3295
|
+
// Reset check count for this act() call
|
|
3296
|
+
const originalCheckCount = this.agent.checkCount;
|
|
3297
|
+
this.agent.checkCount = 0;
|
|
3298
|
+
|
|
3299
|
+
// Emit scoped start marker for act()
|
|
3300
|
+
this.emitter.emit(events.log.log, formatter.formatActStart(task));
|
|
3301
|
+
|
|
3302
|
+
try {
|
|
3303
|
+
// Use the agent's exploratoryLoop method directly
|
|
3304
|
+
const response = await this.agent.exploratoryLoop(task, false, true, false);
|
|
3305
|
+
|
|
3306
|
+
const duration = Date.now() - startTime;
|
|
3307
|
+
const triesUsed = this.agent.checkCount;
|
|
3308
|
+
|
|
3309
|
+
this.emitter.emit(events.log.log, formatter.formatActComplete(duration, true));
|
|
3310
|
+
|
|
3311
|
+
// Restore original checkLimit
|
|
3312
|
+
this.agent.checkLimit = originalCheckLimit;
|
|
3313
|
+
this.agent.checkCount = originalCheckCount;
|
|
3314
|
+
|
|
3315
|
+
return {
|
|
3316
|
+
success: true,
|
|
3317
|
+
task,
|
|
3318
|
+
tries: triesUsed,
|
|
3319
|
+
maxTries: tries,
|
|
3320
|
+
duration,
|
|
3321
|
+
response: response || undefined,
|
|
3322
|
+
};
|
|
3323
|
+
} catch (error) {
|
|
3324
|
+
const duration = Date.now() - startTime;
|
|
3325
|
+
const triesUsed = this.agent.checkCount;
|
|
3326
|
+
|
|
3327
|
+
this.emitter.emit(events.log.log, formatter.formatActComplete(duration, false, error.message));
|
|
3328
|
+
|
|
3329
|
+
// Restore original checkLimit
|
|
3330
|
+
this.agent.checkLimit = originalCheckLimit;
|
|
3331
|
+
this.agent.checkCount = originalCheckCount;
|
|
3332
|
+
|
|
3333
|
+
// Create an enhanced error with additional context using ActError class
|
|
3334
|
+
throw new ActError(`Act failed: ${error.message}`, {
|
|
3335
|
+
task,
|
|
3336
|
+
tries: triesUsed,
|
|
3337
|
+
maxTries: tries,
|
|
3338
|
+
duration,
|
|
3339
|
+
cause: error,
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3099
3342
|
}
|
|
3100
3343
|
|
|
3101
3344
|
/**
|
|
@@ -3103,15 +3346,16 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
|
3103
3346
|
* Execute a natural language task using AI
|
|
3104
3347
|
*
|
|
3105
3348
|
* @param {string} task - Natural language description of what to do
|
|
3106
|
-
* @param {Object} options - Execution options
|
|
3107
|
-
* @param {
|
|
3108
|
-
* @returns {Promise<
|
|
3349
|
+
* @param {Object} [options] - Execution options
|
|
3350
|
+
* @param {number} [options.tries=7] - Maximum number of check/retry attempts
|
|
3351
|
+
* @returns {Promise<ActResult>} Result object with success status and details
|
|
3109
3352
|
*/
|
|
3110
|
-
async ai(task) {
|
|
3111
|
-
return await this.act(task);
|
|
3353
|
+
async ai(task, options) {
|
|
3354
|
+
return await this.act(task, options);
|
|
3112
3355
|
}
|
|
3113
3356
|
}
|
|
3114
3357
|
|
|
3115
3358
|
module.exports = TestDriverSDK;
|
|
3116
3359
|
module.exports.Element = Element;
|
|
3117
3360
|
module.exports.ElementNotFoundError = ElementNotFoundError;
|
|
3361
|
+
module.exports.ActError = ActError;
|