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/agent/lib/provision-commands.js +38 -11
- package/docs/_data/examples-manifest.json +52 -52
- package/docs/v7/examples/ai.mdx +1 -1
- package/docs/v7/examples/assert.mdx +1 -1
- package/docs/v7/examples/chrome-extension.mdx +30 -13
- package/docs/v7/examples/element-not-found.mdx +1 -1
- package/docs/v7/examples/findall-coffee-icons.mdx +1 -1
- package/docs/v7/examples/hover-image.mdx +1 -1
- package/docs/v7/examples/hover-text-with-description.mdx +1 -1
- package/docs/v7/examples/hover-text.mdx +1 -1
- package/docs/v7/examples/installer.mdx +1 -1
- package/docs/v7/examples/launch-vscode-linux.mdx +1 -1
- package/docs/v7/examples/parse.mdx +1 -1
- package/docs/v7/examples/press-keys.mdx +1 -1
- package/docs/v7/examples/scroll-keyboard.mdx +1 -1
- package/docs/v7/examples/scroll.mdx +1 -1
- package/docs/v7/examples/type.mdx +1 -1
- package/examples/chrome-extension.test.mjs +29 -12
- package/lib/core/Dashcam.js +18 -0
- package/lib/provision.js +749 -0
- package/package.json +1 -1
- package/sdk.js +2 -727
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
|
-
|
|
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
|
/**
|