testdriverai 7.2.2 → 7.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  */
@@ -1702,26 +1744,49 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1702
1744
  * @returns {Promise<void>}
1703
1745
  */
1704
1746
  vscode: async (options = {}) => {
1705
- this._ensureConnected();
1747
+ // Automatically wait for connection to be ready
1748
+ await this.ready();
1706
1749
 
1707
1750
  const {
1708
1751
  workspace = null,
1709
1752
  extensions = [],
1710
1753
  } = options;
1711
1754
 
1755
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1756
+
1757
+ // If dashcam is available, set up file logging
1758
+ if (this._dashcam) {
1759
+ // Create the log file on the remote machine
1760
+ const logPath = this.os === "windows"
1761
+ ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
1762
+ : "/tmp/testdriver.log";
1763
+
1764
+ const createLogCmd = this.os === "windows"
1765
+ ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
1766
+ : `touch ${logPath}`;
1767
+
1768
+ await this.exec(shell, createLogCmd, 10000, true);
1769
+ await this._dashcam.addFileLog(logPath, "TestDriver Log");
1770
+ }
1771
+
1772
+ // Automatically start dashcam if not already recording
1773
+ if (!this._dashcam || !this._dashcam.recording) {
1774
+ await this.dashcam.start();
1775
+ }
1776
+
1712
1777
  // Install extensions if provided
1713
1778
  for (const extension of extensions) {
1714
- const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1779
+ console.log(`[provision.vscode] Installing extension: ${extension}`);
1715
1780
  await this.exec(
1716
1781
  shell,
1717
- `code --install-extension ${extension}`,
1718
- 60000,
1782
+ `code --install-extension ${extension} --force`,
1783
+ 120000,
1719
1784
  true
1720
1785
  );
1786
+ console.log(`[provision.vscode] ✅ Extension installed: ${extension}`);
1721
1787
  }
1722
1788
 
1723
1789
  // Launch VS Code
1724
- const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1725
1790
  const workspaceArg = workspace ? `"${workspace}"` : '';
1726
1791
 
1727
1792
  if (this.os === 'windows') {
@@ -1738,10 +1803,148 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
1738
1803
  );
1739
1804
  }
1740
1805
 
1806
+ // Wait for VS Code to start up
1807
+ await new Promise(resolve => setTimeout(resolve, 3000));
1808
+
1741
1809
  // Wait for VS Code to be ready
1742
1810
  await this.focusApplication('Visual Studio Code');
1743
1811
  },
1744
1812
 
1813
+ /**
1814
+ * Download and install an application
1815
+ * @param {Object} options - Installer options
1816
+ * @param {string} options.url - URL to download the installer from
1817
+ * @param {string} [options.filename] - Filename to save as (auto-detected from URL if not provided)
1818
+ * @param {string} [options.appName] - Application name to focus after install
1819
+ * @param {boolean} [options.launch=true] - Whether to launch the app after installation
1820
+ * @returns {Promise<string>} Path to the downloaded file
1821
+ * @example
1822
+ * // Install a .deb package on Linux (auto-detected)
1823
+ * await testdriver.provision.installer({
1824
+ * url: 'https://example.com/app.deb',
1825
+ * appName: 'MyApp'
1826
+ * });
1827
+ *
1828
+ * @example
1829
+ * // Download and run custom commands
1830
+ * const filePath = await testdriver.provision.installer({
1831
+ * url: 'https://example.com/app.AppImage',
1832
+ * launch: false
1833
+ * });
1834
+ * await testdriver.exec('sh', `chmod +x "${filePath}" && "${filePath}" &`, 10000);
1835
+ */
1836
+ installer: async (options = {}) => {
1837
+ // Automatically wait for connection to be ready
1838
+ await this.ready();
1839
+
1840
+ const {
1841
+ url,
1842
+ filename,
1843
+ appName,
1844
+ launch = true,
1845
+ } = options;
1846
+
1847
+ if (!url) {
1848
+ throw new Error('[provision.installer] url is required');
1849
+ }
1850
+
1851
+ const shell = this.os === 'windows' ? 'pwsh' : 'sh';
1852
+
1853
+ // If dashcam is available, set up file logging
1854
+ if (this._dashcam) {
1855
+ const logPath = this.os === "windows"
1856
+ ? "C:\\Users\\testdriver\\Documents\\testdriver.log"
1857
+ : "/tmp/testdriver.log";
1858
+
1859
+ const createLogCmd = this.os === "windows"
1860
+ ? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
1861
+ : `touch ${logPath}`;
1862
+
1863
+ await this.exec(shell, createLogCmd, 10000, true);
1864
+ await this._dashcam.addFileLog(logPath, "TestDriver Log");
1865
+ }
1866
+
1867
+ // Automatically start dashcam if not already recording
1868
+ if (!this._dashcam || !this._dashcam.recording) {
1869
+ await this.dashcam.start();
1870
+ }
1871
+
1872
+ // Determine filename from URL if not provided
1873
+ const urlObj = new URL(url);
1874
+ const detectedFilename = filename || urlObj.pathname.split('/').pop() || 'installer';
1875
+
1876
+ // Determine download directory and full path
1877
+ const downloadDir = this.os === 'windows'
1878
+ ? 'C:\\Users\\testdriver\\Downloads'
1879
+ : '/tmp';
1880
+ const filePath = this.os === 'windows'
1881
+ ? `${downloadDir}\\${detectedFilename}`
1882
+ : `${downloadDir}/${detectedFilename}`;
1883
+
1884
+ console.log(`[provision.installer] Downloading ${url}...`);
1885
+
1886
+ // Download the file
1887
+ if (this.os === 'windows') {
1888
+ await this.exec(
1889
+ shell,
1890
+ `Invoke-WebRequest -Uri "${url}" -OutFile "${filePath}"`,
1891
+ 300000, // 5 min timeout for download
1892
+ true
1893
+ );
1894
+ } else {
1895
+ await this.exec(
1896
+ shell,
1897
+ `curl -L -o "${filePath}" "${url}"`,
1898
+ 300000,
1899
+ true
1900
+ );
1901
+ }
1902
+
1903
+ console.log(`[provision.installer] ✅ Downloaded to ${filePath}`);
1904
+
1905
+ // Auto-detect install command based on file extension
1906
+ const ext = detectedFilename.split('.').pop()?.toLowerCase();
1907
+ let installCommand = null;
1908
+
1909
+ if (this.os === 'windows') {
1910
+ if (ext === 'msi') {
1911
+ installCommand = `Start-Process msiexec -ArgumentList '/i', '"${filePath}"', '/quiet', '/norestart' -Wait`;
1912
+ } else if (ext === 'exe') {
1913
+ installCommand = `Start-Process "${filePath}" -ArgumentList '/S' -Wait`;
1914
+ }
1915
+ } else if (this.os === 'linux') {
1916
+ if (ext === 'deb') {
1917
+ installCommand = `sudo dpkg -i "${filePath}" && sudo apt-get install -f -y`;
1918
+ } else if (ext === 'rpm') {
1919
+ installCommand = `sudo rpm -i "${filePath}"`;
1920
+ } else if (ext === 'appimage') {
1921
+ installCommand = `chmod +x "${filePath}"`;
1922
+ } else if (ext === 'sh') {
1923
+ installCommand = `chmod +x "${filePath}" && "${filePath}"`;
1924
+ }
1925
+ } else if (this.os === 'darwin') {
1926
+ if (ext === 'dmg') {
1927
+ installCommand = `hdiutil attach "${filePath}" -mountpoint /Volumes/installer && cp -R /Volumes/installer/*.app /Applications/ && hdiutil detach /Volumes/installer`;
1928
+ } else if (ext === 'pkg') {
1929
+ installCommand = `sudo installer -pkg "${filePath}" -target /`;
1930
+ }
1931
+ }
1932
+
1933
+ if (installCommand) {
1934
+ console.log(`[provision.installer] Installing...`);
1935
+ await this.exec(shell, installCommand, 300000, true);
1936
+ console.log(`[provision.installer] ✅ Installation complete`);
1937
+ }
1938
+
1939
+ // Launch and focus the app if appName is provided and launch is true
1940
+ if (appName && launch) {
1941
+ await new Promise(resolve => setTimeout(resolve, 2000));
1942
+ await this.focusApplication(appName);
1943
+ }
1944
+
1945
+ return filePath;
1946
+ },
1947
+
1745
1948
  /**
1746
1949
  * Launch Electron app
1747
1950
  * @param {Object} options - Electron launch options
@@ -2027,10 +2230,7 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2027
2230
  const absoluteTimestamp = Date.now();
2028
2231
  const startTime = absoluteTimestamp;
2029
2232
 
2030
- // Log finding all action
2031
2233
  const { events } = require("./agent/events.js");
2032
- const findingMessage = formatter.formatElementsFinding(description);
2033
- this.emitter.emit(events.log.log, findingMessage);
2034
2234
 
2035
2235
  try {
2036
2236
  const screenshot = await this.system.captureScreenBase64();
@@ -2096,16 +2296,16 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2096
2296
  const duration = Date.now() - startTime;
2097
2297
 
2098
2298
  if (response && response.elements && response.elements.length > 0) {
2099
- // Log found elements
2100
- const foundMessage = formatter.formatElementsFound(
2299
+ // Single log at the end - found elements
2300
+ const formattedMessage = formatter.formatFindAllSingleLine(
2101
2301
  description,
2102
2302
  response.elements.length,
2103
2303
  {
2104
- duration: `${duration}ms`,
2304
+ duration: duration,
2105
2305
  cacheHit: response.cached || false,
2106
2306
  },
2107
2307
  );
2108
- this.emitter.emit(events.log.log, foundMessage);
2308
+ this.emitter.emit(events.log.narration, formattedMessage, true);
2109
2309
 
2110
2310
  // Create Element instances for each found element
2111
2311
  const elements = response.elements.map((elementData) => {
@@ -2155,7 +2355,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2155
2355
 
2156
2356
  // Log debug information when elements are found
2157
2357
  if (process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG) {
2158
- const { events } = require("./agent/events.js");
2159
2358
  this.emitter.emit(
2160
2359
  events.log.debug,
2161
2360
  `✓ Found ${elements.length} element(s): "${description}"`,
@@ -2169,6 +2368,19 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2169
2368
 
2170
2369
  return elements;
2171
2370
  } else {
2371
+ const duration = Date.now() - startTime;
2372
+
2373
+ // Single log at the end - no elements found
2374
+ const formattedMessage = formatter.formatFindAllSingleLine(
2375
+ description,
2376
+ 0,
2377
+ {
2378
+ duration: duration,
2379
+ cacheHit: response?.cached || false,
2380
+ },
2381
+ );
2382
+ this.emitter.emit(events.log.narration, formattedMessage, true);
2383
+
2172
2384
  // No elements found - track interaction (fire-and-forget, don't block)
2173
2385
  const sessionId = this.getSessionId();
2174
2386
  if (sessionId && this.sandbox?.send) {
@@ -2193,6 +2405,18 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2193
2405
  return [];
2194
2406
  }
2195
2407
  } catch (error) {
2408
+ const duration = Date.now() - startTime;
2409
+
2410
+ // Single log at the end - error
2411
+ const formattedMessage = formatter.formatFindAllSingleLine(
2412
+ description,
2413
+ 0,
2414
+ {
2415
+ duration: duration,
2416
+ },
2417
+ );
2418
+ this.emitter.emit(events.log.narration, formattedMessage, true);
2419
+
2196
2420
  // Track findAll error interaction (fire-and-forget, don't block)
2197
2421
  const sessionId = this.getSessionId();
2198
2422
  if (sessionId && this.sandbox?.send) {
@@ -2210,8 +2434,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2210
2434
  });
2211
2435
  }
2212
2436
 
2213
- const { events } = require("./agent/events.js");
2214
- this.emitter.emit(events.log.log, `Error in findAll: ${error.message}`);
2215
2437
  return [];
2216
2438
  }
2217
2439
  }
@@ -2606,17 +2828,21 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2606
2828
  createMarkdownLogger(this.emitter);
2607
2829
 
2608
2830
  // Set up basic event logging
2831
+ // Note: We only console.log here - the console spy in vitest/hooks.mjs
2832
+ // handles forwarding to sandbox. This prevents duplicate output to server.
2609
2833
  this.emitter.on("log:**", (message) => {
2610
2834
  const event = this.emitter.event;
2835
+
2836
+ if (event.includes("markdown")) {
2837
+ return;
2838
+ }
2839
+
2611
2840
  if (event === events.log.debug && !debugMode) return;
2612
2841
  if (this.loggingEnabled && message) {
2613
2842
  const prefixedMessage = this.testContext
2614
2843
  ? `[${this.testContext}] ${message}`
2615
2844
  : message;
2616
2845
  console.log(prefixedMessage);
2617
-
2618
- // Also forward to sandbox for dashcam
2619
- this._forwardLogToSandbox(prefixedMessage);
2620
2846
  }
2621
2847
  });
2622
2848
 
@@ -2675,36 +2901,6 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2675
2901
  });
2676
2902
  }
2677
2903
 
2678
- /**
2679
- * Forward log message to sandbox for debugger display
2680
- * @private
2681
- * @param {string} message - Log message to forward
2682
- */
2683
- _forwardLogToSandbox(message) {
2684
- try {
2685
- // Only forward if sandbox is connected
2686
- if (this.sandbox && this.sandbox.instanceSocketConnected) {
2687
- // Don't send objects as they cause base64 encoding errors
2688
- if (typeof message === "object") {
2689
- return;
2690
- }
2691
-
2692
- // Add test context prefix if available
2693
- const prefixedMessage = this.testContext
2694
- ? `[${this.testContext}] ${message}`
2695
- : message;
2696
-
2697
- this.sandbox.send({
2698
- type: "output",
2699
- output: Buffer.from(prefixedMessage).toString("base64"),
2700
- });
2701
- }
2702
- } catch {
2703
- // Silently fail to avoid breaking the log flow
2704
- // console.error("Error forwarding log to sandbox:", error);
2705
- }
2706
- }
2707
-
2708
2904
  /**
2709
2905
  * Open URL in default browser
2710
2906
  * @private
@@ -2790,39 +2986,11 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2790
2986
  const platform = options.platform || this.config.TD_PLATFORM || "linux";
2791
2987
 
2792
2988
  // Auto-detect sandbox ID from the active sandbox if not provided
2793
- const sandboxId = options.sandboxId || this.agent?.sandbox?.id || null;
2989
+ // For E2B (Linux), the instance has sandboxId; for AWS (Windows), it has instanceId
2990
+ const sandboxId = options.sandboxId || this.instance?.sandboxId || this.instance?.instanceId || this.agent?.sandboxId || null;
2794
2991
 
2795
2992
  // Get or create session ID using the agent's newSession method
2796
2993
  let sessionId = this.agent?.sessionInstance?.get() || null;
2797
-
2798
- // If no session exists, create one using the agent's method
2799
- if (!sessionId && this.agent?.newSession) {
2800
- try {
2801
- await this.agent.newSession();
2802
- sessionId = this.agent.sessionInstance.get();
2803
-
2804
- // Save session ID to file for reuse across test runs
2805
- if (sessionId) {
2806
- const sessionFile = path.join(os.homedir(), '.testdriverai-session');
2807
- fs.writeFileSync(sessionFile, sessionId, { encoding: 'utf-8' });
2808
- }
2809
- } catch (error) {
2810
- // Log but don't fail - tests can run without a session
2811
- console.warn('Failed to create session:', error.message);
2812
- }
2813
- }
2814
-
2815
- // If still no session, try reading from file (for reporter/separate processes)
2816
- if (!sessionId) {
2817
- try {
2818
- const sessionFile = path.join(os.homedir(), '.testdriverai-session');
2819
- if (fs.existsSync(sessionFile)) {
2820
- sessionId = fs.readFileSync(sessionFile, 'utf-8').trim();
2821
- }
2822
- } catch (error) {
2823
- // Ignore file read errors
2824
- }
2825
- }
2826
2994
 
2827
2995
  const data = {
2828
2996
  runId: options.runId,
@@ -2946,26 +3114,98 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2946
3114
  * This is the SDK equivalent of the CLI's exploratory loop
2947
3115
  *
2948
3116
  * @param {string} task - Natural language description of what to do
2949
- * @param {Object} options - Execution options
2950
- * @param {boolean} [options.validateAndLoop=false] - Whether to validate completion and retry if incomplete
2951
- * @returns {Promise<string|void>} Final AI response if validateAndLoop is true
3117
+ * @param {Object} [options] - Execution options
3118
+ * @param {number} [options.tries=7] - Maximum number of check/retry attempts before giving up
3119
+ * @returns {Promise<ActResult>} Result object with success status and details
3120
+ * @throws {ActError} When the task fails after all tries are exhausted
3121
+ *
3122
+ * @typedef {Object} ActResult
3123
+ * @property {boolean} success - Whether the task completed successfully
3124
+ * @property {string} task - The original task that was executed
3125
+ * @property {number} tries - Number of check attempts made
3126
+ * @property {number} maxTries - Maximum tries that were allowed
3127
+ * @property {number} duration - Total execution time in milliseconds
3128
+ * @property {string} [response] - AI's final response if available
2952
3129
  *
2953
3130
  * @example
2954
3131
  * // Simple execution
2955
- * await client.act('Click the submit button');
3132
+ * const result = await client.act('Click the submit button');
3133
+ * console.log(result.success); // true
3134
+ *
3135
+ * @example
3136
+ * // With custom retry limit
3137
+ * const result = await client.act('Fill out the contact form', { tries: 10 });
3138
+ * console.log(`Completed in ${result.tries} tries`);
2956
3139
  *
2957
3140
  * @example
2958
- * // With validation loop
2959
- * const result = await client.act('Fill out the contact form', { validateAndLoop: true });
2960
- * console.log(result); // AI's final assessment
3141
+ * // Handle failures
3142
+ * try {
3143
+ * await client.act('Complete the checkout process', { tries: 3 });
3144
+ * } catch (error) {
3145
+ * console.log(`Failed after ${error.tries} tries: ${error.message}`);
3146
+ * }
2961
3147
  */
2962
- async act(task) {
3148
+ async act(task, options = {}) {
2963
3149
  this._ensureConnected();
2964
3150
 
2965
- this.analytics.track("sdk.act", { task });
3151
+ const { tries = 7 } = options;
3152
+
3153
+ this.analytics.track("sdk.act", { task, tries });
2966
3154
 
2967
- // Use the agent's exploratoryLoop method directly
2968
- return await this.agent.exploratoryLoop(task, false, true, false);
3155
+ const { events } = require("./agent/events.js");
3156
+ const startTime = Date.now();
3157
+
3158
+ // Store original checkLimit and set custom one if provided
3159
+ const originalCheckLimit = this.agent.checkLimit;
3160
+ this.agent.checkLimit = tries;
3161
+
3162
+ // Reset check count for this act() call
3163
+ const originalCheckCount = this.agent.checkCount;
3164
+ this.agent.checkCount = 0;
3165
+
3166
+ // Emit scoped start marker for act()
3167
+ this.emitter.emit(events.log.log, formatter.formatActStart(task));
3168
+
3169
+ try {
3170
+ // Use the agent's exploratoryLoop method directly
3171
+ const response = await this.agent.exploratoryLoop(task, false, true, false);
3172
+
3173
+ const duration = Date.now() - startTime;
3174
+ const triesUsed = this.agent.checkCount;
3175
+
3176
+ this.emitter.emit(events.log.log, formatter.formatActComplete(duration, true));
3177
+
3178
+ // Restore original checkLimit
3179
+ this.agent.checkLimit = originalCheckLimit;
3180
+ this.agent.checkCount = originalCheckCount;
3181
+
3182
+ return {
3183
+ success: true,
3184
+ task,
3185
+ tries: triesUsed,
3186
+ maxTries: tries,
3187
+ duration,
3188
+ response: response || undefined,
3189
+ };
3190
+ } catch (error) {
3191
+ const duration = Date.now() - startTime;
3192
+ const triesUsed = this.agent.checkCount;
3193
+
3194
+ this.emitter.emit(events.log.log, formatter.formatActComplete(duration, false, error.message));
3195
+
3196
+ // Restore original checkLimit
3197
+ this.agent.checkLimit = originalCheckLimit;
3198
+ this.agent.checkCount = originalCheckCount;
3199
+
3200
+ // Create an enhanced error with additional context using ActError class
3201
+ throw new ActError(`Act failed: ${error.message}`, {
3202
+ task,
3203
+ tries: triesUsed,
3204
+ maxTries: tries,
3205
+ duration,
3206
+ cause: error,
3207
+ });
3208
+ }
2969
3209
  }
2970
3210
 
2971
3211
  /**
@@ -2973,15 +3213,16 @@ with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2973
3213
  * Execute a natural language task using AI
2974
3214
  *
2975
3215
  * @param {string} task - Natural language description of what to do
2976
- * @param {Object} options - Execution options
2977
- * @param {boolean} [options.validateAndLoop=false] - Whether to validate completion and retry if incomplete
2978
- * @returns {Promise<string|void>} Final AI response if validateAndLoop is true
3216
+ * @param {Object} [options] - Execution options
3217
+ * @param {number} [options.tries=7] - Maximum number of check/retry attempts
3218
+ * @returns {Promise<ActResult>} Result object with success status and details
2979
3219
  */
2980
- async ai(task) {
2981
- return await this.act(task);
3220
+ async ai(task, options) {
3221
+ return await this.act(task, options);
2982
3222
  }
2983
3223
  }
2984
3224
 
2985
3225
  module.exports = TestDriverSDK;
2986
3226
  module.exports.Element = Element;
2987
3227
  module.exports.ElementNotFoundError = ElementNotFoundError;
3228
+ module.exports.ActError = ActError;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * TestDriver SDK - Act Test (Vitest)
3
+ * Tests the AI exploratory loop (act) functionality
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import { TestDriver } from "../../lib/vitest/hooks.mjs";
8
+
9
+ describe("Act Test", () => {
10
+ it("should use act to search for testdriver on Google", async (context) => {
11
+ const testdriver = TestDriver(context, { newSandbox: true });
12
+
13
+ // provision.chrome() automatically calls ready() and starts dashcam
14
+ await testdriver.provision.chrome({
15
+ url: 'https://www.google.com',
16
+ });
17
+
18
+ // Use act to search for testdriver
19
+ let actRes = await testdriver.act("click on the empty search box, type 'testdriver', and hit enter. do not click the plus button in the search bar");
20
+
21
+ console.log("Act response:", actRes);
22
+
23
+ // Assert the search results are displayed
24
+ const result = await testdriver.assert(
25
+ "search results for testdriver are visible",
26
+ );
27
+
28
+ expect(result).toBeTruthy();
29
+ });
30
+ });
@@ -8,7 +8,7 @@ import { TestDriver } from "../../lib/vitest/hooks.mjs";
8
8
 
9
9
  describe("Assert Test", () => {
10
10
  it("should assert the testdriver login page shows", async (context) => {
11
- const testdriver = TestDriver(context, { newSandbox: true });
11
+ const testdriver = TestDriver(context, { newSandbox: true, headless: false });
12
12
 
13
13
  // provision.chrome() automatically calls ready() and starts dashcam
14
14
  await testdriver.provision.chrome({
@@ -8,7 +8,7 @@ import { TestDriver } from "../../lib/vitest/hooks.mjs";
8
8
 
9
9
  describe("Hover Text Test", () => {
10
10
  it("should click Sign In and verify error message", async (context) => {
11
- const testdriver = TestDriver(context, { headless: true, newSandbox: true, cacheKey: 'hover-text-test' });
11
+ const testdriver = TestDriver(context, { headless: false, newSandbox: true, cacheKey: 'hover-text-test' });
12
12
  await testdriver.provision.chrome({ url: 'http://testdriver-sandbox.vercel.app/login' });
13
13
 
14
14
  // Click on Sign In button using new find() API
@@ -0,0 +1,47 @@
1
+ /**
2
+ * TestDriver SDK - Installer Test (Vitest)
3
+ * Tests the provision.installer() method for downloading and installing apps
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import { TestDriver } from "../../lib/vitest/hooks.mjs";
8
+
9
+ describe("Provision Installer", () => {
10
+ it(
11
+ "should download and install a .deb package on Linux",
12
+ async (context) => {
13
+ const testdriver = TestDriver(context, { newSandbox: true });
14
+
15
+ // Install bat (a cat clone with syntax highlighting) using provision.installer
16
+ const filePath = await testdriver.provision.installer({
17
+ url: 'https://github.com/sharkdp/bat/releases/download/v0.24.0/bat_0.24.0_amd64.deb',
18
+ });
19
+
20
+ // Verify the file was downloaded
21
+ expect(filePath).toContain('bat');
22
+
23
+ // Verify bat was installed by running it
24
+ await testdriver.exec('sh', 'bat --version', 10000);
25
+ },
26
+ );
27
+
28
+ it(
29
+ "should download a shell script and verify it exists",
30
+ async (context) => {
31
+ const testdriver = TestDriver(context, { newSandbox: true });
32
+
33
+ // Download a shell script (nvm installer)
34
+ const filePath = await testdriver.provision.installer({
35
+ url: 'https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh',
36
+ launch: false, // Don't auto-run the script
37
+ });
38
+
39
+ // Verify the file was downloaded
40
+ expect(filePath).toContain('install.sh');
41
+
42
+ // Verify the file is executable
43
+ const result = await testdriver.exec('sh', `ls -la "${filePath}"`, 10000);
44
+ expect(result).toBeTruthy();
45
+ },
46
+ );
47
+ });