@testdriverai/mcp 7.10.0-canary.0 → 7.10.1-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/.claude-plugin/marketplace.json +17 -0
- package/agent/index.js +116 -71
- package/agent/interface.js +13 -1
- package/agent/lib/commands.js +35 -15
- package/agent/lib/provision-commands.js +92 -11
- package/agent/lib/sandbox.js +263 -30
- package/agent/lib/sdk.js +16 -12
- package/agent/lib/system.js +107 -31
- package/ai/.claude-plugin/plugin.json +89 -4
- package/ai/agents/testdriver.md +4 -8
- package/ai/skills/testdriver-assert/SKILL.md +0 -1
- package/ai/skills/testdriver-aws-setup/SKILL.md +1 -1
- package/ai/skills/testdriver-caching/SKILL.md +13 -6
- package/ai/skills/testdriver-ci-cd/SKILL.md +57 -14
- package/ai/skills/testdriver-client/SKILL.md +10 -6
- package/ai/skills/testdriver-customizing-devices/SKILL.md +7 -5
- package/ai/skills/testdriver-dashcam/SKILL.md +36 -3
- package/ai/skills/testdriver-debugging-with-screenshots/SKILL.md +28 -4
- package/ai/skills/testdriver-device-config/SKILL.md +5 -5
- package/ai/skills/testdriver-find/SKILL.md +50 -11
- package/ai/skills/testdriver-generating-tests/SKILL.md +163 -8
- package/ai/skills/testdriver-hosted/SKILL.md +1 -1
- package/ai/skills/testdriver-machine-setup/SKILL.md +331 -0
- package/ai/skills/testdriver-making-assertions/SKILL.md +78 -2
- package/ai/skills/testdriver-mcp-workflow/SKILL.md +2 -2
- package/ai/skills/testdriver-parse/SKILL.md +1 -1
- package/ai/skills/testdriver-performing-actions/SKILL.md +97 -3
- package/ai/skills/testdriver-provision/SKILL.md +0 -20
- package/ai/skills/testdriver-quickstart/SKILL.md +299 -21
- package/ai/skills/testdriver-reusable-code/SKILL.md +3 -3
- package/ai/skills/testdriver-running-tests/SKILL.md +1 -1
- package/ai/skills/testdriver-screenshot/SKILL.md +5 -5
- package/ai/skills/testdriver-scroll/SKILL.md +30 -160
- package/ai/skills/testdriver-test-writer/SKILL.md +3 -5
- package/ai/skills/testdriver-testdriver/SKILL.md +3 -7
- package/ai/skills/testdriver-wait/SKILL.md +1 -1
- package/{docs/v7/ai.mdx → ai/skills/testdriver:ai/SKILL.md} +3 -4
- package/ai/skills/testdriver:assert/SKILL.md +315 -0
- package/ai/skills/testdriver:aws-setup/SKILL.md +448 -0
- package/ai/skills/testdriver:cache/SKILL.md +221 -0
- package/ai/skills/testdriver:caching/SKILL.md +124 -0
- package/ai/skills/testdriver:captcha/SKILL.md +158 -0
- package/ai/skills/testdriver:ci-cd/SKILL.md +659 -0
- package/ai/skills/testdriver:click/SKILL.md +286 -0
- package/ai/skills/testdriver:client/SKILL.md +477 -0
- package/ai/skills/testdriver:customizing-devices/SKILL.md +319 -0
- package/ai/skills/testdriver:dashcam/SKILL.md +451 -0
- package/ai/skills/testdriver:debugging-with-screenshots/SKILL.md +415 -0
- package/{docs/v7/device-config.mdx → ai/skills/testdriver:device-config/SKILL.md} +8 -8
- package/ai/skills/testdriver:double-click/SKILL.md +102 -0
- package/ai/skills/testdriver:elements/SKILL.md +605 -0
- package/ai/skills/testdriver:enterprise/SKILL.md +7 -0
- package/ai/skills/testdriver:errors/SKILL.md +246 -0
- package/ai/skills/testdriver:events/SKILL.md +356 -0
- package/ai/skills/testdriver:exec/SKILL.md +317 -0
- package/ai/skills/testdriver:find/SKILL.md +860 -0
- package/ai/skills/testdriver:focus-application/SKILL.md +293 -0
- package/ai/skills/testdriver:generating-tests/SKILL.md +25 -0
- package/ai/skills/testdriver:hosted/SKILL.md +156 -0
- package/ai/skills/testdriver:hover/SKILL.md +278 -0
- package/{docs/v7/locating-elements.mdx → ai/skills/testdriver:locating-elements/SKILL.md} +3 -3
- package/ai/skills/testdriver:making-assertions/SKILL.md +32 -0
- package/ai/skills/testdriver:mcp/SKILL.md +7 -0
- package/ai/skills/testdriver:mouse-down/SKILL.md +161 -0
- package/ai/skills/testdriver:mouse-up/SKILL.md +164 -0
- package/ai/skills/testdriver:parse/SKILL.md +236 -0
- package/ai/skills/testdriver:performing-actions/SKILL.md +53 -0
- package/ai/skills/testdriver:press-keys/SKILL.md +348 -0
- package/ai/skills/testdriver:provision/SKILL.md +331 -0
- package/ai/skills/testdriver:quickstart/SKILL.md +172 -0
- package/ai/skills/testdriver:redraw/SKILL.md +214 -0
- package/ai/skills/testdriver:reusable-code/SKILL.md +249 -0
- package/ai/skills/testdriver:right-click/SKILL.md +123 -0
- package/{docs/v7/running-tests.mdx → ai/skills/testdriver:running-tests/SKILL.md} +4 -4
- package/ai/skills/testdriver:screenshot/SKILL.md +248 -0
- package/ai/skills/testdriver:screenshots/SKILL.md +184 -0
- package/ai/skills/testdriver:scroll/SKILL.md +205 -0
- package/ai/skills/testdriver:secrets/SKILL.md +115 -0
- package/ai/skills/testdriver:self-hosted/SKILL.md +147 -0
- package/ai/skills/testdriver:test-results-json/SKILL.md +257 -0
- package/ai/skills/testdriver:testdriver/SKILL.md +624 -0
- package/ai/skills/testdriver:type/SKILL.md +357 -0
- package/ai/skills/testdriver:variables/SKILL.md +111 -0
- package/ai/skills/testdriver:wait/SKILL.md +50 -0
- package/{docs/v7/waiting-for-elements.mdx → ai/skills/testdriver:waiting-for-elements/SKILL.md} +3 -3
- package/ai/skills/testdriver:what-is-testdriver/SKILL.md +54 -0
- package/debugger/index.html +12 -0
- package/docs/_data/examples-manifest.json +54 -46
- package/docs/_data/examples-manifest.schema.json +12 -2
- package/docs/_scripts/extract-example-urls.js +21 -3
- package/docs/_scripts/generate-examples.js +64 -16
- package/docs/changelog.mdx +29 -2
- package/docs/claude-mcp-plugin.mdx +65 -18
- package/docs/docs.json +16 -33
- package/docs/snippets/tests/scroll-yaml.mdx +2 -2
- package/docs/v6/commands/scroll-until-image.mdx +1 -1
- package/docs/v6/commands/scroll-until-text.mdx +2 -2
- package/docs/v6/commands/scroll.mdx +2 -2
- package/docs/v7/_drafts/agents.mdx +0 -4
- package/docs/v7/_drafts/best-practices.mdx +0 -12
- package/docs/v7/_drafts/commands/scroll-until-image.mdx +1 -1
- package/docs/v7/_drafts/commands/scroll-until-text.mdx +2 -2
- package/docs/v7/_drafts/commands/scroll.mdx +1 -1
- package/docs/v7/_drafts/configuration.mdx +0 -12
- package/docs/v7/_drafts/faq.mdx +2 -10
- package/docs/v7/_drafts/readme.mdx +0 -1
- package/docs/v7/_drafts/troubleshooting.mdx +1 -4
- package/docs/v7/assert.mdx +0 -1
- package/docs/v7/aws-setup.mdx +1 -1
- package/docs/v7/caching.mdx +14 -10
- package/docs/v7/ci-cd.mdx +98 -14
- package/docs/v7/client.mdx +10 -6
- package/docs/v7/copilot/auto-healing.mdx +167 -18
- package/docs/v7/copilot/running-tests.mdx +915 -54
- package/docs/v7/customizing-devices.mdx +7 -5
- package/docs/v7/dashcam.mdx +36 -3
- package/docs/v7/debugging-with-screenshots.mdx +31 -7
- package/docs/v7/examples/ai.mdx +13 -8
- 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 +3 -2
- package/docs/v7/examples/formatted-logging.mdx +74 -0
- package/docs/v7/examples/hover-image.mdx +1 -1
- package/docs/v7/examples/hover-text-with-description.mdx +2 -2
- 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 +27 -27
- package/docs/v7/examples/match-image.mdx +1 -1
- package/docs/v7/examples/parse.mdx +3 -2
- package/docs/v7/examples/press-keys.mdx +2 -2
- package/docs/v7/examples/scroll-keyboard.mdx +2 -2
- package/docs/v7/examples/scroll-until-text.mdx +1 -1
- package/docs/v7/examples/scroll.mdx +1 -1
- package/docs/v7/examples/type.mdx +1 -1
- package/docs/v7/find.mdx +50 -11
- package/docs/v7/generating-tests.mdx +166 -10
- package/docs/v7/hosted.mdx +1 -1
- package/docs/v7/making-assertions.mdx +81 -4
- package/docs/v7/parse.mdx +1 -1
- package/docs/v7/performing-actions.mdx +100 -5
- package/docs/v7/provision.mdx +0 -20
- package/docs/v7/quickstart.mdx +299 -21
- package/docs/v7/reusable-code.mdx +3 -3
- package/docs/v7/screenshot.mdx +5 -5
- package/docs/v7/scroll.mdx +30 -160
- package/docs/v7/wait.mdx +1 -1
- package/examples/chrome-extension.test.mjs +29 -12
- package/examples/config.mjs +1 -1
- package/examples/launch-vscode-linux.test.mjs +25 -25
- package/interfaces/cli/commands/init.js +84 -2
- package/interfaces/vitest-plugin.mjs +51 -61
- package/lib/core/Dashcam.js +43 -2
- package/lib/core/index.d.ts +13 -1
- package/lib/github-comment.mjs +11 -5
- package/lib/init-project.js +98 -44
- package/lib/install-clients.js +470 -0
- package/lib/provision.js +749 -0
- package/lib/resolve-channel.js +2 -2
- package/lib/sentry.js +15 -2
- package/lib/vitest/hooks.mjs +44 -34
- package/mcp-server/README.md +32 -0
- package/mcp-server/dist/core/actions.d.ts +176 -0
- package/mcp-server/dist/core/actions.js +753 -0
- package/mcp-server/dist/env-utils.d.ts +45 -0
- package/mcp-server/dist/env-utils.js +63 -0
- package/mcp-server/dist/provision-types.d.ts +6 -2
- package/mcp-server/dist/provision-types.js +3 -1
- package/mcp-server/dist/server.mjs +449 -751
- package/mcp-server/package-lock.json +384 -1
- package/mcp-server/package.json +5 -2
- package/mcp-server/src/core/actions.ts +912 -0
- package/mcp-server/src/env-utils.test.ts +82 -0
- package/mcp-server/src/env-utils.ts +77 -0
- package/mcp-server/src/provision-types.ts +4 -1
- package/mcp-server/src/server.ts +533 -954
- package/mcp-server/tsconfig.json +1 -1
- package/mcp-server/vitest.config.ts +7 -0
- package/package.json +15 -3
- package/sdk.d.ts +11 -9
- package/sdk.js +116 -746
- package/setup/aws/spawn-runner.sh +24 -3
- package/docs/v7/_drafts/caching-ai.mdx +0 -215
- package/docs/v7/_drafts/caching.mdx +0 -366
- package/docs/v7/_drafts/prompt-cache.mdx +0 -200
- package/docs/v7/copilot/creating-tests.mdx +0 -156
- package/docs/v7/copilot/github.mdx +0 -143
- package/docs/v7/copilot/setup.mdx +0 -143
- package/docs/v7/examples/exec-output.mdx +0 -84
- package/docs/v7/examples/exec-pwsh.mdx +0 -82
- package/docs/v7/examples/focus-window.mdx +0 -61
- package/docs/v7/examples/scroll-until-image.mdx +0 -82
- package/docs/v7/examples/windows-installer.mdx +0 -95
- package/examples/formatted-logging.test.mjs +0 -27
- package/examples/match-image.test.mjs +0 -55
- package/manual/exec-stream-logs.test.mjs +0 -25
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.
|
|
@@ -471,17 +472,20 @@ class Element {
|
|
|
471
472
|
this.sdk.emitter.emit(events.log.log, findingMessage);
|
|
472
473
|
|
|
473
474
|
try {
|
|
474
|
-
|
|
475
|
+
// Returns { imageKey } (fast S3-key path, no local round-trip) or
|
|
476
|
+
// { image } (base64 fallback). See system.captureScreenImage.
|
|
477
|
+
const imagePayload = await this.system.captureScreenImage();
|
|
475
478
|
// Only store screenshot in DEBUG mode to prevent memory leaks
|
|
476
|
-
if (debugMode) {
|
|
477
|
-
this._screenshot =
|
|
479
|
+
if (debugMode && imagePayload.image) {
|
|
480
|
+
this._screenshot = imagePayload.image;
|
|
478
481
|
}
|
|
479
482
|
|
|
480
483
|
// Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold/cache
|
|
481
484
|
let cacheKey = null;
|
|
482
485
|
let cacheThreshold = null;
|
|
483
486
|
let perCommandThresholds = null; // Per-command { screen, element } override
|
|
484
|
-
let zoom =
|
|
487
|
+
let zoom = false; // Default to disabled
|
|
488
|
+
let verify = false; // Default to disabled (skip AI verification)
|
|
485
489
|
let perCommandAi = null; // Per-command AI config override
|
|
486
490
|
|
|
487
491
|
let minConfidence = null; // Minimum confidence threshold
|
|
@@ -494,8 +498,10 @@ class Element {
|
|
|
494
498
|
// New: options is an object with cacheKey and/or cacheThreshold
|
|
495
499
|
cacheKey = options.cacheKey || null;
|
|
496
500
|
cacheThreshold = options.cacheThreshold ?? null;
|
|
497
|
-
// zoom defaults to
|
|
498
|
-
zoom = options.zoom
|
|
501
|
+
// zoom defaults to false unless explicitly set to true
|
|
502
|
+
zoom = options.zoom === true;
|
|
503
|
+
// verify defaults to false unless explicitly set to true
|
|
504
|
+
verify = options.verify === true;
|
|
499
505
|
// Minimum confidence threshold: fail find if AI confidence is below this value
|
|
500
506
|
minConfidence = options.confidence ?? null;
|
|
501
507
|
// Element type hint for prompt wrapping
|
|
@@ -562,13 +568,14 @@ class Element {
|
|
|
562
568
|
response = await this.sdk.apiClient.req("find", {
|
|
563
569
|
session: this.sdk.getSessionId(),
|
|
564
570
|
element: description,
|
|
565
|
-
|
|
571
|
+
...imagePayload,
|
|
566
572
|
threshold: threshold,
|
|
567
573
|
elementSimilarity: elementSimilarity,
|
|
568
574
|
cacheKey: cacheKey,
|
|
569
575
|
os: this.sdk.os,
|
|
570
576
|
resolution: this.sdk.resolution,
|
|
571
577
|
zoom: zoom === true ? 1 : zoom === false ? 0 : zoom,
|
|
578
|
+
skipVerify: !verify,
|
|
572
579
|
confidence: minConfidence,
|
|
573
580
|
type: elementType,
|
|
574
581
|
ai: {
|
|
@@ -1505,6 +1512,25 @@ class TestDriverSDK {
|
|
|
1505
1512
|
this.reconnect =
|
|
1506
1513
|
options.reconnect !== undefined ? options.reconnect : false;
|
|
1507
1514
|
|
|
1515
|
+
// Explicit sandbox id to reconnect to (overrides last-sandbox file)
|
|
1516
|
+
this.sandboxId = options.sandboxId || null;
|
|
1517
|
+
|
|
1518
|
+
// When reconnect is requested, an explicit sandboxId implies newSandbox=false.
|
|
1519
|
+
// If reconnect:true but no sandboxId given, try to load from .testdriver/last-sandbox.
|
|
1520
|
+
if (this.reconnect && !this.sandboxId) {
|
|
1521
|
+
const last = TestDriverSDK._readLastSandbox();
|
|
1522
|
+
if (last && last.sandboxId) {
|
|
1523
|
+
this.sandboxId = last.sandboxId;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
if (this.sandboxId && options.newSandbox === undefined) {
|
|
1527
|
+
this.newSandbox = false;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Store keepAlive preference from options
|
|
1531
|
+
this.keepAlive =
|
|
1532
|
+
options.keepAlive !== undefined ? options.keepAlive : undefined;
|
|
1533
|
+
|
|
1508
1534
|
// Store dashcam preference (default: true)
|
|
1509
1535
|
this.dashcamEnabled = options.dashcam !== false;
|
|
1510
1536
|
|
|
@@ -1790,733 +1816,7 @@ class TestDriverSDK {
|
|
|
1790
1816
|
}
|
|
1791
1817
|
|
|
1792
1818
|
_createProvisionAPI() {
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
const provisionMethods = {
|
|
1796
|
-
/**
|
|
1797
|
-
* Launch Chrome browser
|
|
1798
|
-
* @param {Object} options - Chrome launch options
|
|
1799
|
-
* @param {string} [options.url='http://testdriver-sandbox.vercel.app/'] - URL to navigate to
|
|
1800
|
-
* @param {boolean} [options.maximized=true] - Start maximized
|
|
1801
|
-
* @param {boolean} [options.guest=false] - Use guest mode
|
|
1802
|
-
* @returns {Promise<void>}
|
|
1803
|
-
*/
|
|
1804
|
-
chrome: async (options = {}) => {
|
|
1805
|
-
const {
|
|
1806
|
-
url = "http://testdriver-sandbox.vercel.app/",
|
|
1807
|
-
maximized = true,
|
|
1808
|
-
guest = false,
|
|
1809
|
-
} = options;
|
|
1810
|
-
|
|
1811
|
-
// Store the URL for domain-specific web log tracking
|
|
1812
|
-
self._provisionedChromeUrl = url;
|
|
1813
|
-
|
|
1814
|
-
// Set up Chrome profile with preferences
|
|
1815
|
-
const shell = this.os === "windows" ? "pwsh" : "sh";
|
|
1816
|
-
const userDataDir =
|
|
1817
|
-
this.os === "windows"
|
|
1818
|
-
? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
|
|
1819
|
-
: "/tmp/testdriver-chrome-profile";
|
|
1820
|
-
|
|
1821
|
-
// Create user data directory and Default profile directory
|
|
1822
|
-
const defaultProfileDir =
|
|
1823
|
-
this.os === "windows"
|
|
1824
|
-
? `${userDataDir}\\Default`
|
|
1825
|
-
: `${userDataDir}/Default`;
|
|
1826
|
-
|
|
1827
|
-
const createDirCmd =
|
|
1828
|
-
this.os === "windows"
|
|
1829
|
-
? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
|
|
1830
|
-
: `mkdir -p "${defaultProfileDir}"`;
|
|
1831
|
-
|
|
1832
|
-
await this.exec(shell, createDirCmd, 60000, true);
|
|
1833
|
-
|
|
1834
|
-
// Write Chrome preferences
|
|
1835
|
-
const chromePrefs = {
|
|
1836
|
-
credentials_enable_service: false,
|
|
1837
|
-
profile: {
|
|
1838
|
-
password_manager_enabled: false,
|
|
1839
|
-
default_content_setting_values: {},
|
|
1840
|
-
},
|
|
1841
|
-
signin: {
|
|
1842
|
-
allowed: false,
|
|
1843
|
-
},
|
|
1844
|
-
sync: {
|
|
1845
|
-
requested: false,
|
|
1846
|
-
first_setup_complete: true,
|
|
1847
|
-
sync_all_os_types: false,
|
|
1848
|
-
},
|
|
1849
|
-
autofill: {
|
|
1850
|
-
enabled: false,
|
|
1851
|
-
},
|
|
1852
|
-
local_state: {
|
|
1853
|
-
browser: {
|
|
1854
|
-
has_seen_welcome_page: true,
|
|
1855
|
-
},
|
|
1856
|
-
},
|
|
1857
|
-
};
|
|
1858
|
-
|
|
1859
|
-
const prefsPath =
|
|
1860
|
-
this.os === "windows"
|
|
1861
|
-
? `${defaultProfileDir}\\Preferences`
|
|
1862
|
-
: `${defaultProfileDir}/Preferences`;
|
|
1863
|
-
|
|
1864
|
-
const prefsJson = JSON.stringify(chromePrefs, null, 2);
|
|
1865
|
-
const writePrefCmd =
|
|
1866
|
-
this.os === "windows"
|
|
1867
|
-
? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
|
|
1868
|
-
`[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
|
|
1869
|
-
: `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
|
|
1870
|
-
|
|
1871
|
-
await this.exec(shell, writePrefCmd, 60000, true);
|
|
1872
|
-
|
|
1873
|
-
// Build Chrome launch command
|
|
1874
|
-
const chromeArgs = [];
|
|
1875
|
-
if (maximized) chromeArgs.push("--start-maximized");
|
|
1876
|
-
if (guest) chromeArgs.push("--guest");
|
|
1877
|
-
chromeArgs.push(
|
|
1878
|
-
"--disable-fre",
|
|
1879
|
-
"--no-default-browser-check",
|
|
1880
|
-
"--no-first-run",
|
|
1881
|
-
"--no-experiments",
|
|
1882
|
-
"--disable-infobars",
|
|
1883
|
-
"--disable-features=StartupBrowserCreator",
|
|
1884
|
-
"--disable-features=ChromeWhatsNewUI",
|
|
1885
|
-
`--user-data-dir=${userDataDir}`,
|
|
1886
|
-
);
|
|
1887
|
-
|
|
1888
|
-
// Add remote debugging port for captcha solving support
|
|
1889
|
-
chromeArgs.push("--remote-debugging-port=9222");
|
|
1890
|
-
|
|
1891
|
-
// Add dashcam-chrome extension
|
|
1892
|
-
const dashcamChromePath = await this._getDashcamChromeExtensionPath();
|
|
1893
|
-
if (dashcamChromePath) {
|
|
1894
|
-
chromeArgs.push(`--load-extension=${dashcamChromePath}`);
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
// Launch Chrome
|
|
1898
|
-
|
|
1899
|
-
if (this.os === "windows") {
|
|
1900
|
-
const argsString = chromeArgs.map((arg) => `"${arg}"`).join(", ");
|
|
1901
|
-
await this.exec(
|
|
1902
|
-
shell,
|
|
1903
|
-
`Start-Process "C:\\ChromeForTesting\\chrome-win64\\chrome.exe" -ArgumentList ${argsString}, "${url}"`,
|
|
1904
|
-
30000,
|
|
1905
|
-
);
|
|
1906
|
-
} else {
|
|
1907
|
-
const argsString = chromeArgs.join(" ");
|
|
1908
|
-
await this.exec(
|
|
1909
|
-
shell,
|
|
1910
|
-
`chrome-for-testing ${argsString} "${url}" >/dev/null 2>&1 &`,
|
|
1911
|
-
30000,
|
|
1912
|
-
);
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
// Wait for Chrome debugger port and page to be ready
|
|
1916
|
-
await this._waitForChromeDebuggerReady();
|
|
1917
|
-
await this.focusApplication("Google Chrome");
|
|
1918
|
-
|
|
1919
|
-
// Add web log tracking with domain wildcard pattern, then start dashcam
|
|
1920
|
-
if (this.dashcamEnabled) {
|
|
1921
|
-
const domainPattern = this._getUrlDomainPattern(url);
|
|
1922
|
-
await this.dashcam.addWebLog(domainPattern, "Web Logs");
|
|
1923
|
-
|
|
1924
|
-
// Start dashcam recording after logs are configured
|
|
1925
|
-
if (!(await this.dashcam.isRecording())) {
|
|
1926
|
-
await this.dashcam.start();
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
},
|
|
1930
|
-
|
|
1931
|
-
/**
|
|
1932
|
-
* Launch Chrome browser with a custom extension loaded
|
|
1933
|
-
* @param {Object} options - Chrome extension launch options
|
|
1934
|
-
* @param {string} [options.extensionPath] - Local filesystem path to the unpacked extension directory
|
|
1935
|
-
* @param {string} [options.extensionId] - Chrome Web Store extension ID (e.g., "cjpalhdlnbpafiamejdnhcphjbkeiagm" for uBlock Origin)
|
|
1936
|
-
* @param {boolean} [options.maximized=true] - Start maximized
|
|
1937
|
-
* @returns {Promise<void>}
|
|
1938
|
-
* @example
|
|
1939
|
-
* // Load extension from local path
|
|
1940
|
-
* await testdriver.exec('sh', 'git clone https://github.com/user/extension.git /tmp/extension');
|
|
1941
|
-
* await testdriver.provision.chromeExtension({
|
|
1942
|
-
* extensionPath: '/tmp/extension'
|
|
1943
|
-
* });
|
|
1944
|
-
*
|
|
1945
|
-
* @example
|
|
1946
|
-
* // Load extension by Chrome Web Store ID
|
|
1947
|
-
* await testdriver.provision.chromeExtension({
|
|
1948
|
-
* extensionId: 'cjpalhdlnbpafiamejdnhcphjbkeiagm' // uBlock Origin
|
|
1949
|
-
* });
|
|
1950
|
-
*/
|
|
1951
|
-
chromeExtension: async (options = {}) => {
|
|
1952
|
-
const {
|
|
1953
|
-
extensionPath: providedExtensionPath,
|
|
1954
|
-
extensionId,
|
|
1955
|
-
maximized = true,
|
|
1956
|
-
} = options;
|
|
1957
|
-
|
|
1958
|
-
if (!providedExtensionPath && !extensionId) {
|
|
1959
|
-
throw new Error(
|
|
1960
|
-
"[provision.chromeExtension] Either extensionPath or extensionId is required",
|
|
1961
|
-
);
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
let extensionPath = providedExtensionPath;
|
|
1965
|
-
const shell = this.os === "windows" ? "pwsh" : "sh";
|
|
1966
|
-
|
|
1967
|
-
// If extensionId is provided, download and extract the extension from Chrome Web Store
|
|
1968
|
-
if (extensionId && !extensionPath) {
|
|
1969
|
-
console.log(
|
|
1970
|
-
`[provision.chromeExtension] Downloading extension ${extensionId} from Chrome Web Store...`,
|
|
1971
|
-
);
|
|
1972
|
-
|
|
1973
|
-
const extensionDir =
|
|
1974
|
-
this.os === "windows"
|
|
1975
|
-
? `C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Extensions\\${extensionId}`
|
|
1976
|
-
: `/tmp/testdriver-extensions/${extensionId}`;
|
|
1977
|
-
|
|
1978
|
-
// Create extension directory
|
|
1979
|
-
const mkdirCmd =
|
|
1980
|
-
this.os === "windows"
|
|
1981
|
-
? `New-Item -ItemType Directory -Path "${extensionDir}" -Force | Out-Null`
|
|
1982
|
-
: `mkdir -p "${extensionDir}"`;
|
|
1983
|
-
await this.exec(shell, mkdirCmd, 60000, true);
|
|
1984
|
-
|
|
1985
|
-
// Download CRX from Chrome Web Store
|
|
1986
|
-
// The CRX download URL format for Chrome Web Store
|
|
1987
|
-
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`;
|
|
1988
|
-
const crxPath =
|
|
1989
|
-
this.os === "windows"
|
|
1990
|
-
? `${extensionDir}\\extension.crx`
|
|
1991
|
-
: `${extensionDir}/extension.crx`;
|
|
1992
|
-
|
|
1993
|
-
if (this.os === "windows") {
|
|
1994
|
-
await this.exec(
|
|
1995
|
-
"pwsh",
|
|
1996
|
-
`Invoke-WebRequest -Uri "${crxUrl}" -OutFile "${crxPath}"`,
|
|
1997
|
-
60000,
|
|
1998
|
-
true,
|
|
1999
|
-
);
|
|
2000
|
-
} else {
|
|
2001
|
-
await this.exec(
|
|
2002
|
-
"sh",
|
|
2003
|
-
`curl -L -o "${crxPath}" "${crxUrl}"`,
|
|
2004
|
-
60000,
|
|
2005
|
-
true,
|
|
2006
|
-
);
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
// Extract the CRX file (CRX is a ZIP with a header)
|
|
2010
|
-
// Skip the CRX header and extract as ZIP
|
|
2011
|
-
if (this.os === "windows") {
|
|
2012
|
-
// PowerShell: Read CRX, skip header, extract ZIP
|
|
2013
|
-
await this.exec(
|
|
2014
|
-
"pwsh",
|
|
2015
|
-
`
|
|
2016
|
-
$crxBytes = [System.IO.File]::ReadAllBytes("${crxPath}")
|
|
2017
|
-
# CRX3 header: 4 bytes magic + 4 bytes version + 4 bytes header length + header
|
|
2018
|
-
$magic = [System.Text.Encoding]::ASCII.GetString($crxBytes[0..3])
|
|
2019
|
-
if ($magic -eq "Cr24") {
|
|
2020
|
-
$headerLen = [BitConverter]::ToUInt32($crxBytes, 8)
|
|
2021
|
-
$zipStart = 12 + $headerLen
|
|
2022
|
-
} else {
|
|
2023
|
-
# CRX2 format
|
|
2024
|
-
$zipStart = 16 + [BitConverter]::ToUInt32($crxBytes, 8) + [BitConverter]::ToUInt32($crxBytes, 12)
|
|
2025
|
-
}
|
|
2026
|
-
$zipBytes = $crxBytes[$zipStart..($crxBytes.Length - 1)]
|
|
2027
|
-
$zipPath = "${extensionDir}\\extension.zip"
|
|
2028
|
-
[System.IO.File]::WriteAllBytes($zipPath, $zipBytes)
|
|
2029
|
-
Expand-Archive -Path $zipPath -DestinationPath "${extensionDir}\\unpacked" -Force
|
|
2030
|
-
`,
|
|
2031
|
-
30000,
|
|
2032
|
-
true,
|
|
2033
|
-
);
|
|
2034
|
-
extensionPath = `${extensionDir}\\unpacked`;
|
|
2035
|
-
} else {
|
|
2036
|
-
// Linux: Use unzip with offset or python to extract
|
|
2037
|
-
await this.exec(
|
|
2038
|
-
"sh",
|
|
2039
|
-
`
|
|
2040
|
-
cd "${extensionDir}"
|
|
2041
|
-
# Extract CRX (skip header and unzip)
|
|
2042
|
-
# CRX3 format: magic(4) + version(4) + header_length(4) + header + zip
|
|
2043
|
-
python3 -c "
|
|
2044
|
-
import struct
|
|
2045
|
-
import zipfile
|
|
2046
|
-
import io
|
|
2047
|
-
import os
|
|
2048
|
-
|
|
2049
|
-
with open('extension.crx', 'rb') as f:
|
|
2050
|
-
data = f.read()
|
|
2051
|
-
|
|
2052
|
-
# Check magic number
|
|
2053
|
-
magic = data[:4]
|
|
2054
|
-
if magic == b'Cr24':
|
|
2055
|
-
# CRX3 format
|
|
2056
|
-
header_len = struct.unpack('<I', data[8:12])[0]
|
|
2057
|
-
zip_start = 12 + header_len
|
|
2058
|
-
else:
|
|
2059
|
-
# CRX2 format
|
|
2060
|
-
pub_key_len = struct.unpack('<I', data[8:12])[0]
|
|
2061
|
-
sig_len = struct.unpack('<I', data[12:16])[0]
|
|
2062
|
-
zip_start = 16 + pub_key_len + sig_len
|
|
2063
|
-
|
|
2064
|
-
zip_data = data[zip_start:]
|
|
2065
|
-
os.makedirs('unpacked', exist_ok=True)
|
|
2066
|
-
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
|
2067
|
-
zf.extractall('unpacked')
|
|
2068
|
-
"
|
|
2069
|
-
`,
|
|
2070
|
-
30000,
|
|
2071
|
-
true,
|
|
2072
|
-
);
|
|
2073
|
-
extensionPath = `${extensionDir}/unpacked`;
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
console.log(
|
|
2077
|
-
`[provision.chromeExtension] Extension ${extensionId} extracted to ${extensionPath}`,
|
|
2078
|
-
);
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
// Set up Chrome profile with preferences
|
|
2082
|
-
const userDataDir =
|
|
2083
|
-
this.os === "windows"
|
|
2084
|
-
? "C:\\Users\\testdriver\\AppData\\Local\\TestDriver\\Chrome"
|
|
2085
|
-
: "/tmp/testdriver-chrome-profile";
|
|
2086
|
-
|
|
2087
|
-
// Create user data directory and Default profile directory
|
|
2088
|
-
const defaultProfileDir =
|
|
2089
|
-
this.os === "windows"
|
|
2090
|
-
? `${userDataDir}\\Default`
|
|
2091
|
-
: `${userDataDir}/Default`;
|
|
2092
|
-
|
|
2093
|
-
const createDirCmd =
|
|
2094
|
-
this.os === "windows"
|
|
2095
|
-
? `New-Item -ItemType Directory -Path "${defaultProfileDir}" -Force | Out-Null`
|
|
2096
|
-
: `mkdir -p "${defaultProfileDir}"`;
|
|
2097
|
-
|
|
2098
|
-
await this.exec(shell, createDirCmd, 60000, true);
|
|
2099
|
-
|
|
2100
|
-
// Write Chrome preferences
|
|
2101
|
-
const chromePrefs = {
|
|
2102
|
-
credentials_enable_service: false,
|
|
2103
|
-
profile: {
|
|
2104
|
-
password_manager_enabled: false,
|
|
2105
|
-
default_content_setting_values: {},
|
|
2106
|
-
},
|
|
2107
|
-
signin: {
|
|
2108
|
-
allowed: false,
|
|
2109
|
-
},
|
|
2110
|
-
sync: {
|
|
2111
|
-
requested: false,
|
|
2112
|
-
first_setup_complete: true,
|
|
2113
|
-
sync_all_os_types: false,
|
|
2114
|
-
},
|
|
2115
|
-
autofill: {
|
|
2116
|
-
enabled: false,
|
|
2117
|
-
},
|
|
2118
|
-
local_state: {
|
|
2119
|
-
browser: {
|
|
2120
|
-
has_seen_welcome_page: true,
|
|
2121
|
-
},
|
|
2122
|
-
},
|
|
2123
|
-
};
|
|
2124
|
-
|
|
2125
|
-
const prefsPath =
|
|
2126
|
-
this.os === "windows"
|
|
2127
|
-
? `${defaultProfileDir}\\Preferences`
|
|
2128
|
-
: `${defaultProfileDir}/Preferences`;
|
|
2129
|
-
|
|
2130
|
-
const prefsJson = JSON.stringify(chromePrefs, null, 2);
|
|
2131
|
-
const writePrefCmd =
|
|
2132
|
-
this.os === "windows"
|
|
2133
|
-
? // Use compact JSON and [System.IO.File]::WriteAllText to avoid Set-Content hanging issues
|
|
2134
|
-
`[System.IO.File]::WriteAllText("${prefsPath}", '${JSON.stringify(chromePrefs).replace(/'/g, "''")}')`
|
|
2135
|
-
: `cat > "${prefsPath}" << 'EOF'\n${prefsJson}\nEOF`;
|
|
2136
|
-
|
|
2137
|
-
await this.exec(shell, writePrefCmd, 60000, true);
|
|
2138
|
-
|
|
2139
|
-
// Build Chrome launch command
|
|
2140
|
-
const chromeArgs = [];
|
|
2141
|
-
if (maximized) chromeArgs.push("--start-maximized");
|
|
2142
|
-
chromeArgs.push(
|
|
2143
|
-
"--disable-fre",
|
|
2144
|
-
"--no-default-browser-check",
|
|
2145
|
-
"--no-first-run",
|
|
2146
|
-
"--no-experiments",
|
|
2147
|
-
"--disable-infobars",
|
|
2148
|
-
"--disable-features=ChromeLabs",
|
|
2149
|
-
`--user-data-dir=${userDataDir}`,
|
|
2150
|
-
);
|
|
2151
|
-
|
|
2152
|
-
// Add remote debugging port for captcha solving support
|
|
2153
|
-
chromeArgs.push("--remote-debugging-port=9222");
|
|
2154
|
-
|
|
2155
|
-
// Add user extension and dashcam-chrome extension
|
|
2156
|
-
const dashcamChromePath = await this._getDashcamChromeExtensionPath();
|
|
2157
|
-
if (dashcamChromePath) {
|
|
2158
|
-
// Load both user extension and dashcam-chrome for web log capture
|
|
2159
|
-
chromeArgs.push(
|
|
2160
|
-
`--load-extension=${extensionPath},${dashcamChromePath}`,
|
|
2161
|
-
);
|
|
2162
|
-
} else {
|
|
2163
|
-
// If dashcam-chrome unavailable, just load user extension
|
|
2164
|
-
chromeArgs.push(`--load-extension=${extensionPath}`);
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
// Launch Chrome (opens to New Tab by default)
|
|
2168
|
-
if (this.os === "windows") {
|
|
2169
|
-
const argsString = chromeArgs.map((arg) => `"${arg}"`).join(", ");
|
|
2170
|
-
await this.exec(
|
|
2171
|
-
shell,
|
|
2172
|
-
`Start-Process "C:\\ChromeForTesting\\chrome-win64\\chrome.exe" -ArgumentList ${argsString}`,
|
|
2173
|
-
30000,
|
|
2174
|
-
);
|
|
2175
|
-
} else {
|
|
2176
|
-
const argsString = chromeArgs.join(" ");
|
|
2177
|
-
await this.exec(
|
|
2178
|
-
shell,
|
|
2179
|
-
`chrome-for-testing ${argsString} >/dev/null 2>&1 &`,
|
|
2180
|
-
30000,
|
|
2181
|
-
);
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
// Wait for Chrome debugger port and page to be ready
|
|
2185
|
-
await this._waitForChromeDebuggerReady();
|
|
2186
|
-
await this.focusApplication("Google Chrome");
|
|
2187
|
-
|
|
2188
|
-
// Start dashcam recording
|
|
2189
|
-
if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
|
|
2190
|
-
await this.dashcam.start();
|
|
2191
|
-
}
|
|
2192
|
-
},
|
|
2193
|
-
|
|
2194
|
-
/**
|
|
2195
|
-
* Launch VS Code
|
|
2196
|
-
* @param {Object} options - VS Code launch options
|
|
2197
|
-
* @param {string} [options.workspace] - Workspace/folder to open
|
|
2198
|
-
* @param {string[]} [options.extensions=[]] - Extensions to install
|
|
2199
|
-
* @returns {Promise<void>}
|
|
2200
|
-
*/
|
|
2201
|
-
vscode: async (options = {}) => {
|
|
2202
|
-
const { workspace = null, extensions = [] } = options;
|
|
2203
|
-
|
|
2204
|
-
const shell = this.os === "windows" ? "pwsh" : "sh";
|
|
2205
|
-
|
|
2206
|
-
// Install extensions if provided
|
|
2207
|
-
for (const extension of extensions) {
|
|
2208
|
-
console.log(`[provision.vscode] Installing extension: ${extension}`);
|
|
2209
|
-
await this.exec(
|
|
2210
|
-
shell,
|
|
2211
|
-
`code --install-extension ${extension} --force`,
|
|
2212
|
-
120000,
|
|
2213
|
-
true,
|
|
2214
|
-
);
|
|
2215
|
-
console.log(
|
|
2216
|
-
`[provision.vscode] ✅ Extension installed: ${extension}`,
|
|
2217
|
-
);
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
// Launch VS Code
|
|
2221
|
-
const workspaceArg = workspace ? `"${workspace}"` : "";
|
|
2222
|
-
|
|
2223
|
-
if (this.os === "windows") {
|
|
2224
|
-
await this.exec(
|
|
2225
|
-
shell,
|
|
2226
|
-
`Start-Process code -ArgumentList ${workspaceArg}`,
|
|
2227
|
-
30000,
|
|
2228
|
-
);
|
|
2229
|
-
} else {
|
|
2230
|
-
await this.exec(
|
|
2231
|
-
shell,
|
|
2232
|
-
`code ${workspaceArg} >/dev/null 2>&1 &`,
|
|
2233
|
-
30000,
|
|
2234
|
-
);
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
// Wait for VS Code to start up
|
|
2238
|
-
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
2239
|
-
|
|
2240
|
-
// Wait for VS Code to be ready
|
|
2241
|
-
await this.focusApplication("Visual Studio Code");
|
|
2242
|
-
|
|
2243
|
-
// Start dashcam recording
|
|
2244
|
-
if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
|
|
2245
|
-
await this.dashcam.start();
|
|
2246
|
-
}
|
|
2247
|
-
},
|
|
2248
|
-
|
|
2249
|
-
/**
|
|
2250
|
-
* Download and install an application
|
|
2251
|
-
* @param {Object} options - Installer options
|
|
2252
|
-
* @param {string} options.url - URL to download the installer from
|
|
2253
|
-
* @param {string} [options.filename] - Filename to save as (auto-detected from URL if not provided)
|
|
2254
|
-
* @param {string} [options.appName] - Application name to focus after install
|
|
2255
|
-
* @param {boolean} [options.launch=true] - Whether to launch the app after installation
|
|
2256
|
-
* @returns {Promise<string>} Path to the downloaded file
|
|
2257
|
-
* @example
|
|
2258
|
-
* // Install a .deb package on Linux (auto-detected)
|
|
2259
|
-
* await testdriver.provision.installer({
|
|
2260
|
-
* url: 'https://example.com/app.deb',
|
|
2261
|
-
* appName: 'MyApp'
|
|
2262
|
-
* });
|
|
2263
|
-
*
|
|
2264
|
-
* @example
|
|
2265
|
-
* // Download and run custom commands
|
|
2266
|
-
* const filePath = await testdriver.provision.installer({
|
|
2267
|
-
* url: 'https://example.com/app.AppImage',
|
|
2268
|
-
* launch: false
|
|
2269
|
-
* });
|
|
2270
|
-
* await testdriver.exec('sh', `chmod +x "${filePath}" && "${filePath}" &`, 10000);
|
|
2271
|
-
*/
|
|
2272
|
-
installer: async (options = {}) => {
|
|
2273
|
-
const { url, filename, appName, launch = true } = options;
|
|
2274
|
-
|
|
2275
|
-
if (!url) {
|
|
2276
|
-
throw new Error("[provision.installer] url is required");
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
const shell = this.os === "windows" ? "pwsh" : "sh";
|
|
2280
|
-
|
|
2281
|
-
// Determine download directory
|
|
2282
|
-
const downloadDir =
|
|
2283
|
-
this.os === "windows" ? "C:\\Users\\testdriver\\Downloads" : "/tmp";
|
|
2284
|
-
|
|
2285
|
-
console.log(`[provision.installer] Downloading ${url}...`);
|
|
2286
|
-
|
|
2287
|
-
let actualFilePath;
|
|
2288
|
-
|
|
2289
|
-
// Download the file and get the actual filename (handles redirects)
|
|
2290
|
-
if (this.os === "windows") {
|
|
2291
|
-
// Simple approach: download first, then get the actual filename from the response
|
|
2292
|
-
const tempFile = `${downloadDir}\\installer_temp_${Date.now()}`;
|
|
2293
|
-
|
|
2294
|
-
const downloadScript = `
|
|
2295
|
-
$ProgressPreference = 'SilentlyContinue'
|
|
2296
|
-
$response = Invoke-WebRequest -Uri "${url}" -OutFile "${tempFile}" -PassThru -UseBasicParsing
|
|
2297
|
-
|
|
2298
|
-
# Try to get filename from Content-Disposition header
|
|
2299
|
-
$filename = $null
|
|
2300
|
-
if ($response.Headers['Content-Disposition']) {
|
|
2301
|
-
if ($response.Headers['Content-Disposition'] -match 'filename=\\"?([^\\"]+)\\"?') {
|
|
2302
|
-
$filename = $matches[1]
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
# If no filename from header, try to get from URL or use default
|
|
2307
|
-
if (-not $filename) {
|
|
2308
|
-
$uri = [System.Uri]"${url}"
|
|
2309
|
-
$filename = [System.IO.Path]::GetFileName($uri.LocalPath)
|
|
2310
|
-
if (-not $filename -or $filename -eq '') {
|
|
2311
|
-
$filename = "installer"
|
|
2312
|
-
}
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
# Move temp file to final location with proper filename
|
|
2316
|
-
$finalPath = Join-Path "${downloadDir}" $filename
|
|
2317
|
-
Move-Item -Path "${tempFile}" -Destination $finalPath -Force
|
|
2318
|
-
Write-Output $finalPath
|
|
2319
|
-
`;
|
|
2320
|
-
|
|
2321
|
-
const result = await this.exec(shell, downloadScript, 300000, true);
|
|
2322
|
-
actualFilePath = result ? result.trim() : null;
|
|
2323
|
-
|
|
2324
|
-
if (!actualFilePath) {
|
|
2325
|
-
throw new Error("[provision.installer] Failed to download file");
|
|
2326
|
-
}
|
|
2327
|
-
} else {
|
|
2328
|
-
// Use curl with options to get the final filename
|
|
2329
|
-
const tempMarker = `installer_${Date.now()}`;
|
|
2330
|
-
const downloadScript = `
|
|
2331
|
-
cd "${downloadDir}"
|
|
2332
|
-
curl -L -J -O -w "%{filename_effective}" "${url}" 2>/dev/null || echo "${tempMarker}"
|
|
2333
|
-
`;
|
|
2334
|
-
|
|
2335
|
-
const result = await this.exec(shell, downloadScript, 300000, true);
|
|
2336
|
-
const downloadedFile = result ? result.trim() : null;
|
|
2337
|
-
|
|
2338
|
-
if (downloadedFile && downloadedFile !== tempMarker) {
|
|
2339
|
-
actualFilePath = `${downloadDir}/${downloadedFile}`;
|
|
2340
|
-
} else {
|
|
2341
|
-
// Fallback: use curl without -J and specify output file
|
|
2342
|
-
const fallbackFilename = filename || "installer";
|
|
2343
|
-
actualFilePath = `${downloadDir}/${fallbackFilename}`;
|
|
2344
|
-
await this.exec(
|
|
2345
|
-
shell,
|
|
2346
|
-
`curl -L -o "${actualFilePath}" "${url}"`,
|
|
2347
|
-
300000,
|
|
2348
|
-
true,
|
|
2349
|
-
);
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
console.log(`[provision.installer] ✅ Downloaded to ${actualFilePath}`);
|
|
2354
|
-
|
|
2355
|
-
// Auto-detect install command based on file extension (use actualFilePath for extension detection)
|
|
2356
|
-
const actualFilename = actualFilePath.split(/[/\\]/).pop() || "";
|
|
2357
|
-
const ext = actualFilename.split(".").pop()?.toLowerCase();
|
|
2358
|
-
let installCommand = null;
|
|
2359
|
-
|
|
2360
|
-
if (this.os === "windows") {
|
|
2361
|
-
if (ext === "msi") {
|
|
2362
|
-
installCommand = `Start-Process msiexec -ArgumentList '/i', '"${actualFilePath}"', '/quiet', '/norestart' -Wait`;
|
|
2363
|
-
} else if (ext === "exe") {
|
|
2364
|
-
installCommand = `Start-Process "${actualFilePath}" -ArgumentList '/S' -Wait`;
|
|
2365
|
-
}
|
|
2366
|
-
} else if (this.os === "linux") {
|
|
2367
|
-
if (ext === "deb") {
|
|
2368
|
-
installCommand = `sudo dpkg -i "${actualFilePath}" && sudo apt-get install -f -y`;
|
|
2369
|
-
} else if (ext === "rpm") {
|
|
2370
|
-
installCommand = `sudo rpm -i "${actualFilePath}"`;
|
|
2371
|
-
} else if (ext === "appimage") {
|
|
2372
|
-
installCommand = `chmod +x "${actualFilePath}"`;
|
|
2373
|
-
} else if (ext === "sh") {
|
|
2374
|
-
installCommand = `chmod +x "${actualFilePath}" && "${actualFilePath}"`;
|
|
2375
|
-
}
|
|
2376
|
-
} else if (this.os === "darwin") {
|
|
2377
|
-
if (ext === "dmg") {
|
|
2378
|
-
installCommand = `hdiutil attach "${actualFilePath}" -mountpoint /Volumes/installer && cp -R /Volumes/installer/*.app /Applications/ && hdiutil detach /Volumes/installer`;
|
|
2379
|
-
} else if (ext === "pkg") {
|
|
2380
|
-
installCommand = `sudo installer -pkg "${actualFilePath}" -target /`;
|
|
2381
|
-
}
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
if (installCommand) {
|
|
2385
|
-
console.log(`[provision.installer] Installing...`);
|
|
2386
|
-
await this.exec(shell, installCommand, 300000, true);
|
|
2387
|
-
console.log(`[provision.installer] ✅ Installation complete`);
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
// Launch and focus the app if appName is provided and launch is true
|
|
2391
|
-
if (appName && launch) {
|
|
2392
|
-
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
2393
|
-
await this.focusApplication(appName);
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
// Start dashcam recording
|
|
2397
|
-
if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
|
|
2398
|
-
await this.dashcam.start();
|
|
2399
|
-
}
|
|
2400
|
-
|
|
2401
|
-
return actualFilePath;
|
|
2402
|
-
},
|
|
2403
|
-
|
|
2404
|
-
/**
|
|
2405
|
-
* Launch Electron app
|
|
2406
|
-
* @param {Object} options - Electron launch options
|
|
2407
|
-
* @param {string} options.appPath - Path to Electron app (required)
|
|
2408
|
-
* @param {string[]} [options.args=[]] - Additional electron args
|
|
2409
|
-
* @returns {Promise<void>}
|
|
2410
|
-
*/
|
|
2411
|
-
electron: async (options = {}) => {
|
|
2412
|
-
const { appPath, args = [] } = options;
|
|
2413
|
-
|
|
2414
|
-
if (!appPath) {
|
|
2415
|
-
throw new Error("provision.electron requires appPath option");
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
const shell = this.os === "windows" ? "pwsh" : "sh";
|
|
2419
|
-
|
|
2420
|
-
const argsString = args.join(" ");
|
|
2421
|
-
|
|
2422
|
-
if (this.os === "windows") {
|
|
2423
|
-
await this.exec(
|
|
2424
|
-
shell,
|
|
2425
|
-
`Start-Process electron -ArgumentList "${appPath}", ${argsString}`,
|
|
2426
|
-
30000,
|
|
2427
|
-
);
|
|
2428
|
-
} else {
|
|
2429
|
-
await this.exec(
|
|
2430
|
-
shell,
|
|
2431
|
-
`electron "${appPath}" ${argsString} >/dev/null 2>&1 &`,
|
|
2432
|
-
30000,
|
|
2433
|
-
);
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
await this.focusApplication("Electron");
|
|
2437
|
-
|
|
2438
|
-
// Start dashcam recording
|
|
2439
|
-
if (this.dashcamEnabled && !(await this.dashcam.isRecording())) {
|
|
2440
|
-
await this.dashcam.start();
|
|
2441
|
-
}
|
|
2442
|
-
},
|
|
2443
|
-
|
|
2444
|
-
/**
|
|
2445
|
-
* Initialize Dashcam recording with logging
|
|
2446
|
-
* @param {Object} options - Dashcam options
|
|
2447
|
-
* @param {string} [options.logPath] - Path to log file (auto-generated if not provided)
|
|
2448
|
-
* @param {string} [options.logName='TestDriver Log'] - Display name for the log
|
|
2449
|
-
* @param {boolean} [options.webLogs=true] - Enable web log tracking
|
|
2450
|
-
* @param {string} [options.title] - Custom title for the recording
|
|
2451
|
-
* @returns {Promise<void>}
|
|
2452
|
-
*/
|
|
2453
|
-
dashcam: async (options = {}) => {
|
|
2454
|
-
const {
|
|
2455
|
-
logPath,
|
|
2456
|
-
logName = "TestDriver Log",
|
|
2457
|
-
webLogs = true,
|
|
2458
|
-
title,
|
|
2459
|
-
} = options;
|
|
2460
|
-
|
|
2461
|
-
// Ensure dashcam is enabled
|
|
2462
|
-
if (!this.dashcamEnabled) {
|
|
2463
|
-
console.warn(
|
|
2464
|
-
"[provision.dashcam] Dashcam is not enabled. Skipping.",
|
|
2465
|
-
);
|
|
2466
|
-
return;
|
|
2467
|
-
}
|
|
2468
|
-
|
|
2469
|
-
// Set custom title if provided
|
|
2470
|
-
if (title) {
|
|
2471
|
-
this.dashcam.setTitle(title);
|
|
2472
|
-
}
|
|
2473
|
-
|
|
2474
|
-
// Add file log tracking
|
|
2475
|
-
const actualLogPath =
|
|
2476
|
-
logPath ||
|
|
2477
|
-
(this.os === "windows"
|
|
2478
|
-
? "C:\\Users\\testdriver\\testdriver.log"
|
|
2479
|
-
: "/tmp/testdriver.log");
|
|
2480
|
-
|
|
2481
|
-
await this.dashcam.addFileLog(actualLogPath, logName);
|
|
2482
|
-
|
|
2483
|
-
// Add web log tracking if enabled
|
|
2484
|
-
// Use domain pattern from provisioned Chrome URL if available
|
|
2485
|
-
if (webLogs) {
|
|
2486
|
-
const pattern = this._provisionedChromeUrl
|
|
2487
|
-
? this._getUrlDomainPattern(this._provisionedChromeUrl)
|
|
2488
|
-
: "**";
|
|
2489
|
-
await this.dashcam.addWebLog(pattern, "Web Logs");
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
// Start recording if not already recording
|
|
2493
|
-
if (!(await this.dashcam.isRecording())) {
|
|
2494
|
-
await this.dashcam.start();
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
console.log("[provision.dashcam] ✅ Dashcam recording started");
|
|
2498
|
-
},
|
|
2499
|
-
};
|
|
2500
|
-
|
|
2501
|
-
// Wrap all provision methods with reconnect check using Proxy
|
|
2502
|
-
return new Proxy(provisionMethods, {
|
|
2503
|
-
get(target, prop) {
|
|
2504
|
-
const method = target[prop];
|
|
2505
|
-
if (typeof method === "function") {
|
|
2506
|
-
return async (...args) => {
|
|
2507
|
-
// Skip provisioning if reconnecting to existing sandbox
|
|
2508
|
-
if (self.reconnect) {
|
|
2509
|
-
console.log(
|
|
2510
|
-
`[provision.${prop}] Skipping provisioning (reconnect mode)`,
|
|
2511
|
-
);
|
|
2512
|
-
return;
|
|
2513
|
-
}
|
|
2514
|
-
return method(...args);
|
|
2515
|
-
};
|
|
2516
|
-
}
|
|
2517
|
-
return method;
|
|
2518
|
-
},
|
|
2519
|
-
});
|
|
1819
|
+
return createProvisionAPI(this);
|
|
2520
1820
|
}
|
|
2521
1821
|
|
|
2522
1822
|
/**
|
|
@@ -2787,6 +2087,9 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2787
2087
|
// Set agent properties for buildEnv to use
|
|
2788
2088
|
if (connectOptions.sandboxId) {
|
|
2789
2089
|
this.agent.sandboxId = connectOptions.sandboxId;
|
|
2090
|
+
} else if (this.sandboxId) {
|
|
2091
|
+
// Constructor-provided sandboxId (explicit or loaded from .testdriver/last-sandbox)
|
|
2092
|
+
this.agent.sandboxId = this.sandboxId;
|
|
2790
2093
|
}
|
|
2791
2094
|
// Use IP from connectOptions if provided, otherwise fall back to constructor IP
|
|
2792
2095
|
if (connectOptions.ip !== undefined) {
|
|
@@ -2826,9 +2129,11 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2826
2129
|
} else {
|
|
2827
2130
|
this.agent.sandboxOs = this.os;
|
|
2828
2131
|
}
|
|
2829
|
-
// Use keepAlive from connectOptions if provided
|
|
2132
|
+
// Use keepAlive from connectOptions if provided, otherwise fall back to constructor option
|
|
2830
2133
|
if (connectOptions.keepAlive !== undefined) {
|
|
2831
2134
|
this.agent.keepAlive = connectOptions.keepAlive;
|
|
2135
|
+
} else if (this.keepAlive !== undefined) {
|
|
2136
|
+
this.agent.keepAlive = this.keepAlive;
|
|
2832
2137
|
}
|
|
2833
2138
|
|
|
2834
2139
|
// Set redrawThreshold on agent's cliArgs.options
|
|
@@ -2883,6 +2188,20 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
2883
2188
|
sandboxId: this.instance?.instanceId,
|
|
2884
2189
|
});
|
|
2885
2190
|
|
|
2191
|
+
// Persist the active sandbox id so a later run with `reconnect: true`
|
|
2192
|
+
// can reattach without the caller having to thread the id through.
|
|
2193
|
+
const activeSandboxId =
|
|
2194
|
+
this.instance?.sandboxId || this.instance?.instanceId || null;
|
|
2195
|
+
if (activeSandboxId) {
|
|
2196
|
+
this.sandboxId = activeSandboxId;
|
|
2197
|
+
TestDriverSDK._writeLastSandbox({
|
|
2198
|
+
sandboxId: activeSandboxId,
|
|
2199
|
+
os: this.os,
|
|
2200
|
+
e2bTemplateId: this.e2bTemplateId || null,
|
|
2201
|
+
createdAt: Date.now(),
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2886
2205
|
return this.instance;
|
|
2887
2206
|
}
|
|
2888
2207
|
|
|
@@ -3088,7 +2407,8 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3088
2407
|
const { events } = require("./agent/events.js");
|
|
3089
2408
|
|
|
3090
2409
|
try {
|
|
3091
|
-
|
|
2410
|
+
// { imageKey } (fast S3-key path) or { image } (base64 fallback).
|
|
2411
|
+
const imagePayload = await this.system.captureScreenImage();
|
|
3092
2412
|
|
|
3093
2413
|
// Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold/cache
|
|
3094
2414
|
let cacheKey = null;
|
|
@@ -3157,7 +2477,7 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3157
2477
|
{
|
|
3158
2478
|
session: this.getSessionId(),
|
|
3159
2479
|
element: description,
|
|
3160
|
-
|
|
2480
|
+
...imagePayload,
|
|
3161
2481
|
threshold: threshold,
|
|
3162
2482
|
cacheKey: cacheKey,
|
|
3163
2483
|
os: this.os,
|
|
@@ -3196,11 +2516,12 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3196
2516
|
elementData,
|
|
3197
2517
|
);
|
|
3198
2518
|
|
|
3199
|
-
// Only store screenshot in DEBUG mode
|
|
2519
|
+
// Only store screenshot in DEBUG mode (and only when we have the
|
|
2520
|
+
// bytes locally — the fast imageKey path doesn't download them)
|
|
3200
2521
|
const debugMode =
|
|
3201
2522
|
process.env.VERBOSE || process.env.TD_DEBUG;
|
|
3202
|
-
if (debugMode) {
|
|
3203
|
-
element._screenshot =
|
|
2523
|
+
if (debugMode && imagePayload.image) {
|
|
2524
|
+
element._screenshot = imagePayload.image;
|
|
3204
2525
|
}
|
|
3205
2526
|
|
|
3206
2527
|
return element;
|
|
@@ -3684,11 +3005,9 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3684
3005
|
const { events } = require("./agent/events.js");
|
|
3685
3006
|
this.emitter.emit(events.log.log, "🔍 Running OmniParser screen analysis...");
|
|
3686
3007
|
|
|
3687
|
-
const screenshot = await this.system.captureScreenBase64();
|
|
3688
|
-
|
|
3689
3008
|
const response = await this.apiClient.req("parse", {
|
|
3690
3009
|
session: this.getSessionId(),
|
|
3691
|
-
|
|
3010
|
+
...(await this.system.captureScreenImage()),
|
|
3692
3011
|
});
|
|
3693
3012
|
|
|
3694
3013
|
if (response.error) {
|
|
@@ -3845,8 +3164,9 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
3845
3164
|
const apiRoot = this.config?.TD_API_ROOT || 'unknown';
|
|
3846
3165
|
const apiKey = this.config?.TD_API_KEY || '';
|
|
3847
3166
|
const maskedKey = apiKey.length > 4 ? '***' + apiKey.slice(-4) : '(not set)';
|
|
3848
|
-
const
|
|
3849
|
-
const
|
|
3167
|
+
const channelConfig = require('./lib/resolve-channel.js');
|
|
3168
|
+
const env = process.env.TD_CHANNEL || process.env.TD_ENV || channelConfig.active || 'unknown';
|
|
3169
|
+
const os = this.os || this.agent?.options?.os || process.env.TD_OS || 'linux';
|
|
3850
3170
|
const sdkVersion = require('./package.json').version;
|
|
3851
3171
|
|
|
3852
3172
|
// Always print local config immediately
|
|
@@ -4345,7 +3665,11 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
4345
3665
|
if (this._logBuffer.length === 0) return "";
|
|
4346
3666
|
const startTime = this._logBuffer[0].time;
|
|
4347
3667
|
return this._logBuffer
|
|
4348
|
-
.map((entry) =>
|
|
3668
|
+
.map((entry, index) => {
|
|
3669
|
+
const obj = { ...entry, time: entry.time - startTime };
|
|
3670
|
+
if (index === 0) obj.epoch = startTime; // absolute epoch for video alignment
|
|
3671
|
+
return JSON.stringify(obj);
|
|
3672
|
+
})
|
|
4349
3673
|
.join("\n");
|
|
4350
3674
|
}
|
|
4351
3675
|
|
|
@@ -4355,6 +3679,52 @@ CAPTCHA_SOLVER_EOF`,
|
|
|
4355
3679
|
clearLogs() {
|
|
4356
3680
|
this._logBuffer = [];
|
|
4357
3681
|
}
|
|
3682
|
+
|
|
3683
|
+
/**
|
|
3684
|
+
* Return info about the most recently provisioned sandbox (from this process
|
|
3685
|
+
* or persisted from a previous run via .testdriver/last-sandbox).
|
|
3686
|
+
* @returns {{ sandboxId: string, os?: string, e2bTemplateId?: string|null, createdAt?: number } | null}
|
|
3687
|
+
*/
|
|
3688
|
+
getLastSandboxId() {
|
|
3689
|
+
if (this.sandboxId) {
|
|
3690
|
+
return {
|
|
3691
|
+
sandboxId: this.sandboxId,
|
|
3692
|
+
os: this.os,
|
|
3693
|
+
e2bTemplateId: this.e2bTemplateId || null,
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
return TestDriverSDK._readLastSandbox();
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
/** @private */
|
|
3700
|
+
static _lastSandboxPath() {
|
|
3701
|
+
return path.join(process.cwd(), ".testdriver", "last-sandbox");
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
/** @private */
|
|
3705
|
+
static _readLastSandbox() {
|
|
3706
|
+
try {
|
|
3707
|
+
const p = TestDriverSDK._lastSandboxPath();
|
|
3708
|
+
if (!fs.existsSync(p)) return null;
|
|
3709
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
3710
|
+
const data = JSON.parse(raw);
|
|
3711
|
+
if (!data || !data.sandboxId) return null;
|
|
3712
|
+
return data;
|
|
3713
|
+
} catch (_) {
|
|
3714
|
+
return null;
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
/** @private */
|
|
3719
|
+
static _writeLastSandbox(data) {
|
|
3720
|
+
try {
|
|
3721
|
+
const p = TestDriverSDK._lastSandboxPath();
|
|
3722
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
3723
|
+
fs.writeFileSync(p, JSON.stringify(data, null, 2));
|
|
3724
|
+
} catch (_) {
|
|
3725
|
+
// Best-effort — don't fail the test if we can't persist.
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
4358
3728
|
}
|
|
4359
3729
|
|
|
4360
3730
|
// Expose SDK version as a static property for use by vitest hooks/plugins
|