testdriverai 7.9.81-test → 7.9.91-test

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
@@ -3,6 +3,7 @@ const path = require("path");
3
3
  const os = require("os");
4
4
  const crypto = require("crypto");
5
5
  const { formatter } = require("./sdk-log-formatter");
6
+ const { createProvisionAPI } = require("./lib/provision");
6
7
 
7
8
  // Load .env — use monorepo root .env when running inside the monorepo,
8
9
  // otherwise fall back to default dotenv.config() for end users.
@@ -1798,733 +1799,7 @@ class TestDriverSDK {
1798
1799
  }
1799
1800
 
1800
1801
  _createProvisionAPI() {
1801
- const self = this;
1802
-
1803
- const provisionMethods = {
1804
- /**
1805
- * Launch Chrome browser
1806
- * @param {Object} options - Chrome launch options
1807
- * @param {string} [options.url='http://testdriver-sandbox.vercel.app/'] - URL to navigate to
1808
- * @param {boolean} [options.maximized=true] - Start maximized
1809
- * @param {boolean} [options.guest=false] - Use guest mode
1810
- * @returns {Promise<void>}
1811
- */
1812
- chrome: async (options = {}) => {
1813
- const {
1814
- url = "http://testdriver-sandbox.vercel.app/",
1815
- maximized = true,
1816
- guest = false,
1817
- } = options;
1818
-
1819
- // Store the URL for domain-specific web log tracking
1820
- self._provisionedChromeUrl = url;
1821
-
1822
- // Set up Chrome profile with preferences
1823
- const shell = this.os === "windows" ? "pwsh" : "sh";
1824
- const userDataDir =
1825
- this.os === "windows"
1826
- ? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
1827
- : "/tmp/testdriver-chrome-profile";
1828
-
1829
- // Create user data directory and Default profile directory
1830
- const defaultProfileDir =
1831
- this.os === "windows"
1832
- ? `${userDataDir}\\Default`
1833
- : `${userDataDir}/Default`;
1834
-
1835
- const createDirCmd =
1836
- this.os === "windows"
1837
- ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
1838
- : `mkdir -p "${defaultProfileDir}"`;
1839
-
1840
- await this.exec(shell, createDirCmd, 60000, true);
1841
-
1842
- // Write Chrome preferences
1843
- const chromePrefs = {
1844
- credentials_enable_service: false,
1845
- profile: {
1846
- password_manager_enabled: false,
1847
- default_content_setting_values: {},
1848
- },
1849
- signin: {
1850
- allowed: false,
1851
- },
1852
- sync: {
1853
- requested: false,
1854
- first_setup_complete: true,
1855
- sync_all_os_types: false,
1856
- },
1857
- autofill: {
1858
- enabled: false,
1859
- },
1860
- local_state: {
1861
- browser: {
1862
- has_seen_welcome_page: true,
1863
- },
1864
- },
1865
- };
1866
-
1867
- const prefsPath =
1868
- this.os === "windows"
1869
- ? `${defaultProfileDir}\\Preferences`
1870
- : `${defaultProfileDir}/Preferences`;
1871
-
1872
- const prefsJson = JSON.stringify(chromePrefs, null, 2);
1873
- const writePrefCmd =
1874
- this.os === "windows"
1875
- ? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
1876
- `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
1877
- : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
1878
-
1879
- await this.exec(shell, writePrefCmd, 60000, true);
1880
-
1881
- // Build Chrome launch command
1882
- const chromeArgs = [];
1883
- if (maximized) chromeArgs.push("--start-maximized");
1884
- if (guest) chromeArgs.push("--guest");
1885
- chromeArgs.push(
1886
- "--disable-fre",
1887
- "--no-default-browser-check",
1888
- "--no-first-run",
1889
- "--no-experiments",
1890
- "--disable-infobars",
1891
- "--disable-features=StartupBrowserCreator",
1892
- "--disable-features=ChromeWhatsNewUI",
1893
- `--user-data-dir=${userDataDir}`,
1894
- );
1895
-
1896
- // Add remote debugging port for captcha solving support
1897
- chromeArgs.push("--remote-debugging-port=9222");
1898
-
1899
- // Add dashcam-chrome extension
1900
- const dashcamChromePath = await this._getDashcamChromeExtensionPath();
1901
- if (dashcamChromePath) {
1902
- chromeArgs.push(`--load-extension=${dashcamChromePath}`);
1903
- }
1904
-
1905
- // Launch Chrome
1906
-
1907
- if (this.os === "windows") {
1908
- const argsString = chromeArgs.map((arg) => `"${arg}"`).join(", ");
1909
- await this.exec(
1910
- shell,
1911
- `Start-Process "C:\\ChromeForTesting\\chrome-win64\\chrome.exe" -ArgumentList ${argsString}, "${url}"`,
1912
- 30000,
1913
- );
1914
- } else {
1915
- const argsString = chromeArgs.join(" ");
1916
- await this.exec(
1917
- shell,
1918
- `chrome-for-testing ${argsString} "${url}" >/dev/null 2>&1 &`,
1919
- 30000,
1920
- );
1921
- }
1922
-
1923
- // Wait for Chrome debugger port and page to be ready
1924
- await this._waitForChromeDebuggerReady();
1925
- await this.focusApplication("Google Chrome");
1926
-
1927
- // Add web log tracking with domain wildcard pattern, then start dashcam
1928
- if (this.dashcamEnabled) {
1929
- const domainPattern = this._getUrlDomainPattern(url);
1930
- await this.dashcam.addWebLog(domainPattern, "Web Logs");
1931
-
1932
- // Start dashcam recording after logs are configured
1933
- if (!(await this.dashcam.isRecording())) {
1934
- await this.dashcam.start();
1935
- }
1936
- }
1937
- },
1938
-
1939
- /**
1940
- * Launch Chrome browser with a custom extension loaded
1941
- * @param {Object} options - Chrome extension launch options
1942
- * @param {string} [options.extensionPath] - Local filesystem path to the unpacked extension directory
1943
- * @param {string} [options.extensionId] - Chrome Web Store extension ID (e.g., "cjpalhdlnbpafiamejdnhcphjbkeiagm" for uBlock Origin)
1944
- * @param {boolean} [options.maximized=true] - Start maximized
1945
- * @returns {Promise<void>}
1946
- * @example
1947
- * // Load extension from local path
1948
- * await testdriver.exec('sh', 'git clone https://github.com/user/extension.git /tmp/extension');
1949
- * await testdriver.provision.chromeExtension({
1950
- * extensionPath: '/tmp/extension'
1951
- * });
1952
- *
1953
- * @example
1954
- * // Load extension by Chrome Web Store ID
1955
- * await testdriver.provision.chromeExtension({
1956
- * extensionId: 'cjpalhdlnbpafiamejdnhcphjbkeiagm' // uBlock Origin
1957
- * });
1958
- */
1959
- chromeExtension: async (options = {}) => {
1960
- const {
1961
- extensionPath: providedExtensionPath,
1962
- extensionId,
1963
- maximized = true,
1964
- } = options;
1965
-
1966
- if (!providedExtensionPath && !extensionId) {
1967
- throw new Error(
1968
- "[provision.chromeExtension] Either extensionPath or extensionId is required",
1969
- );
1970
- }
1971
-
1972
- let extensionPath = providedExtensionPath;
1973
- const shell = this.os === "windows" ? "pwsh" : "sh";
1974
-
1975
- // If extensionId is provided, download and extract the extension from Chrome Web Store
1976
- if (extensionId && !extensionPath) {
1977
- console.log(
1978
- `[provision.chromeExtension] Downloading extension ${extensionId} from Chrome Web Store...`,
1979
- );
1980
-
1981
- const extensionDir =
1982
- this.os === "windows"
1983
- ? `C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Extensions\\${extensionId}`
1984
- : `/tmp/testdriver-extensions/${extensionId}`;
1985
-
1986
- // Create extension directory
1987
- const mkdirCmd =
1988
- this.os === "windows"
1989
- ? `New-Item -ItemType Directory -Path "${extensionDir}" -Force | Out-Null`
1990
- : `mkdir -p "${extensionDir}"`;
1991
- await this.exec(shell, mkdirCmd, 60000, true);
1992
-
1993
- // Download CRX from Chrome Web Store
1994
- // The CRX download URL format for Chrome Web Store
1995
- const crxUrl = `https://clients2.google.com/service/update2/crx?response=redirect&prodversion=131.0.0.0&acceptformat=crx2,crx3&x=id%3D${extensionId}%26installsource%3Dondemand%26uc`;
1996
- const crxPath =
1997
- this.os === "windows"
1998
- ? `${extensionDir}\\extension.crx`
1999
- : `${extensionDir}/extension.crx`;
2000
-
2001
- if (this.os === "windows") {
2002
- await this.exec(
2003
- "pwsh",
2004
- `Invoke-WebRequest -Uri "${crxUrl}" -OutFile "${crxPath}"`,
2005
- 60000,
2006
- true,
2007
- );
2008
- } else {
2009
- await this.exec(
2010
- "sh",
2011
- `curl -L -o "${crxPath}" "${crxUrl}"`,
2012
- 60000,
2013
- true,
2014
- );
2015
- }
2016
-
2017
- // Extract the CRX file (CRX is a ZIP with a header)
2018
- // Skip the CRX header and extract as ZIP
2019
- if (this.os === "windows") {
2020
- // PowerShell: Read CRX, skip header, extract ZIP
2021
- await this.exec(
2022
- "pwsh",
2023
- `
2024
- $crxBytes = [System.IO.File]::ReadAllBytes("${crxPath}")
2025
- # CRX3 header: 4 bytes magic + 4 bytes version + 4 bytes header length + header
2026
- $magic = [System.Text.Encoding]::ASCII.GetString($crxBytes[0..3])
2027
- if ($magic -eq "Cr24") {
2028
- $headerLen = [BitConverter]::ToUInt32($crxBytes, 8)
2029
- $zipStart = 12 + $headerLen
2030
- } else {
2031
- # CRX2 format
2032
- $zipStart = 16 + [BitConverter]::ToUInt32($crxBytes, 8) + [BitConverter]::ToUInt32($crxBytes, 12)
2033
- }
2034
- $zipBytes = $crxBytes[$zipStart..($crxBytes.Length - 1)]
2035
- $zipPath = "${extensionDir}\\extension.zip"
2036
- [System.IO.File]::WriteAllBytes($zipPath, $zipBytes)
2037
- Expand-Archive -Path $zipPath -DestinationPath "${extensionDir}\\unpacked" -Force
2038
- `,
2039
- 30000,
2040
- true,
2041
- );
2042
- extensionPath = `${extensionDir}\\unpacked`;
2043
- } else {
2044
- // Linux: Use unzip with offset or python to extract
2045
- await this.exec(
2046
- "sh",
2047
- `
2048
- cd "${extensionDir}"
2049
- # Extract CRX (skip header and unzip)
2050
- # CRX3 format: magic(4) + version(4) + header_length(4) + header + zip
2051
- python3 -c "
2052
- import struct
2053
- import zipfile
2054
- import io
2055
- import os
2056
-
2057
- with open('extension.crx', 'rb') as f:
2058
- data = f.read()
2059
-
2060
- # Check magic number
2061
- magic = data[:4]
2062
- if magic == b'Cr24':
2063
- # CRX3 format
2064
- header_len = struct.unpack('<I', data[8:12])[0]
2065
- zip_start = 12 + header_len
2066
- else:
2067
- # CRX2 format
2068
- pub_key_len = struct.unpack('<I', data[8:12])[0]
2069
- sig_len = struct.unpack('<I', data[12:16])[0]
2070
- zip_start = 16 + pub_key_len + sig_len
2071
-
2072
- zip_data = data[zip_start:]
2073
- os.makedirs('unpacked', exist_ok=True)
2074
- with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
2075
- zf.extractall('unpacked')
2076
- "
2077
- `,
2078
- 30000,
2079
- true,
2080
- );
2081
- extensionPath = `${extensionDir}/unpacked`;
2082
- }
2083
-
2084
- console.log(
2085
- `[provision.chromeExtension] Extension ${extensionId} extracted to ${extensionPath}`,
2086
- );
2087
- }
2088
-
2089
- // Set up Chrome profile with preferences
2090
- const userDataDir =
2091
- this.os === "windows"
2092
- ? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
2093
- : "/tmp/testdriver-chrome-profile";
2094
-
2095
- // Create user data directory and Default profile directory
2096
- const defaultProfileDir =
2097
- this.os === "windows"
2098
- ? `${userDataDir}\\Default`
2099
- : `${userDataDir}/Default`;
2100
-
2101
- const createDirCmd =
2102
- this.os === "windows"
2103
- ? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
2104
- : `mkdir -p "${defaultProfileDir}"`;
2105
-
2106
- await this.exec(shell, createDirCmd, 60000, true);
2107
-
2108
- // Write Chrome preferences
2109
- const chromePrefs = {
2110
- credentials_enable_service: false,
2111
- profile: {
2112
- password_manager_enabled: false,
2113
- default_content_setting_values: {},
2114
- },
2115
- signin: {
2116
- allowed: false,
2117
- },
2118
- sync: {
2119
- requested: false,
2120
- first_setup_complete: true,
2121
- sync_all_os_types: false,
2122
- },
2123
- autofill: {
2124
- enabled: false,
2125
- },
2126
- local_state: {
2127
- browser: {
2128
- has_seen_welcome_page: true,
2129
- },
2130
- },
2131
- };
2132
-
2133
- const prefsPath =
2134
- this.os === "windows"
2135
- ? `${defaultProfileDir}\\Preferences`
2136
- : `${defaultProfileDir}/Preferences`;
2137
-
2138
- const prefsJson = JSON.stringify(chromePrefs, null, 2);
2139
- const writePrefCmd =
2140
- this.os === "windows"
2141
- ? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
2142
- `[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
2143
- : `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
2144
-
2145
- await this.exec(shell, writePrefCmd, 60000, true);
2146
-
2147
- // Build Chrome launch command
2148
- const chromeArgs = [];
2149
- if (maximized) chromeArgs.push("--start-maximized");
2150
- chromeArgs.push(
2151
- "--disable-fre",
2152
- "--no-default-browser-check",
2153
- "--no-first-run",
2154
- "--no-experiments",
2155
- "--disable-infobars",
2156
- "--disable-features=ChromeLabs",
2157
- `--user-data-dir=${userDataDir}`,
2158
- );
2159
-
2160
- // Add remote debugging port for captcha solving support
2161
- chromeArgs.push("--remote-debugging-port=9222");
2162
-
2163
- // Add user extension and dashcam-chrome extension
2164
- const dashcamChromePath = await this._getDashcamChromeExtensionPath();
2165
- if (dashcamChromePath) {
2166
- // Load both user extension and dashcam-chrome for web log capture
2167
- chromeArgs.push(
2168
- `--load-extension=${extensionPath},${dashcamChromePath}`,
2169
- );
2170
- } else {
2171
- // If dashcam-chrome unavailable, just load user extension
2172
- chromeArgs.push(`--load-extension=${extensionPath}`);
2173
- }
2174
-
2175
- // Launch Chrome (opens to New Tab by default)
2176
- if (this.os === "windows") {
2177
- const argsString = chromeArgs.map((arg) => `"${arg}"`).join(", ");
2178
- await this.exec(
2179
- shell,
2180
- `Start-Process "C:\\ChromeForTesting\\chrome-win64\\chrome.exe" -ArgumentList ${argsString}`,
2181
- 30000,
2182
- );
2183
- } else {
2184
- const argsString = chromeArgs.join(" ");
2185
- await this.exec(
2186
- shell,
2187
- `chrome-for-testing ${argsString} >/dev/null 2>&1 &`,
2188
- 30000,
2189
- );
2190
- }
2191
-
2192
- // Wait for Chrome debugger port and page to be ready
2193
- await this._waitForChromeDebuggerReady();
2194
- await this.focusApplication("Google Chrome");
2195
-
2196
- // Start dashcam recording
2197
- if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
2198
- await this.dashcam.start();
2199
- }
2200
- },
2201
-
2202
- /**
2203
- * Launch VS Code
2204
- * @param {Object} options - VS Code launch options
2205
- * @param {string} [options.workspace] - Workspace/folder to open
2206
- * @param {string[]} [options.extensions=[]] - Extensions to install
2207
- * @returns {Promise<void>}
2208
- */
2209
- vscode: async (options = {}) => {
2210
- const { workspace = null, extensions = [] } = options;
2211
-
2212
- const shell = this.os === "windows" ? "pwsh" : "sh";
2213
-
2214
- // Install extensions if provided
2215
- for (const extension of extensions) {
2216
- console.log(`[provision.vscode] Installing extension: ${extension}`);
2217
- await this.exec(
2218
- shell,
2219
- `code --install-extension ${extension} --force`,
2220
- 120000,
2221
- true,
2222
- );
2223
- console.log(
2224
- `[provision.vscode] ✅ Extension installed: ${extension}`,
2225
- );
2226
- }
2227
-
2228
- // Launch VS Code
2229
- const workspaceArg = workspace ? `"${workspace}"` : "";
2230
-
2231
- if (this.os === "windows") {
2232
- await this.exec(
2233
- shell,
2234
- `Start-Process code -ArgumentList ${workspaceArg}`,
2235
- 30000,
2236
- );
2237
- } else {
2238
- await this.exec(
2239
- shell,
2240
- `code ${workspaceArg} >/dev/null 2>&1 &`,
2241
- 30000,
2242
- );
2243
- }
2244
-
2245
- // Wait for VS Code to start up
2246
- await new Promise((resolve) => setTimeout(resolve, 3000));
2247
-
2248
- // Wait for VS Code to be ready
2249
- await this.focusApplication("Visual Studio Code");
2250
-
2251
- // Start dashcam recording
2252
- if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
2253
- await this.dashcam.start();
2254
- }
2255
- },
2256
-
2257
- /**
2258
- * Download and install an application
2259
- * @param {Object} options - Installer options
2260
- * @param {string} options.url - URL to download the installer from
2261
- * @param {string} [options.filename] - Filename to save as (auto-detected from URL if not provided)
2262
- * @param {string} [options.appName] - Application name to focus after install
2263
- * @param {boolean} [options.launch=true] - Whether to launch the app after installation
2264
- * @returns {Promise<string>} Path to the downloaded file
2265
- * @example
2266
- * // Install a .deb package on Linux (auto-detected)
2267
- * await testdriver.provision.installer({
2268
- * url: 'https://example.com/app.deb',
2269
- * appName: 'MyApp'
2270
- * });
2271
- *
2272
- * @example
2273
- * // Download and run custom commands
2274
- * const filePath = await testdriver.provision.installer({
2275
- * url: 'https://example.com/app.AppImage',
2276
- * launch: false
2277
- * });
2278
- * await testdriver.exec('sh', `chmod +x "${filePath}" && "${filePath}" &`, 10000);
2279
- */
2280
- installer: async (options = {}) => {
2281
- const { url, filename, appName, launch = true } = options;
2282
-
2283
- if (!url) {
2284
- throw new Error("[provision.installer] url is required");
2285
- }
2286
-
2287
- const shell = this.os === "windows" ? "pwsh" : "sh";
2288
-
2289
- // Determine download directory
2290
- const downloadDir =
2291
- this.os === "windows" ? "C:\\Users\\testdriver\\Downloads" : "/tmp";
2292
-
2293
- console.log(`[provision.installer] Downloading ${url}...`);
2294
-
2295
- let actualFilePath;
2296
-
2297
- // Download the file and get the actual filename (handles redirects)
2298
- if (this.os === "windows") {
2299
- // Simple approach: download first, then get the actual filename from the response
2300
- const tempFile = `${downloadDir}\\installer_temp_${Date.now()}`;
2301
-
2302
- const downloadScript = `
2303
- $ProgressPreference = 'SilentlyContinue'
2304
- $response = Invoke-WebRequest -Uri "${url}" -OutFile "${tempFile}" -PassThru -UseBasicParsing
2305
-
2306
- # Try to get filename from Content-Disposition header
2307
- $filename = $null
2308
- if ($response.Headers['Content-Disposition']) {
2309
- if ($response.Headers['Content-Disposition'] -match 'filename=\\"?([^\\"]+)\\"?') {
2310
- $filename = $matches[1]
2311
- }
2312
- }
2313
-
2314
- # If no filename from header, try to get from URL or use default
2315
- if (-not $filename) {
2316
- $uri = [System.Uri]"${url}"
2317
- $filename = [System.IO.Path]::GetFileName($uri.LocalPath)
2318
- if (-not $filename -or $filename -eq '') {
2319
- $filename = "installer"
2320
- }
2321
- }
2322
-
2323
- # Move temp file to final location with proper filename
2324
- $finalPath = Join-Path "${downloadDir}" $filename
2325
- Move-Item -Path "${tempFile}" -Destination $finalPath -Force
2326
- Write-Output $finalPath
2327
- `;
2328
-
2329
- const result = await this.exec(shell, downloadScript, 300000, true);
2330
- actualFilePath = result ? result.trim() : null;
2331
-
2332
- if (!actualFilePath) {
2333
- throw new Error("[provision.installer] Failed to download file");
2334
- }
2335
- } else {
2336
- // Use curl with options to get the final filename
2337
- const tempMarker = `installer_${Date.now()}`;
2338
- const downloadScript = `
2339
- cd "${downloadDir}"
2340
- curl -L -J -O -w "%{filename_effective}" "${url}" 2>/dev/null || echo "${tempMarker}"
2341
- `;
2342
-
2343
- const result = await this.exec(shell, downloadScript, 300000, true);
2344
- const downloadedFile = result ? result.trim() : null;
2345
-
2346
- if (downloadedFile && downloadedFile !== tempMarker) {
2347
- actualFilePath = `${downloadDir}/${downloadedFile}`;
2348
- } else {
2349
- // Fallback: use curl without -J and specify output file
2350
- const fallbackFilename = filename || "installer";
2351
- actualFilePath = `${downloadDir}/${fallbackFilename}`;
2352
- await this.exec(
2353
- shell,
2354
- `curl -L -o "${actualFilePath}" "${url}"`,
2355
- 300000,
2356
- true,
2357
- );
2358
- }
2359
- }
2360
-
2361
- console.log(`[provision.installer] ✅ Downloaded to ${actualFilePath}`);
2362
-
2363
- // Auto-detect install command based on file extension (use actualFilePath for extension detection)
2364
- const actualFilename = actualFilePath.split(/[/\\]/).pop() || "";
2365
- const ext = actualFilename.split(".").pop()?.toLowerCase();
2366
- let installCommand = null;
2367
-
2368
- if (this.os === "windows") {
2369
- if (ext === "msi") {
2370
- installCommand = `Start-Process msiexec -ArgumentList '/i', '"${actualFilePath}"', '/quiet', '/norestart' -Wait`;
2371
- } else if (ext === "exe") {
2372
- installCommand = `Start-Process "${actualFilePath}" -ArgumentList '/S' -Wait`;
2373
- }
2374
- } else if (this.os === "linux") {
2375
- if (ext === "deb") {
2376
- installCommand = `sudo dpkg -i "${actualFilePath}" && sudo apt-get install -f -y`;
2377
- } else if (ext === "rpm") {
2378
- installCommand = `sudo rpm -i "${actualFilePath}"`;
2379
- } else if (ext === "appimage") {
2380
- installCommand = `chmod +x "${actualFilePath}"`;
2381
- } else if (ext === "sh") {
2382
- installCommand = `chmod +x "${actualFilePath}" && "${actualFilePath}"`;
2383
- }
2384
- } else if (this.os === "darwin") {
2385
- if (ext === "dmg") {
2386
- installCommand = `hdiutil attach "${actualFilePath}" -mountpoint /Volumes/installer && cp -R /Volumes/installer/*.app /Applications/ && hdiutil detach /Volumes/installer`;
2387
- } else if (ext === "pkg") {
2388
- installCommand = `sudo installer -pkg "${actualFilePath}" -target /`;
2389
- }
2390
- }
2391
-
2392
- if (installCommand) {
2393
- console.log(`[provision.installer] Installing...`);
2394
- await this.exec(shell, installCommand, 300000, true);
2395
- console.log(`[provision.installer] ✅ Installation complete`);
2396
- }
2397
-
2398
- // Launch and focus the app if appName is provided and launch is true
2399
- if (appName && launch) {
2400
- await new Promise((resolve) => setTimeout(resolve, 2000));
2401
- await this.focusApplication(appName);
2402
- }
2403
-
2404
- // Start dashcam recording
2405
- if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
2406
- await this.dashcam.start();
2407
- }
2408
-
2409
- return actualFilePath;
2410
- },
2411
-
2412
- /**
2413
- * Launch Electron app
2414
- * @param {Object} options - Electron launch options
2415
- * @param {string} options.appPath - Path to Electron app (required)
2416
- * @param {string[]} [options.args=[]] - Additional electron args
2417
- * @returns {Promise<void>}
2418
- */
2419
- electron: async (options = {}) => {
2420
- const { appPath, args = [] } = options;
2421
-
2422
- if (!appPath) {
2423
- throw new Error("provision.electron requires appPath option");
2424
- }
2425
-
2426
- const shell = this.os === "windows" ? "pwsh" : "sh";
2427
-
2428
- const argsString = args.join(" ");
2429
-
2430
- if (this.os === "windows") {
2431
- await this.exec(
2432
- shell,
2433
- `Start-Process electron -ArgumentList "${appPath}", ${argsString}`,
2434
- 30000,
2435
- );
2436
- } else {
2437
- await this.exec(
2438
- shell,
2439
- `electron "${appPath}" ${argsString} >/dev/null 2>&1 &`,
2440
- 30000,
2441
- );
2442
- }
2443
-
2444
- await this.focusApplication("Electron");
2445
-
2446
- // Start dashcam recording
2447
- if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
2448
- await this.dashcam.start();
2449
- }
2450
- },
2451
-
2452
- /**
2453
- * Initialize Dashcam recording with logging
2454
- * @param {Object} options - Dashcam options
2455
- * @param {string} [options.logPath] - Path to log file (auto-generated if not provided)
2456
- * @param {string} [options.logName='TestDriver Log'] - Display name for the log
2457
- * @param {boolean} [options.webLogs=true] - Enable web log tracking
2458
- * @param {string} [options.title] - Custom title for the recording
2459
- * @returns {Promise<void>}
2460
- */
2461
- dashcam: async (options = {}) => {
2462
- const {
2463
- logPath,
2464
- logName = "TestDriver Log",
2465
- webLogs = true,
2466
- title,
2467
- } = options;
2468
-
2469
- // Ensure dashcam is enabled
2470
- if (!this.dashcamEnabled) {
2471
- console.warn(
2472
- "[provision.dashcam] Dashcam is not enabled. Skipping.",
2473
- );
2474
- return;
2475
- }
2476
-
2477
- // Set custom title if provided
2478
- if (title) {
2479
- this.dashcam.setTitle(title);
2480
- }
2481
-
2482
- // Add file log tracking
2483
- const actualLogPath =
2484
- logPath ||
2485
- (this.os === "windows"
2486
- ? "C:\\Users\\testdriver\\testdriver.log"
2487
- : "/tmp/testdriver.log");
2488
-
2489
- await this.dashcam.addFileLog(actualLogPath, logName);
2490
-
2491
- // Add web log tracking if enabled
2492
- // Use domain pattern from provisioned Chrome URL if available
2493
- if (webLogs) {
2494
- const pattern = this._provisionedChromeUrl
2495
- ? this._getUrlDomainPattern(this._provisionedChromeUrl)
2496
- : "**";
2497
- await this.dashcam.addWebLog(pattern, "Web Logs");
2498
- }
2499
-
2500
- // Start recording if not already recording
2501
- if (!(await this.dashcam.isRecording())) {
2502
- await this.dashcam.start();
2503
- }
2504
-
2505
- console.log("[provision.dashcam] ✅ Dashcam recording started");
2506
- },
2507
- };
2508
-
2509
- // Wrap all provision methods with reconnect check using Proxy
2510
- return new Proxy(provisionMethods, {
2511
- get(target, prop) {
2512
- const method = target[prop];
2513
- if (typeof method === "function") {
2514
- return async (...args) => {
2515
- // Skip provisioning if reconnecting to existing sandbox
2516
- if (self.reconnect) {
2517
- console.log(
2518
- `[provision.${prop}] Skipping provisioning (reconnect mode)`,
2519
- );
2520
- return;
2521
- }
2522
- return method(...args);
2523
- };
2524
- }
2525
- return method;
2526
- },
2527
- });
1802
+ return createProvisionAPI(this);
2528
1803
  }
2529
1804
 
2530
1805
  /**