testdriverai 7.0.0 → 7.1.1
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/.env.example +2 -0
- package/.github/workflows/linux-tests.yml +28 -0
- package/README.md +126 -0
- package/agent/index.js +7 -9
- package/agent/interface.js +13 -2
- package/agent/lib/commands.js +795 -136
- package/agent/lib/redraw.js +124 -39
- package/agent/lib/sandbox.js +40 -3
- package/agent/lib/sdk.js +21 -0
- package/agent/lib/valid-version.js +2 -2
- package/debugger/index.html +1 -1
- package/docs/docs.json +86 -71
- package/docs/guide/best-practices-polling.mdx +154 -0
- package/docs/v6/getting-started/self-hosting.mdx +3 -2
- package/docs/v7/_drafts/agents.mdx +852 -0
- package/docs/v7/_drafts/auto-cache-key.mdx +167 -0
- package/docs/v7/_drafts/best-practices.mdx +486 -0
- package/docs/v7/_drafts/caching-ai.mdx +215 -0
- package/docs/v7/_drafts/caching-selectors.mdx +400 -0
- package/docs/v7/_drafts/caching.mdx +366 -0
- package/docs/v7/_drafts/cli-to-sdk-migration.mdx +425 -0
- package/docs/v7/_drafts/core.mdx +459 -0
- package/docs/v7/_drafts/dashcam-title-feature.mdx +89 -0
- package/docs/v7/_drafts/debugging.mdx +349 -0
- package/docs/v7/_drafts/error-handling.mdx +501 -0
- package/docs/v7/_drafts/faq.mdx +393 -0
- package/docs/v7/_drafts/hooks.mdx +360 -0
- package/docs/v7/_drafts/implementation-plan.mdx +994 -0
- package/docs/v7/_drafts/init-command.mdx +95 -0
- package/docs/v7/_drafts/optimal-sdk-design.mdx +1348 -0
- package/docs/v7/_drafts/performance.mdx +517 -0
- package/docs/v7/_drafts/presets.mdx +210 -0
- package/docs/v7/_drafts/progressive-disclosure.mdx +230 -0
- package/docs/v7/_drafts/provision.mdx +266 -0
- package/docs/{QUICK_START_TEST_RECORDING.md → v7/_drafts/quick-start-test-recording.mdx} +3 -3
- package/docs/v7/_drafts/sdk-v7-complete.mdx +345 -0
- package/docs/v7/{guides → _drafts}/self-hosting.mdx +1 -1
- package/docs/v7/_drafts/troubleshooting.mdx +526 -0
- package/docs/v7/_drafts/vitest-plugin.mdx +477 -0
- package/docs/v7/_drafts/vitest.mdx +535 -0
- package/docs/v7/api/{ai.mdx → act.mdx} +24 -24
- package/docs/v7/api/client.mdx +1 -1
- package/docs/v7/api/dashcam.mdx +497 -0
- package/docs/v7/api/doubleClick.mdx +102 -0
- package/docs/v7/api/elements.mdx +143 -41
- package/docs/v7/api/find.mdx +258 -0
- package/docs/v7/api/mouseDown.mdx +161 -0
- package/docs/v7/api/mouseUp.mdx +164 -0
- package/docs/v7/api/rightClick.mdx +123 -0
- package/docs/v7/api/type.mdx +51 -7
- package/docs/v7/features/ai-native.mdx +427 -0
- package/docs/v7/features/easy-to-write.mdx +351 -0
- package/docs/v7/features/enterprise.mdx +540 -0
- package/docs/v7/features/fast.mdx +424 -0
- package/docs/v7/features/observable.mdx +623 -0
- package/docs/v7/features/powerful.mdx +531 -0
- package/docs/v7/features/scalable.mdx +417 -0
- package/docs/v7/features/stable.mdx +514 -0
- package/docs/v7/getting-started/configuration.mdx +380 -0
- package/docs/v7/getting-started/generating-tests.mdx +525 -0
- package/docs/v7/getting-started/installation.mdx +486 -0
- package/docs/v7/getting-started/quickstart.mdx +320 -141
- package/docs/v7/getting-started/running-and-debugging.mdx +511 -0
- package/docs/v7/getting-started/setting-up-in-ci.mdx +612 -0
- package/docs/v7/getting-started/writing-tests.mdx +535 -0
- package/docs/v7/overview/what-is-testdriver.mdx +398 -0
- package/docs/v7/platforms/linux.mdx +308 -0
- package/docs/v7/platforms/macos.mdx +433 -0
- package/docs/v7/platforms/windows.mdx +430 -0
- package/docs/v7/playwright.mdx +3 -3
- package/docs/v7/presets/chrome-extension.mdx +223 -0
- package/docs/v7/presets/chrome.mdx +303 -0
- package/docs/v7/presets/electron.mdx +453 -0
- package/docs/v7/presets/vscode.mdx +417 -0
- package/docs/v7/presets/webapp.mdx +396 -0
- package/examples/run-tests-with-recording.sh +2 -2
- package/interfaces/cli/commands/init.js +358 -0
- package/interfaces/vitest-plugin.mjs +393 -103
- package/lib/core/Dashcam.js +506 -0
- package/lib/core/index.d.ts +150 -0
- package/lib/core/index.js +12 -0
- package/lib/presets/index.mjs +331 -0
- package/lib/vitest/hooks.d.ts +119 -0
- package/lib/vitest/hooks.mjs +316 -0
- package/lib/vitest/setup.mjs +44 -0
- package/package.json +13 -3
- package/sdk.d.ts +350 -44
- package/sdk.js +818 -105
- package/{self-hosted.yml → setup/aws/self-hosted.yml} +1 -1
- package/test/manual/test-console-logs.test.mjs +42 -0
- package/test/manual/test-init.sh +54 -0
- package/test/manual/test-provision-auth.mjs +22 -0
- package/test/testdriver/assert.test.mjs +41 -0
- package/test/testdriver/auto-cache-key-demo.test.mjs +56 -0
- package/test/testdriver/chrome-extension.test.mjs +89 -0
- package/{testdriver/acceptance-sdk → test/testdriver}/drag-and-drop.test.mjs +7 -19
- package/{testdriver/acceptance-sdk → test/testdriver}/element-not-found.test.mjs +6 -19
- package/{testdriver/acceptance-sdk → test/testdriver}/exec-js.test.mjs +6 -18
- package/{testdriver/acceptance-sdk → test/testdriver}/exec-output.test.mjs +9 -21
- package/{testdriver/acceptance-sdk → test/testdriver}/exec-pwsh.test.mjs +14 -26
- package/{testdriver/acceptance-sdk → test/testdriver}/focus-window.test.mjs +8 -20
- package/{testdriver/acceptance-sdk → test/testdriver}/formatted-logging.test.mjs +5 -20
- package/{testdriver/acceptance-sdk → test/testdriver}/hover-image.test.mjs +10 -19
- package/{testdriver/acceptance-sdk → test/testdriver}/hover-text-with-description.test.mjs +7 -19
- package/{testdriver/acceptance-sdk → test/testdriver}/hover-text.test.mjs +5 -19
- package/{testdriver/acceptance-sdk → test/testdriver}/match-image.test.mjs +7 -19
- package/{testdriver/acceptance-sdk → test/testdriver}/press-keys.test.mjs +5 -19
- package/{testdriver/acceptance-sdk → test/testdriver}/prompt.test.mjs +7 -19
- package/{testdriver/acceptance-sdk → test/testdriver}/scroll-keyboard.test.mjs +6 -20
- package/{testdriver/acceptance-sdk → test/testdriver}/scroll-until-image.test.mjs +6 -18
- package/test/testdriver/scroll-until-text.test.mjs +28 -0
- package/{testdriver/acceptance-sdk → test/testdriver}/scroll.test.mjs +12 -21
- package/test/testdriver/setup/lifecycleHelpers.mjs +262 -0
- package/{testdriver/acceptance-sdk → test/testdriver}/setup/testHelpers.mjs +25 -20
- package/test/testdriver/type.test.mjs +45 -0
- package/vitest.config.mjs +11 -56
- package/.github/dependabot.yml +0 -11
- package/.github/workflows/acceptance-linux.yml +0 -75
- package/.github/workflows/acceptance-sdk-tests.yml +0 -133
- package/.github/workflows/acceptance-tests.yml +0 -130
- package/.github/workflows/lint.yml +0 -27
- package/.github/workflows/publish-canary.yml +0 -40
- package/.github/workflows/publish-latest.yml +0 -61
- package/.github/workflows/test-install.yml +0 -29
- package/.vscode/extensions.json +0 -3
- package/.vscode/launch.json +0 -22
- package/.vscode/mcp.json +0 -9
- package/.vscode/settings.json +0 -14
- package/CODEOWNERS +0 -3
- package/MIGRATION.md +0 -389
- package/SDK_README.md +0 -1122
- package/_testdriver/acceptance/assert.yaml +0 -7
- package/_testdriver/acceptance/dashcam.yaml +0 -9
- package/_testdriver/acceptance/drag-and-drop.yaml +0 -49
- package/_testdriver/acceptance/embed.yaml +0 -9
- package/_testdriver/acceptance/exec-js.yaml +0 -29
- package/_testdriver/acceptance/exec-output.yaml +0 -43
- package/_testdriver/acceptance/exec-shell.yaml +0 -40
- package/_testdriver/acceptance/focus-window.yaml +0 -16
- package/_testdriver/acceptance/hover-image.yaml +0 -18
- package/_testdriver/acceptance/hover-text-with-description.yaml +0 -29
- package/_testdriver/acceptance/hover-text.yaml +0 -14
- package/_testdriver/acceptance/if-else.yaml +0 -31
- package/_testdriver/acceptance/match-image.yaml +0 -15
- package/_testdriver/acceptance/press-keys.yaml +0 -35
- package/_testdriver/acceptance/prompt.yaml +0 -11
- package/_testdriver/acceptance/remember.yaml +0 -27
- package/_testdriver/acceptance/screenshots/cart.png +0 -0
- package/_testdriver/acceptance/scroll-keyboard.yaml +0 -34
- package/_testdriver/acceptance/scroll-until-image.yaml +0 -26
- package/_testdriver/acceptance/scroll-until-text.yaml +0 -20
- package/_testdriver/acceptance/scroll.yaml +0 -33
- package/_testdriver/acceptance/snippets/login.yaml +0 -29
- package/_testdriver/acceptance/snippets/match-cart.yaml +0 -8
- package/_testdriver/acceptance/type.yaml +0 -29
- package/_testdriver/behavior/failure.yaml +0 -7
- package/_testdriver/behavior/hover-text.yaml +0 -13
- package/_testdriver/behavior/lifecycle/postrun.yaml +0 -10
- package/_testdriver/behavior/lifecycle/prerun.yaml +0 -8
- package/_testdriver/behavior/lifecycle/provision.yaml +0 -8
- package/_testdriver/behavior/secrets.yaml +0 -7
- package/_testdriver/edge-cases/dashcam-chrome.yaml +0 -8
- package/_testdriver/edge-cases/exec-pwsh-multiline.yaml +0 -10
- package/_testdriver/edge-cases/js-exception.yaml +0 -8
- package/_testdriver/edge-cases/js-promise.yaml +0 -19
- package/_testdriver/edge-cases/lifecycle/postrun.yaml +0 -10
- package/_testdriver/edge-cases/prompt-in-middle.yaml +0 -23
- package/_testdriver/edge-cases/prompt-nested.yaml +0 -7
- package/_testdriver/edge-cases/success-test.yaml +0 -9
- package/_testdriver/examples/android/example.yaml +0 -12
- package/_testdriver/examples/android/lifecycle/postrun.yaml +0 -11
- package/_testdriver/examples/android/lifecycle/provision.yaml +0 -47
- package/_testdriver/examples/android/readme.md +0 -7
- package/_testdriver/examples/chrome-extension/lifecycle/provision.yaml +0 -74
- package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
- package/_testdriver/examples/desktop/lifecycle/provision.yaml +0 -64
- package/_testdriver/examples/vscode-extension/lifecycle/provision.yaml +0 -73
- package/_testdriver/examples/web/lifecycle/postrun.yaml +0 -7
- package/_testdriver/examples/web/lifecycle/prerun.yaml +0 -22
- package/_testdriver/lifecycle/postrun.yaml +0 -8
- package/_testdriver/lifecycle/prerun.yaml +0 -15
- package/_testdriver/lifecycle/provision.yaml +0 -25
- package/debug-screenshot-1763401388589.png +0 -0
- package/mcp-server/AI_GUIDELINES.md +0 -57
- package/scripts/view-test-results.mjs +0 -96
- package/styles/.vale-config/2-MDX.ini +0 -5
- package/styles/Microsoft/AMPM.yml +0 -9
- package/styles/Microsoft/Accessibility.yml +0 -30
- package/styles/Microsoft/Acronyms.yml +0 -64
- package/styles/Microsoft/Adverbs.yml +0 -272
- package/styles/Microsoft/Auto.yml +0 -11
- package/styles/Microsoft/Avoid.yml +0 -14
- package/styles/Microsoft/Contractions.yml +0 -50
- package/styles/Microsoft/Dashes.yml +0 -13
- package/styles/Microsoft/DateFormat.yml +0 -8
- package/styles/Microsoft/DateNumbers.yml +0 -40
- package/styles/Microsoft/DateOrder.yml +0 -8
- package/styles/Microsoft/Ellipses.yml +0 -9
- package/styles/Microsoft/FirstPerson.yml +0 -16
- package/styles/Microsoft/Foreign.yml +0 -13
- package/styles/Microsoft/Gender.yml +0 -8
- package/styles/Microsoft/GenderBias.yml +0 -42
- package/styles/Microsoft/GeneralURL.yml +0 -11
- package/styles/Microsoft/HeadingAcronyms.yml +0 -7
- package/styles/Microsoft/HeadingColons.yml +0 -8
- package/styles/Microsoft/HeadingPunctuation.yml +0 -13
- package/styles/Microsoft/Headings.yml +0 -28
- package/styles/Microsoft/Hyphens.yml +0 -14
- package/styles/Microsoft/Negative.yml +0 -13
- package/styles/Microsoft/Ordinal.yml +0 -13
- package/styles/Microsoft/OxfordComma.yml +0 -8
- package/styles/Microsoft/Passive.yml +0 -183
- package/styles/Microsoft/Percentages.yml +0 -7
- package/styles/Microsoft/Plurals.yml +0 -7
- package/styles/Microsoft/Quotes.yml +0 -7
- package/styles/Microsoft/RangeTime.yml +0 -13
- package/styles/Microsoft/Semicolon.yml +0 -8
- package/styles/Microsoft/SentenceLength.yml +0 -6
- package/styles/Microsoft/Spacing.yml +0 -8
- package/styles/Microsoft/Suspended.yml +0 -7
- package/styles/Microsoft/Terms.yml +0 -42
- package/styles/Microsoft/URLFormat.yml +0 -9
- package/styles/Microsoft/Units.yml +0 -16
- package/styles/Microsoft/Vocab.yml +0 -25
- package/styles/Microsoft/We.yml +0 -11
- package/styles/Microsoft/Wordiness.yml +0 -127
- package/styles/Microsoft/meta.json +0 -4
- package/styles/alex/Ablist.yml +0 -274
- package/styles/alex/Condescending.yml +0 -16
- package/styles/alex/Gendered.yml +0 -110
- package/styles/alex/LGBTQ.yml +0 -55
- package/styles/alex/OCD.yml +0 -10
- package/styles/alex/Press.yml +0 -12
- package/styles/alex/ProfanityLikely.yml +0 -1289
- package/styles/alex/ProfanityMaybe.yml +0 -282
- package/styles/alex/ProfanityUnlikely.yml +0 -251
- package/styles/alex/README.md +0 -27
- package/styles/alex/Race.yml +0 -85
- package/styles/alex/Suicide.yml +0 -26
- package/styles/alex/meta.json +0 -4
- package/styles/config/vocabularies/Docs/accept.txt +0 -47
- package/styles/config/vocabularies/Docs/reject.txt +0 -4
- package/styles/proselint/Airlinese.yml +0 -8
- package/styles/proselint/AnimalLabels.yml +0 -48
- package/styles/proselint/Annotations.yml +0 -9
- package/styles/proselint/Apologizing.yml +0 -8
- package/styles/proselint/Archaisms.yml +0 -52
- package/styles/proselint/But.yml +0 -8
- package/styles/proselint/Cliches.yml +0 -782
- package/styles/proselint/CorporateSpeak.yml +0 -30
- package/styles/proselint/Currency.yml +0 -5
- package/styles/proselint/Cursing.yml +0 -15
- package/styles/proselint/DateCase.yml +0 -7
- package/styles/proselint/DateMidnight.yml +0 -7
- package/styles/proselint/DateRedundancy.yml +0 -10
- package/styles/proselint/DateSpacing.yml +0 -7
- package/styles/proselint/DenizenLabels.yml +0 -52
- package/styles/proselint/Diacritical.yml +0 -95
- package/styles/proselint/GenderBias.yml +0 -45
- package/styles/proselint/GroupTerms.yml +0 -39
- package/styles/proselint/Hedging.yml +0 -8
- package/styles/proselint/Hyperbole.yml +0 -6
- package/styles/proselint/Jargon.yml +0 -11
- package/styles/proselint/LGBTOffensive.yml +0 -13
- package/styles/proselint/LGBTTerms.yml +0 -15
- package/styles/proselint/Malapropisms.yml +0 -8
- package/styles/proselint/Needless.yml +0 -358
- package/styles/proselint/Nonwords.yml +0 -38
- package/styles/proselint/Oxymorons.yml +0 -22
- package/styles/proselint/P-Value.yml +0 -6
- package/styles/proselint/RASSyndrome.yml +0 -30
- package/styles/proselint/README.md +0 -12
- package/styles/proselint/Skunked.yml +0 -13
- package/styles/proselint/Spelling.yml +0 -17
- package/styles/proselint/Typography.yml +0 -11
- package/styles/proselint/Uncomparables.yml +0 -50
- package/styles/proselint/Very.yml +0 -6
- package/styles/proselint/meta.json +0 -15
- package/styles/write-good/Cliches.yml +0 -702
- package/styles/write-good/E-Prime.yml +0 -32
- package/styles/write-good/Illusions.yml +0 -11
- package/styles/write-good/Passive.yml +0 -183
- package/styles/write-good/README.md +0 -27
- package/styles/write-good/So.yml +0 -5
- package/styles/write-good/ThereIs.yml +0 -6
- package/styles/write-good/TooWordy.yml +0 -221
- package/styles/write-good/Weasel.yml +0 -29
- package/styles/write-good/meta.json +0 -4
- package/test/mcp-example-test.yaml +0 -27
- package/test/test_parser.js +0 -47
- package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +0 -61
- package/testdriver/acceptance-sdk/README.md +0 -128
- package/testdriver/acceptance-sdk/TEST_REPORTING.md +0 -245
- package/testdriver/acceptance-sdk/assert.test.mjs +0 -44
- package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +0 -42
- package/testdriver/acceptance-sdk/setup/lifecycleHelpers.mjs +0 -239
- package/testdriver/acceptance-sdk/type-checking-demo.js +0 -49
- package/testdriver/acceptance-sdk/type.test.mjs +0 -84
- package/vale.ini +0 -18
- package/vitest.config.example.js +0 -19
- package/vitest.config.mjs.bak +0 -44
- /package/docs/{ARCHITECTURE.md → v7/_drafts/architecture.mdx} +0 -0
- /package/docs/{AWESOME_LOGS_QUICK_REF.md → v7/_drafts/awesome-logs-quick-ref.mdx} +0 -0
- /package/{CONTRIBUTING.md → docs/v7/_drafts/contributing.mdx} +0 -0
- /package/docs/v7/{guides → _drafts}/migration.mdx +0 -0
- /package/{PLUGIN_MIGRATION.md → docs/v7/_drafts/plugin-migration.mdx} +0 -0
- /package/{PROMPT_CACHE.md → docs/v7/_drafts/prompt-cache.mdx} +0 -0
- /package/docs/{SDK_AWESOME_LOGS.md → v7/_drafts/sdk-awesome-logs.mdx} +0 -0
- /package/docs/{sdk-browser-rendering.md → v7/_drafts/sdk-browser-rendering.mdx} +0 -0
- /package/{SDK_LOGGING.md → docs/v7/_drafts/sdk-logging.mdx} +0 -0
- /package/{SDK_MIGRATION.md → docs/v7/_drafts/sdk-migration.mdx} +0 -0
- /package/docs/{TEST_RECORDING.md → v7/_drafts/test-recording.mdx} +0 -0
- /package/docs/v7/{README.md → overview/readme.mdx} +0 -0
- /package/{debug-locate-response.js → test/manual/debug-locate-response.js} +0 -0
- /package/{test-find-api.js → test/manual/test-find-api.js} +0 -0
- /package/{test-prompt-cache.js → test/manual/test-prompt-cache.js} +0 -0
- /package/{test-sandbox-render.js → test/manual/test-sandbox-render.js} +0 -0
- /package/{test-sdk-methods.js → test/manual/test-sdk-methods.js} +0 -0
- /package/{test-sdk-refactor.js → test/manual/test-sdk-refactor.js} +0 -0
- /package/{test-stack-trace.mjs → test/manual/test-stack-trace.mjs} +0 -0
- /package/{verify-element-api.js → test/manual/verify-element-api.js} +0 -0
- /package/{verify-types.js → test/manual/verify-types.js} +0 -0
- /package/{testdriver/acceptance-sdk → test/testdriver}/setup/globalTeardown.mjs +0 -0
- /package/{testdriver/acceptance-sdk → test/testdriver}/setup/vitestSetup.mjs +0 -0
package/sdk.js
CHANGED
|
@@ -1,10 +1,69 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
const fs = require("fs");
|
|
4
2
|
const path = require("path");
|
|
5
3
|
const os = require("os");
|
|
4
|
+
const crypto = require("crypto");
|
|
6
5
|
const { formatter } = require("./sdk-log-formatter");
|
|
7
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Get the file path of the caller (the file that called TestDriver)
|
|
9
|
+
* @returns {string|null} File path or null if not found
|
|
10
|
+
*/
|
|
11
|
+
function getCallerFilePath() {
|
|
12
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
13
|
+
try {
|
|
14
|
+
const err = new Error();
|
|
15
|
+
Error.prepareStackTrace = (_, stack) => stack;
|
|
16
|
+
const stack = err.stack;
|
|
17
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
18
|
+
|
|
19
|
+
// Look for the first file that's not sdk.js, hooks.mjs, or node internals
|
|
20
|
+
for (const callSite of stack) {
|
|
21
|
+
const fileName = callSite.getFileName();
|
|
22
|
+
if (fileName &&
|
|
23
|
+
!fileName.includes('sdk.js') &&
|
|
24
|
+
!fileName.includes('hooks.mjs') &&
|
|
25
|
+
!fileName.includes('hooks.js') &&
|
|
26
|
+
!fileName.includes('node_modules') &&
|
|
27
|
+
!fileName.includes('node:internal') &&
|
|
28
|
+
fileName !== 'evalmachine.<anonymous>') {
|
|
29
|
+
return fileName;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Silently fail and return null
|
|
34
|
+
} finally {
|
|
35
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generate a hash of the caller file for use as a cache key
|
|
42
|
+
* @returns {string|null} Hash of the file or null if file not found
|
|
43
|
+
*/
|
|
44
|
+
function getCallerFileHash() {
|
|
45
|
+
const filePath = getCallerFilePath();
|
|
46
|
+
if (!filePath) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Handle file:// URLs by converting to file system path
|
|
52
|
+
let fsPath = filePath;
|
|
53
|
+
if (filePath.startsWith('file://')) {
|
|
54
|
+
fsPath = filePath.replace('file://', '');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fileContent = fs.readFileSync(fsPath, 'utf-8');
|
|
58
|
+
const hash = crypto.createHash('sha256').update(fileContent).digest('hex');
|
|
59
|
+
// Return first 16 chars of hash for brevity
|
|
60
|
+
return hash.substring(0, 16);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// If we can't read the file, return null
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
8
67
|
/**
|
|
9
68
|
* Custom error class for element operation failures
|
|
10
69
|
* Includes debugging information like screenshots and AI responses
|
|
@@ -13,8 +72,8 @@ class ElementNotFoundError extends Error {
|
|
|
13
72
|
constructor(message, debugInfo = {}) {
|
|
14
73
|
super(message);
|
|
15
74
|
this.name = "ElementNotFoundError";
|
|
16
|
-
|
|
17
|
-
this.aiResponse = debugInfo.aiResponse;
|
|
75
|
+
// Sanitize aiResponse to remove base64 images before storing
|
|
76
|
+
this.aiResponse = this._sanitizeAiResponse(debugInfo.aiResponse);
|
|
18
77
|
this.description = debugInfo.description;
|
|
19
78
|
this.timestamp = new Date().toISOString();
|
|
20
79
|
this.screenshotPath = null;
|
|
@@ -24,8 +83,9 @@ class ElementNotFoundError extends Error {
|
|
|
24
83
|
Error.captureStackTrace(this, ElementNotFoundError);
|
|
25
84
|
}
|
|
26
85
|
|
|
27
|
-
// Write screenshot to temp directory
|
|
28
|
-
|
|
86
|
+
// Write screenshot to temp directory immediately (don't store on error object)
|
|
87
|
+
// This prevents vitest from serializing huge base64 strings
|
|
88
|
+
if (debugInfo.screenshot) {
|
|
29
89
|
try {
|
|
30
90
|
const tempDir = path.join(os.tmpdir(), "testdriver-debug");
|
|
31
91
|
if (!fs.existsSync(tempDir)) {
|
|
@@ -36,7 +96,7 @@ class ElementNotFoundError extends Error {
|
|
|
36
96
|
this.screenshotPath = path.join(tempDir, filename);
|
|
37
97
|
|
|
38
98
|
// Remove data:image/png;base64, prefix if present
|
|
39
|
-
const base64Data =
|
|
99
|
+
const base64Data = debugInfo.screenshot.replace(
|
|
40
100
|
/^data:image\/\w+;base64,/,
|
|
41
101
|
"",
|
|
42
102
|
);
|
|
@@ -182,6 +242,25 @@ class ElementNotFoundError extends Error {
|
|
|
182
242
|
this.stack = filteredLines.join("\n");
|
|
183
243
|
}
|
|
184
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Sanitize AI response by removing large base64 data to prevent serialization issues
|
|
248
|
+
* @private
|
|
249
|
+
* @param {Object} response - AI response
|
|
250
|
+
* @returns {Object} Sanitized response
|
|
251
|
+
*/
|
|
252
|
+
_sanitizeAiResponse(response) {
|
|
253
|
+
if (!response) return null;
|
|
254
|
+
|
|
255
|
+
// Create shallow copy and remove large base64 fields
|
|
256
|
+
const sanitized = { ...response };
|
|
257
|
+
delete sanitized.croppedImage;
|
|
258
|
+
delete sanitized.screenshot;
|
|
259
|
+
delete sanitized.pixelDiffImage;
|
|
260
|
+
// Keep cachedImageUrl as it's just a URL string, not base64 data
|
|
261
|
+
|
|
262
|
+
return sanitized;
|
|
263
|
+
}
|
|
185
264
|
}
|
|
186
265
|
|
|
187
266
|
/**
|
|
@@ -213,16 +292,18 @@ class Element {
|
|
|
213
292
|
/**
|
|
214
293
|
* Find the element on screen
|
|
215
294
|
* @param {string} [newDescription] - Optional new description to search for
|
|
216
|
-
* @param {
|
|
295
|
+
* @param {Object} [options] - Optional options object with cacheThreshold and/or cacheKey
|
|
217
296
|
* @returns {Promise<Element>} This element instance
|
|
218
297
|
*/
|
|
219
|
-
async find(newDescription,
|
|
298
|
+
async find(newDescription, options) {
|
|
220
299
|
const description = newDescription || this.description;
|
|
221
300
|
if (newDescription) {
|
|
222
301
|
this.description = newDescription;
|
|
223
302
|
}
|
|
224
303
|
|
|
225
304
|
const startTime = Date.now();
|
|
305
|
+
let response = null;
|
|
306
|
+
let findError = null;
|
|
226
307
|
|
|
227
308
|
const debugMode =
|
|
228
309
|
process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
|
|
@@ -239,9 +320,38 @@ class Element {
|
|
|
239
320
|
this._screenshot = screenshot;
|
|
240
321
|
}
|
|
241
322
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
323
|
+
// Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold
|
|
324
|
+
let cacheKey = null;
|
|
325
|
+
let cacheThreshold = null;
|
|
326
|
+
|
|
327
|
+
if (typeof options === 'number') {
|
|
328
|
+
// Legacy: options is just a number threshold
|
|
329
|
+
cacheThreshold = options;
|
|
330
|
+
} else if (typeof options === 'object' && options !== null) {
|
|
331
|
+
// New: options is an object with cacheKey and/or cacheThreshold
|
|
332
|
+
cacheKey = options.cacheKey || null;
|
|
333
|
+
cacheThreshold = options.cacheThreshold ?? null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Use default cacheKey from SDK constructor if not provided in find() options
|
|
337
|
+
if (!cacheKey && this.sdk.options?.cacheKey) {
|
|
338
|
+
cacheKey = this.sdk.options.cacheKey;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Determine threshold:
|
|
342
|
+
// - If cacheKey is provided, enable cache (threshold = 0.05 or custom)
|
|
343
|
+
// - If no cacheKey, disable cache (threshold = -1) unless explicitly overridden
|
|
344
|
+
let threshold;
|
|
345
|
+
if (cacheKey) {
|
|
346
|
+
// cacheKey provided - enable cache with threshold
|
|
347
|
+
threshold = cacheThreshold ?? 0.05;
|
|
348
|
+
} else if (cacheThreshold !== null) {
|
|
349
|
+
// Explicit threshold provided without cacheKey
|
|
350
|
+
threshold = cacheThreshold;
|
|
351
|
+
} else {
|
|
352
|
+
// No cacheKey, no explicit threshold - use global default (which is -1 now)
|
|
353
|
+
threshold = this.sdk.cacheThresholds?.find ?? -1;
|
|
354
|
+
}
|
|
245
355
|
|
|
246
356
|
// Store the threshold for debugging
|
|
247
357
|
this._threshold = threshold;
|
|
@@ -249,16 +359,21 @@ class Element {
|
|
|
249
359
|
// Debug log threshold
|
|
250
360
|
if (debugMode) {
|
|
251
361
|
const { events } = require("./agent/events.js");
|
|
362
|
+
const autoGenMsg = (this.sdk._autoGeneratedCacheKey && cacheKey === this.sdk.options.cacheKey)
|
|
363
|
+
? ' (auto-generated from file hash)'
|
|
364
|
+
: '';
|
|
252
365
|
this.sdk.emitter.emit(
|
|
253
366
|
events.log.debug,
|
|
254
|
-
`🔍 find() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"})`,
|
|
367
|
+
`🔍 find() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
|
|
255
368
|
);
|
|
256
369
|
}
|
|
257
370
|
|
|
258
|
-
|
|
371
|
+
response = await this.sdk.apiClient.req("find", {
|
|
372
|
+
session: this.sdk.getSessionId(),
|
|
259
373
|
element: description,
|
|
260
374
|
image: screenshot,
|
|
261
375
|
threshold: threshold,
|
|
376
|
+
cacheKey: cacheKey,
|
|
262
377
|
os: this.sdk.os,
|
|
263
378
|
resolution: this.sdk.resolution,
|
|
264
379
|
});
|
|
@@ -278,12 +393,36 @@ class Element {
|
|
|
278
393
|
} else {
|
|
279
394
|
this._response = this._sanitizeResponse(response);
|
|
280
395
|
this._found = false;
|
|
396
|
+
findError = "Element not found";
|
|
281
397
|
}
|
|
282
398
|
} catch (error) {
|
|
283
399
|
this._response = error.response
|
|
284
400
|
? this._sanitizeResponse(error.response)
|
|
285
401
|
: null;
|
|
286
402
|
this._found = false;
|
|
403
|
+
findError = error.message;
|
|
404
|
+
response = error.response;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Track find interaction once at the end
|
|
408
|
+
const sessionId = this.sdk.getSessionId();
|
|
409
|
+
if (sessionId && this.sdk.sandbox?.send) {
|
|
410
|
+
try {
|
|
411
|
+
await this.sdk.sandbox.send({
|
|
412
|
+
type: "trackInteraction",
|
|
413
|
+
interactionType: "find",
|
|
414
|
+
session: sessionId,
|
|
415
|
+
prompt: description,
|
|
416
|
+
timestamp: startTime,
|
|
417
|
+
success: this._found,
|
|
418
|
+
error: findError,
|
|
419
|
+
cacheHit: response?.cacheHit || response?.cache_hit || response?.cached || false,
|
|
420
|
+
selector: response?.selector,
|
|
421
|
+
selectorUsed: !!response?.selector,
|
|
422
|
+
});
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.warn("Failed to track find interaction:", err.message);
|
|
425
|
+
}
|
|
287
426
|
}
|
|
288
427
|
|
|
289
428
|
return this;
|
|
@@ -544,11 +683,8 @@ class Element {
|
|
|
544
683
|
`Element "${this.description}" not found.`,
|
|
545
684
|
{
|
|
546
685
|
description: this.description,
|
|
547
|
-
screenshot: this._screenshot,
|
|
548
686
|
aiResponse: this._response,
|
|
549
687
|
threshold: this._threshold,
|
|
550
|
-
cachedImageUrl: this._response?.cachedImageUrl,
|
|
551
|
-
pixelDiffImage: this._response?.pixelDiffImage,
|
|
552
688
|
},
|
|
553
689
|
);
|
|
554
690
|
}
|
|
@@ -562,10 +698,22 @@ class Element {
|
|
|
562
698
|
);
|
|
563
699
|
this.sdk.emitter.emit(events.log.log, formattedMessage);
|
|
564
700
|
|
|
701
|
+
// Prepare element metadata for interaction tracking
|
|
702
|
+
const elementData = {
|
|
703
|
+
prompt: this.description,
|
|
704
|
+
elementType: this._response?.elementType,
|
|
705
|
+
elementBounds: this._response?.elementBounds,
|
|
706
|
+
croppedImageUrl: this._response?.savedImagePath,
|
|
707
|
+
edgeDetectedImageUrl: this._response?.edgeSavedImagePath || null,
|
|
708
|
+
cacheHit: this._response?.cacheHit,
|
|
709
|
+
selectorUsed: !!this._response?.selector,
|
|
710
|
+
selector: this._response?.selector
|
|
711
|
+
};
|
|
712
|
+
|
|
565
713
|
if (action === "hover") {
|
|
566
|
-
await this.commands.hover(this.coordinates.x, this.coordinates.y);
|
|
714
|
+
await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
|
|
567
715
|
} else {
|
|
568
|
-
await this.commands.click(this.coordinates.x, this.coordinates.y, action);
|
|
716
|
+
await this.commands.click(this.coordinates.x, this.coordinates.y, action, elementData);
|
|
569
717
|
}
|
|
570
718
|
}
|
|
571
719
|
|
|
@@ -579,11 +727,8 @@ class Element {
|
|
|
579
727
|
`Element "${this.description}" not found.`,
|
|
580
728
|
{
|
|
581
729
|
description: this.description,
|
|
582
|
-
screenshot: this._screenshot,
|
|
583
730
|
aiResponse: this._response,
|
|
584
731
|
threshold: this._threshold,
|
|
585
|
-
cachedImageUrl: this._response?.cachedImageUrl,
|
|
586
|
-
pixelDiffImage: this._response?.pixelDiffImage,
|
|
587
732
|
},
|
|
588
733
|
);
|
|
589
734
|
}
|
|
@@ -593,7 +738,19 @@ class Element {
|
|
|
593
738
|
const formattedMessage = formatter.formatAction("hover", this.description);
|
|
594
739
|
this.sdk.emitter.emit(events.log.log, formattedMessage);
|
|
595
740
|
|
|
596
|
-
|
|
741
|
+
// Prepare element metadata for interaction tracking
|
|
742
|
+
const elementData = {
|
|
743
|
+
prompt: this.description,
|
|
744
|
+
elementType: this._response?.elementType,
|
|
745
|
+
elementBounds: this._response?.elementBounds,
|
|
746
|
+
croppedImageUrl: this._response?.savedImagePath,
|
|
747
|
+
edgeDetectedImageUrl: this._response?.edgeSavedImagePath || null,
|
|
748
|
+
cacheHit: this._response?.cacheHit,
|
|
749
|
+
selectorUsed: !!this._response?.selector,
|
|
750
|
+
selector: this._response?.selector
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
await this.commands.hover(this.coordinates.x, this.coordinates.y, elementData);
|
|
597
754
|
}
|
|
598
755
|
|
|
599
756
|
/**
|
|
@@ -785,6 +942,60 @@ class Element {
|
|
|
785
942
|
}
|
|
786
943
|
}
|
|
787
944
|
|
|
945
|
+
/**
|
|
946
|
+
* Creates a chainable promise that allows method chaining on find() results
|
|
947
|
+
* This enables syntax like: await testdriver.find("button").click()
|
|
948
|
+
*
|
|
949
|
+
* @param {Promise<Element>} promise - The promise that resolves to an Element
|
|
950
|
+
* @returns {Promise<Element> & ChainableElement} A promise with chainable element methods
|
|
951
|
+
*/
|
|
952
|
+
function createChainablePromise(promise) {
|
|
953
|
+
// Define the chainable methods that should be available
|
|
954
|
+
const chainableMethods = ['click', 'hover', 'doubleClick', 'rightClick', 'mouseDown', 'mouseUp'];
|
|
955
|
+
|
|
956
|
+
// Create a new promise that wraps the original
|
|
957
|
+
const chainablePromise = promise.then(element => element);
|
|
958
|
+
|
|
959
|
+
// Add chainable methods to the promise
|
|
960
|
+
for (const method of chainableMethods) {
|
|
961
|
+
chainablePromise[method] = function(...args) {
|
|
962
|
+
// Return a promise that waits for the element, then calls the method
|
|
963
|
+
return promise.then(element => element[method](...args));
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Add getters for element properties (these return promises)
|
|
968
|
+
Object.defineProperty(chainablePromise, 'x', {
|
|
969
|
+
get() { return promise.then(el => el.x); }
|
|
970
|
+
});
|
|
971
|
+
Object.defineProperty(chainablePromise, 'y', {
|
|
972
|
+
get() { return promise.then(el => el.y); }
|
|
973
|
+
});
|
|
974
|
+
Object.defineProperty(chainablePromise, 'centerX', {
|
|
975
|
+
get() { return promise.then(el => el.centerX); }
|
|
976
|
+
});
|
|
977
|
+
Object.defineProperty(chainablePromise, 'centerY', {
|
|
978
|
+
get() { return promise.then(el => el.centerY); }
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
// Add found() method
|
|
982
|
+
chainablePromise.found = function() {
|
|
983
|
+
return promise.then(el => el.found());
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
// Add getCoordinates() method
|
|
987
|
+
chainablePromise.getCoordinates = function() {
|
|
988
|
+
return promise.then(el => el.getCoordinates());
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
// Add getResponse() method
|
|
992
|
+
chainablePromise.getResponse = function() {
|
|
993
|
+
return promise.then(el => el.getResponse());
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
return chainablePromise;
|
|
997
|
+
}
|
|
998
|
+
|
|
788
999
|
/**
|
|
789
1000
|
* TestDriver SDK
|
|
790
1001
|
*
|
|
@@ -838,6 +1049,17 @@ class TestDriverSDK {
|
|
|
838
1049
|
},
|
|
839
1050
|
});
|
|
840
1051
|
|
|
1052
|
+
// Auto-generate cache key from caller file hash if not explicitly provided
|
|
1053
|
+
// This allows caching to be tied to the specific test file
|
|
1054
|
+
if (!options.cacheKey) {
|
|
1055
|
+
const autoGeneratedKey = getCallerFileHash();
|
|
1056
|
+
if (autoGeneratedKey) {
|
|
1057
|
+
options.cacheKey = autoGeneratedKey;
|
|
1058
|
+
// Store flag to indicate this was auto-generated
|
|
1059
|
+
this._autoGeneratedCacheKey = true;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
841
1063
|
// Store options for later use
|
|
842
1064
|
this.options = options;
|
|
843
1065
|
|
|
@@ -847,7 +1069,7 @@ class TestDriverSDK {
|
|
|
847
1069
|
|
|
848
1070
|
// Store newSandbox preference from options
|
|
849
1071
|
this.newSandbox =
|
|
850
|
-
options.newSandbox !== undefined ? options.newSandbox :
|
|
1072
|
+
options.newSandbox !== undefined ? options.newSandbox : true;
|
|
851
1073
|
|
|
852
1074
|
// Store headless preference from options
|
|
853
1075
|
this.headless = options.headless !== undefined ? options.headless : false;
|
|
@@ -862,31 +1084,48 @@ class TestDriverSDK {
|
|
|
862
1084
|
|
|
863
1085
|
// Cache threshold configuration
|
|
864
1086
|
// threshold = pixel difference allowed (0.05 = 5% difference, 95% similarity)
|
|
865
|
-
// cache
|
|
866
|
-
//
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
if (!useCache) {
|
|
874
|
-
// If cache is disabled, use -1 to bypass cache entirely
|
|
1087
|
+
// By default, cache is DISABLED (threshold = -1) to avoid unnecessary AI costs
|
|
1088
|
+
// To enable cache, provide a cacheKey when calling find() or findAll()
|
|
1089
|
+
// Also support TD_NO_CACHE environment variable and cache: false option for backwards compatibility
|
|
1090
|
+
const cacheDisabled =
|
|
1091
|
+
options.cache === false || process.env.TD_NO_CACHE === "true";
|
|
1092
|
+
|
|
1093
|
+
if (cacheDisabled) {
|
|
1094
|
+
// Explicit cache disabled via option or env var
|
|
875
1095
|
this.cacheThresholds = {
|
|
876
1096
|
find: -1,
|
|
877
1097
|
findAll: -1,
|
|
878
1098
|
};
|
|
879
1099
|
} else {
|
|
880
|
-
//
|
|
1100
|
+
// Cache disabled by default, enabled only when cacheKey is provided
|
|
1101
|
+
// Note: The threshold value here is the fallback when cacheKey is NOT provided
|
|
881
1102
|
this.cacheThresholds = {
|
|
882
|
-
find: options.cacheThreshold?.find ??
|
|
883
|
-
findAll: options.cacheThreshold?.findAll ??
|
|
1103
|
+
find: options.cacheThreshold?.find ?? -1, // Default: cache disabled
|
|
1104
|
+
findAll: options.cacheThreshold?.findAll ?? -1, // Default: cache disabled
|
|
884
1105
|
};
|
|
885
1106
|
}
|
|
886
1107
|
|
|
887
|
-
// Redraw
|
|
888
|
-
//
|
|
889
|
-
|
|
1108
|
+
// Redraw configuration
|
|
1109
|
+
// Supports both:
|
|
1110
|
+
// - redraw: { enabled: true, diffThreshold: 0.1, screenRedraw: true, networkMonitor: true }
|
|
1111
|
+
// - redrawThreshold: 0.1 (legacy, sets diffThreshold)
|
|
1112
|
+
// The `redraw` option takes precedence and matches the per-command API
|
|
1113
|
+
if (options.redraw !== undefined) {
|
|
1114
|
+
// New unified API: redraw object (matches per-command options)
|
|
1115
|
+
this.redrawOptions = typeof options.redraw === 'object'
|
|
1116
|
+
? options.redraw
|
|
1117
|
+
: { enabled: options.redraw }; // Support redraw: false as shorthand
|
|
1118
|
+
} else if (options.redrawThreshold !== undefined) {
|
|
1119
|
+
// Legacy API: redrawThreshold number or object
|
|
1120
|
+
this.redrawOptions = typeof options.redrawThreshold === 'object'
|
|
1121
|
+
? options.redrawThreshold
|
|
1122
|
+
: { diffThreshold: options.redrawThreshold };
|
|
1123
|
+
} else {
|
|
1124
|
+
// Default: enabled (as of v7.2)
|
|
1125
|
+
this.redrawOptions = { enabled: true };
|
|
1126
|
+
}
|
|
1127
|
+
// Keep redrawThreshold for backwards compatibility in connect()
|
|
1128
|
+
this.redrawThreshold = this.redrawOptions;
|
|
890
1129
|
|
|
891
1130
|
// Track connection state
|
|
892
1131
|
this.connected = false;
|
|
@@ -910,6 +1149,258 @@ class TestDriverSDK {
|
|
|
910
1149
|
|
|
911
1150
|
// Set up event listeners once (they live for the lifetime of the SDK instance)
|
|
912
1151
|
this._setupLogging();
|
|
1152
|
+
|
|
1153
|
+
// Set up provision API
|
|
1154
|
+
this.provision = this._createProvisionAPI();
|
|
1155
|
+
|
|
1156
|
+
// Set up dashcam API lazily
|
|
1157
|
+
this._dashcam = null;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Wait for the sandbox connection to complete
|
|
1162
|
+
* @returns {Promise<void>}
|
|
1163
|
+
*/
|
|
1164
|
+
async ready() {
|
|
1165
|
+
if (this.__connectionPromise) {
|
|
1166
|
+
await this.__connectionPromise;
|
|
1167
|
+
}
|
|
1168
|
+
if (!this.connected) {
|
|
1169
|
+
throw new Error('Not connected to sandbox. Call connect() first or use autoConnect option.');
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Get or create the Dashcam instance
|
|
1175
|
+
* @returns {Dashcam} Dashcam instance
|
|
1176
|
+
*/
|
|
1177
|
+
get dashcam() {
|
|
1178
|
+
if (!this._dashcam) {
|
|
1179
|
+
const { Dashcam } = require("./lib/core/index.js");
|
|
1180
|
+
// Don't pass apiKey - let Dashcam use its default key
|
|
1181
|
+
this._dashcam = new Dashcam(this);
|
|
1182
|
+
}
|
|
1183
|
+
return this._dashcam;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Get milliseconds elapsed since dashcam started recording
|
|
1188
|
+
* @returns {number|null} Milliseconds since dashcam start, or null if not recording
|
|
1189
|
+
*/
|
|
1190
|
+
getDashcamElapsedTime() {
|
|
1191
|
+
if (this._dashcam) {
|
|
1192
|
+
return this._dashcam.getElapsedTime();
|
|
1193
|
+
}
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Create the provision API with methods for launching applications
|
|
1199
|
+
* @private
|
|
1200
|
+
*/
|
|
1201
|
+
_createProvisionAPI() {
|
|
1202
|
+
return {
|
|
1203
|
+
/**
|
|
1204
|
+
* Launch Chrome browser
|
|
1205
|
+
* @param {Object} options - Chrome launch options
|
|
1206
|
+
* @param {string} [options.url='http://testdriver-sandbox.vercel.app/'] - URL to navigate to
|
|
1207
|
+
* @param {boolean} [options.maximized=true] - Start maximized
|
|
1208
|
+
* @param {boolean} [options.guest=false] - Use guest mode
|
|
1209
|
+
* @returns {Promise<void>}
|
|
1210
|
+
*/
|
|
1211
|
+
chrome: async (options = {}) => {
|
|
1212
|
+
// Automatically wait for connection to be ready
|
|
1213
|
+
await this.ready();
|
|
1214
|
+
|
|
1215
|
+
const {
|
|
1216
|
+
url = 'http://testdriver-sandbox.vercel.app/',
|
|
1217
|
+
maximized = true,
|
|
1218
|
+
guest = false,
|
|
1219
|
+
} = options;
|
|
1220
|
+
|
|
1221
|
+
// If dashcam is available and recording, add web logs for this domain
|
|
1222
|
+
if (this._dashcam) {
|
|
1223
|
+
|
|
1224
|
+
// Create the log file on the remote machine
|
|
1225
|
+
const shell = this.os === "windows" ? "pwsh" : "sh";
|
|
1226
|
+
const logPath = this.os === "windows"
|
|
1227
|
+
? "C:\\Users\\testdriver\\Documents\\testdriver.log"
|
|
1228
|
+
: "/tmp/testdriver.log";
|
|
1229
|
+
|
|
1230
|
+
const createLogCmd = this.os === "windows"
|
|
1231
|
+
? `New-Item -ItemType File -Path "${logPath}" -Force | Out-Null`
|
|
1232
|
+
: `touch ${logPath}`;
|
|
1233
|
+
|
|
1234
|
+
await this.exec(shell, createLogCmd, 10000, true);
|
|
1235
|
+
|
|
1236
|
+
console.log('[provision.chrome] Adding web logs to dashcam...');
|
|
1237
|
+
try {
|
|
1238
|
+
const urlObj = new URL(url);
|
|
1239
|
+
const domain = urlObj.hostname;
|
|
1240
|
+
const pattern = `*${domain}*`;
|
|
1241
|
+
await this._dashcam.addWebLog(pattern, 'Web Logs');
|
|
1242
|
+
console.log(`[provision.chrome] ✅ Web logs added to dashcam (pattern: ${pattern})`);
|
|
1243
|
+
|
|
1244
|
+
await this._dashcam.addFileLog(logPath, "TestDriver Log");
|
|
1245
|
+
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
console.warn('[provision.chrome] ⚠️ Failed to add web logs:', error.message);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Automatically start dashcam if not already recording
|
|
1252
|
+
if (!this._dashcam || !this._dashcam.recording) {
|
|
1253
|
+
console.log('[provision.chrome] Starting dashcam...');
|
|
1254
|
+
await this.dashcam.start();
|
|
1255
|
+
console.log('[provision.chrome] ✅ Dashcam started');
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Build Chrome launch command
|
|
1259
|
+
const chromeArgs = [];
|
|
1260
|
+
if (maximized) chromeArgs.push('--start-maximized');
|
|
1261
|
+
if (guest) chromeArgs.push('--guest');
|
|
1262
|
+
chromeArgs.push('--disable-fre', '--no-default-browser-check', '--no-first-run');
|
|
1263
|
+
|
|
1264
|
+
// Add dashcam-chrome extension on Linux
|
|
1265
|
+
if (this.os === 'linux') {
|
|
1266
|
+
chromeArgs.push('--load-extension=/usr/lib/node_modules/dashcam-chrome/build');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Launch Chrome
|
|
1270
|
+
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1271
|
+
|
|
1272
|
+
if (this.os === 'windows') {
|
|
1273
|
+
const argsString = chromeArgs.map(arg => `"${arg}"`).join(', ');
|
|
1274
|
+
await this.exec(
|
|
1275
|
+
shell,
|
|
1276
|
+
`Start-Process "C:/Program Files/Google/Chrome/Application/chrome.exe" -ArgumentList ${argsString}, "${url}"`,
|
|
1277
|
+
30000
|
|
1278
|
+
);
|
|
1279
|
+
} else {
|
|
1280
|
+
const argsString = chromeArgs.join(' ');
|
|
1281
|
+
await this.exec(
|
|
1282
|
+
shell,
|
|
1283
|
+
`chrome-for-testing ${argsString} "${url}" >/dev/null 2>&1 &`,
|
|
1284
|
+
30000
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Wait for Chrome to be ready
|
|
1289
|
+
await this.focusApplication('Google Chrome');
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
// Wait for URL to load
|
|
1293
|
+
try {
|
|
1294
|
+
const urlObj = new URL(url);
|
|
1295
|
+
const domain = urlObj.hostname;
|
|
1296
|
+
|
|
1297
|
+
console.log(`[provision.chrome] Waiting for domain "${domain}" to appear in URL bar...`);
|
|
1298
|
+
|
|
1299
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
1300
|
+
try {
|
|
1301
|
+
const result = await this.find(`${domain}`);
|
|
1302
|
+
if (result.found()) {
|
|
1303
|
+
console.log(`[provision.chrome] ✅ Chrome ready at ${url}`);
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
} catch (e) {
|
|
1307
|
+
// Not found yet, continue polling
|
|
1308
|
+
}
|
|
1309
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
await this.focusApplication('Google Chrome');
|
|
1313
|
+
} catch (e) {
|
|
1314
|
+
console.warn(`[provision.chrome] ⚠️ Could not parse URL "${url}":`, e.message);
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Launch VS Code
|
|
1320
|
+
* @param {Object} options - VS Code launch options
|
|
1321
|
+
* @param {string} [options.workspace] - Workspace/folder to open
|
|
1322
|
+
* @param {string[]} [options.extensions=[]] - Extensions to install
|
|
1323
|
+
* @returns {Promise<void>}
|
|
1324
|
+
*/
|
|
1325
|
+
vscode: async (options = {}) => {
|
|
1326
|
+
this._ensureConnected();
|
|
1327
|
+
|
|
1328
|
+
const {
|
|
1329
|
+
workspace = null,
|
|
1330
|
+
extensions = [],
|
|
1331
|
+
} = options;
|
|
1332
|
+
|
|
1333
|
+
// Install extensions if provided
|
|
1334
|
+
for (const extension of extensions) {
|
|
1335
|
+
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1336
|
+
await this.exec(
|
|
1337
|
+
shell,
|
|
1338
|
+
`code --install-extension ${extension}`,
|
|
1339
|
+
60000,
|
|
1340
|
+
true
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Launch VS Code
|
|
1345
|
+
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1346
|
+
const workspaceArg = workspace ? `"${workspace}"` : '';
|
|
1347
|
+
|
|
1348
|
+
if (this.os === 'windows') {
|
|
1349
|
+
await this.exec(
|
|
1350
|
+
shell,
|
|
1351
|
+
`Start-Process code -ArgumentList ${workspaceArg}`,
|
|
1352
|
+
30000
|
|
1353
|
+
);
|
|
1354
|
+
} else {
|
|
1355
|
+
await this.exec(
|
|
1356
|
+
shell,
|
|
1357
|
+
`code ${workspaceArg} >/dev/null 2>&1 &`,
|
|
1358
|
+
30000
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Wait for VS Code to be ready
|
|
1363
|
+
await this.focusApplication('Visual Studio Code');
|
|
1364
|
+
console.log('[provision.vscode] ✅ VS Code ready');
|
|
1365
|
+
},
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Launch Electron app
|
|
1369
|
+
* @param {Object} options - Electron launch options
|
|
1370
|
+
* @param {string} options.appPath - Path to Electron app (required)
|
|
1371
|
+
* @param {string[]} [options.args=[]] - Additional electron args
|
|
1372
|
+
* @returns {Promise<void>}
|
|
1373
|
+
*/
|
|
1374
|
+
electron: async (options = {}) => {
|
|
1375
|
+
this._ensureConnected();
|
|
1376
|
+
|
|
1377
|
+
const { appPath, args = [] } = options;
|
|
1378
|
+
|
|
1379
|
+
if (!appPath) {
|
|
1380
|
+
throw new Error('provision.electron requires appPath option');
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
const shell = this.os === 'windows' ? 'pwsh' : 'sh';
|
|
1384
|
+
const argsString = args.join(' ');
|
|
1385
|
+
|
|
1386
|
+
if (this.os === 'windows') {
|
|
1387
|
+
await this.exec(
|
|
1388
|
+
shell,
|
|
1389
|
+
`Start-Process electron -ArgumentList "${appPath}", ${argsString}`,
|
|
1390
|
+
30000
|
|
1391
|
+
);
|
|
1392
|
+
} else {
|
|
1393
|
+
await this.exec(
|
|
1394
|
+
shell,
|
|
1395
|
+
`electron "${appPath}" ${argsString} >/dev/null 2>&1 &`,
|
|
1396
|
+
30000
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
await this.focusApplication('Electron');
|
|
1401
|
+
console.log('[provision.electron] ✅ Electron app ready');
|
|
1402
|
+
},
|
|
1403
|
+
};
|
|
913
1404
|
}
|
|
914
1405
|
|
|
915
1406
|
/**
|
|
@@ -995,6 +1486,9 @@ class TestDriverSDK {
|
|
|
995
1486
|
this.agent.sandboxOs = connectOptions.os;
|
|
996
1487
|
} else if (this.sandboxOs) {
|
|
997
1488
|
this.agent.sandboxOs = this.sandboxOs;
|
|
1489
|
+
} else {
|
|
1490
|
+
// Fall back to this.os (which defaults to "linux")
|
|
1491
|
+
this.agent.sandboxOs = this.os;
|
|
998
1492
|
}
|
|
999
1493
|
|
|
1000
1494
|
// Set redrawThreshold on agent's cliArgs.options
|
|
@@ -1009,6 +1503,22 @@ class TestDriverSDK {
|
|
|
1009
1503
|
// Expose the agent's commands, parser, and commander
|
|
1010
1504
|
this.commands = this.agent.commands;
|
|
1011
1505
|
|
|
1506
|
+
// Recreate commands with dashcam elapsed time support
|
|
1507
|
+
const { createCommands } = require("./agent/lib/commands.js");
|
|
1508
|
+
const commandsResult = createCommands(
|
|
1509
|
+
this.agent.emitter,
|
|
1510
|
+
this.agent.system,
|
|
1511
|
+
this.agent.sandbox,
|
|
1512
|
+
this.agent.config,
|
|
1513
|
+
this.agent.session,
|
|
1514
|
+
() => this.agent.sourceMapper?.currentFilePath || this.agent.thisFile,
|
|
1515
|
+
this.agent.cliArgs.options.redrawThreshold,
|
|
1516
|
+
() => this.getDashcamElapsedTime(), // Pass dashcam elapsed time function
|
|
1517
|
+
);
|
|
1518
|
+
this.commands = commandsResult.commands;
|
|
1519
|
+
this.agent.commands = commandsResult.commands;
|
|
1520
|
+
this.agent.redraw = commandsResult.redraw;
|
|
1521
|
+
|
|
1012
1522
|
// Dynamically create command methods based on available commands
|
|
1013
1523
|
this._setupCommandMethods();
|
|
1014
1524
|
|
|
@@ -1027,13 +1537,19 @@ class TestDriverSDK {
|
|
|
1027
1537
|
* @returns {Promise<void>}
|
|
1028
1538
|
*/
|
|
1029
1539
|
async disconnect() {
|
|
1540
|
+
// Track disconnect event if we were connected
|
|
1030
1541
|
if (this.connected && this.instance) {
|
|
1031
|
-
// Track disconnect event
|
|
1032
1542
|
this.analytics.track("sdk.disconnect");
|
|
1543
|
+
}
|
|
1033
1544
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1545
|
+
// Always close the sandbox WebSocket connection to clean up resources
|
|
1546
|
+
// This ensures we don't leave orphaned connections even if connect() failed
|
|
1547
|
+
if (this.sandbox && typeof this.sandbox.close === 'function') {
|
|
1548
|
+
this.sandbox.close();
|
|
1036
1549
|
}
|
|
1550
|
+
|
|
1551
|
+
this.connected = false;
|
|
1552
|
+
this.instance = null;
|
|
1037
1553
|
}
|
|
1038
1554
|
|
|
1039
1555
|
/**
|
|
@@ -1054,16 +1570,24 @@ class TestDriverSDK {
|
|
|
1054
1570
|
* Automatically locates the element and returns it
|
|
1055
1571
|
*
|
|
1056
1572
|
* @param {string} description - Description of the element to find
|
|
1057
|
-
* @param {number} [
|
|
1058
|
-
* @returns {Promise<Element>} Element instance that has been located
|
|
1573
|
+
* @param {number | Object} [options] - Cache options: number for threshold, or object with {cacheKey, cacheThreshold}
|
|
1574
|
+
* @returns {Promise<Element> & ChainableElement} Element instance that has been located, with chainable methods
|
|
1059
1575
|
*
|
|
1060
1576
|
* @example
|
|
1061
|
-
* // Find and click immediately
|
|
1577
|
+
* // Find and click immediately (chainable)
|
|
1578
|
+
* await client.find('the sign in button').click();
|
|
1579
|
+
*
|
|
1580
|
+
* @example
|
|
1581
|
+
* // Find and click (traditional)
|
|
1062
1582
|
* const element = await client.find('the sign in button');
|
|
1063
1583
|
* await element.click();
|
|
1064
1584
|
*
|
|
1065
1585
|
* @example
|
|
1066
|
-
* // Find with
|
|
1586
|
+
* // Find with cache key to enable caching
|
|
1587
|
+
* const element = await client.find('login button', { cacheKey: 'my-test-run' });
|
|
1588
|
+
*
|
|
1589
|
+
* @example
|
|
1590
|
+
* // Find with custom cache threshold (legacy)
|
|
1067
1591
|
* const element = await client.find('login button', 0.01);
|
|
1068
1592
|
*
|
|
1069
1593
|
* @example
|
|
@@ -1077,10 +1601,14 @@ class TestDriverSDK {
|
|
|
1077
1601
|
* }
|
|
1078
1602
|
* await element.click();
|
|
1079
1603
|
*/
|
|
1080
|
-
|
|
1604
|
+
find(description, options) {
|
|
1081
1605
|
this._ensureConnected();
|
|
1082
1606
|
const element = new Element(description, this, this.system, this.commands);
|
|
1083
|
-
|
|
1607
|
+
const findPromise = element.find(null, options);
|
|
1608
|
+
|
|
1609
|
+
// Create a chainable promise that allows direct method chaining
|
|
1610
|
+
// e.g., await testdriver.find("button").click()
|
|
1611
|
+
return createChainablePromise(findPromise);
|
|
1084
1612
|
}
|
|
1085
1613
|
|
|
1086
1614
|
/**
|
|
@@ -1088,7 +1616,7 @@ class TestDriverSDK {
|
|
|
1088
1616
|
* Automatically locates all matching elements and returns them as an array
|
|
1089
1617
|
*
|
|
1090
1618
|
* @param {string} description - Description of the elements to find
|
|
1091
|
-
* @param {number} [
|
|
1619
|
+
* @param {number | Object} [options] - Cache options: number for threshold, or object with {cacheKey, cacheThreshold}
|
|
1092
1620
|
* @returns {Promise<Element[]>} Array of Element instances that have been located
|
|
1093
1621
|
*
|
|
1094
1622
|
* @example
|
|
@@ -1099,13 +1627,13 @@ class TestDriverSDK {
|
|
|
1099
1627
|
* }
|
|
1100
1628
|
*
|
|
1101
1629
|
* @example
|
|
1102
|
-
* // Find all list items with
|
|
1103
|
-
* const items = await client.findAll('list item',
|
|
1630
|
+
* // Find all list items with cache key to enable caching
|
|
1631
|
+
* const items = await client.findAll('list item', { cacheKey: 'my-test-run' });
|
|
1104
1632
|
* for (const item of items) {
|
|
1105
1633
|
* console.log(`Found item at (${item.x}, ${item.y})`);
|
|
1106
1634
|
* }
|
|
1107
1635
|
*/
|
|
1108
|
-
async findAll(description,
|
|
1636
|
+
async findAll(description, options) {
|
|
1109
1637
|
this._ensureConnected();
|
|
1110
1638
|
|
|
1111
1639
|
const startTime = Date.now();
|
|
@@ -1118,15 +1646,59 @@ class TestDriverSDK {
|
|
|
1118
1646
|
try {
|
|
1119
1647
|
const screenshot = await this.system.captureScreenBase64();
|
|
1120
1648
|
|
|
1121
|
-
//
|
|
1122
|
-
|
|
1649
|
+
// Handle options - can be a number (cacheThreshold) or object with cacheKey/cacheThreshold
|
|
1650
|
+
let cacheKey = null;
|
|
1651
|
+
let cacheThreshold = null;
|
|
1652
|
+
|
|
1653
|
+
if (typeof options === 'number') {
|
|
1654
|
+
// Legacy: options is just a number threshold
|
|
1655
|
+
cacheThreshold = options;
|
|
1656
|
+
} else if (typeof options === 'object' && options !== null) {
|
|
1657
|
+
// New: options is an object with cacheKey and/or cacheThreshold
|
|
1658
|
+
cacheKey = options.cacheKey || null;
|
|
1659
|
+
cacheThreshold = options.cacheThreshold ?? null;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Use default cacheKey from SDK constructor if not provided in findAll() options
|
|
1663
|
+
if (!cacheKey && this.options?.cacheKey) {
|
|
1664
|
+
cacheKey = this.options.cacheKey;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// Determine threshold:
|
|
1668
|
+
// - If cacheKey is provided, enable cache (threshold = 0.05 or custom)
|
|
1669
|
+
// - If no cacheKey, disable cache (threshold = -1) unless explicitly overridden
|
|
1670
|
+
let threshold;
|
|
1671
|
+
if (cacheKey) {
|
|
1672
|
+
// cacheKey provided - enable cache with threshold
|
|
1673
|
+
threshold = cacheThreshold ?? 0.05;
|
|
1674
|
+
} else if (cacheThreshold !== null) {
|
|
1675
|
+
// Explicit threshold provided without cacheKey
|
|
1676
|
+
threshold = cacheThreshold;
|
|
1677
|
+
} else {
|
|
1678
|
+
// No cacheKey, no explicit threshold - use global default (which is -1 now)
|
|
1679
|
+
threshold = this.cacheThresholds?.findAll ?? -1;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Debug log threshold
|
|
1683
|
+
const debugMode = process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG;
|
|
1684
|
+
if (debugMode) {
|
|
1685
|
+
const autoGenMsg = (this._autoGeneratedCacheKey && cacheKey === this.options.cacheKey)
|
|
1686
|
+
? ' (auto-generated from file hash)'
|
|
1687
|
+
: '';
|
|
1688
|
+
this.emitter.emit(
|
|
1689
|
+
events.log.debug,
|
|
1690
|
+
`🔍 findAll() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey}${autoGenMsg}` : ""})`,
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1123
1693
|
|
|
1124
1694
|
const response = await this.apiClient.req(
|
|
1125
1695
|
"/api/v7.0.0/testdriver-agent/testdriver-find-all",
|
|
1126
1696
|
{
|
|
1697
|
+
session: this.getSessionId(),
|
|
1127
1698
|
element: description,
|
|
1128
1699
|
image: screenshot,
|
|
1129
1700
|
threshold: threshold,
|
|
1701
|
+
cacheKey: cacheKey,
|
|
1130
1702
|
os: this.os,
|
|
1131
1703
|
resolution: this.resolution,
|
|
1132
1704
|
},
|
|
@@ -1173,6 +1745,27 @@ class TestDriverSDK {
|
|
|
1173
1745
|
return element;
|
|
1174
1746
|
});
|
|
1175
1747
|
|
|
1748
|
+
// Track successful findAll interaction
|
|
1749
|
+
const sessionId = this.getSessionId();
|
|
1750
|
+
if (sessionId && this.sandbox?.send) {
|
|
1751
|
+
try {
|
|
1752
|
+
await this.sandbox.send({
|
|
1753
|
+
type: "trackInteraction",
|
|
1754
|
+
interactionType: "findAll",
|
|
1755
|
+
session: sessionId,
|
|
1756
|
+
prompt: description,
|
|
1757
|
+
timestamp: startTime,
|
|
1758
|
+
success: true,
|
|
1759
|
+
input: { count: elements.length },
|
|
1760
|
+
cacheHit: response.cached || false,
|
|
1761
|
+
selector: response.selector,
|
|
1762
|
+
selectorUsed: !!response.selector,
|
|
1763
|
+
});
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
console.warn("Failed to track findAll interaction:", err.message);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1176
1769
|
// Log debug information when elements are found
|
|
1177
1770
|
if (process.env.VERBOSE || process.env.DEBUG || process.env.TD_DEBUG) {
|
|
1178
1771
|
const { events } = require("./agent/events.js");
|
|
@@ -1189,10 +1782,51 @@ class TestDriverSDK {
|
|
|
1189
1782
|
|
|
1190
1783
|
return elements;
|
|
1191
1784
|
} else {
|
|
1785
|
+
// No elements found - track interaction
|
|
1786
|
+
const sessionId = this.getSessionId();
|
|
1787
|
+
if (sessionId && this.sandbox?.send) {
|
|
1788
|
+
try {
|
|
1789
|
+
await this.sandbox.send({
|
|
1790
|
+
type: "trackInteraction",
|
|
1791
|
+
interactionType: "findAll",
|
|
1792
|
+
session: sessionId,
|
|
1793
|
+
prompt: description,
|
|
1794
|
+
timestamp: startTime,
|
|
1795
|
+
success: false,
|
|
1796
|
+
error: "No elements found",
|
|
1797
|
+
input: { count: 0 },
|
|
1798
|
+
cacheHit: response?.cached || false,
|
|
1799
|
+
selector: response?.selector,
|
|
1800
|
+
selectorUsed: !!response?.selector,
|
|
1801
|
+
});
|
|
1802
|
+
} catch (err) {
|
|
1803
|
+
console.warn("Failed to track findAll interaction:", err.message);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1192
1807
|
// No elements found - return empty array
|
|
1193
1808
|
return [];
|
|
1194
1809
|
}
|
|
1195
1810
|
} catch (error) {
|
|
1811
|
+
// Track findAll error interaction
|
|
1812
|
+
const sessionId = this.getSessionId();
|
|
1813
|
+
if (sessionId && this.sandbox?.send) {
|
|
1814
|
+
try {
|
|
1815
|
+
await this.sandbox.send({
|
|
1816
|
+
type: "trackInteraction",
|
|
1817
|
+
interactionType: "findAll",
|
|
1818
|
+
session: sessionId,
|
|
1819
|
+
prompt: description,
|
|
1820
|
+
timestamp: startTime,
|
|
1821
|
+
success: false,
|
|
1822
|
+
error: error.message,
|
|
1823
|
+
input: { count: 0 },
|
|
1824
|
+
});
|
|
1825
|
+
} catch (err) {
|
|
1826
|
+
console.warn("Failed to track findAll interaction:", err.message);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1196
1830
|
const { events } = require("./agent/events.js");
|
|
1197
1831
|
this.emitter.emit(events.log.log, `Error in findAll: ${error.message}`);
|
|
1198
1832
|
return [];
|
|
@@ -1245,17 +1879,18 @@ class TestDriverSDK {
|
|
|
1245
1879
|
*/
|
|
1246
1880
|
_setupCommandMethods() {
|
|
1247
1881
|
// Mapping from command names to SDK method names with type definitions
|
|
1882
|
+
// Each command supports both positional args (legacy) and object args (new)
|
|
1248
1883
|
const commandMapping = {
|
|
1249
1884
|
"hover-text": {
|
|
1250
1885
|
name: "hoverText",
|
|
1251
1886
|
/**
|
|
1252
1887
|
* Hover over text on screen
|
|
1253
1888
|
* @deprecated Use find() and element.click() instead
|
|
1254
|
-
* @param {string}
|
|
1255
|
-
* @param {string
|
|
1256
|
-
* @param {
|
|
1257
|
-
* @param {
|
|
1258
|
-
* @param {number} [timeout=5000] - Timeout in milliseconds
|
|
1889
|
+
* @param {Object|string} options - Options object or text (legacy positional)
|
|
1890
|
+
* @param {string} options.text - Text to find and hover over
|
|
1891
|
+
* @param {string|null} [options.description] - Optional description of the element
|
|
1892
|
+
* @param {ClickAction} [options.action='click'] - Action to perform
|
|
1893
|
+
* @param {number} [options.timeout=5000] - Timeout in milliseconds
|
|
1259
1894
|
* @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
|
|
1260
1895
|
*/
|
|
1261
1896
|
doc: "Hover over text on screen (deprecated - use find() instead)",
|
|
@@ -1265,8 +1900,9 @@ class TestDriverSDK {
|
|
|
1265
1900
|
/**
|
|
1266
1901
|
* Hover over an image on screen
|
|
1267
1902
|
* @deprecated Use find() and element.click() instead
|
|
1268
|
-
* @param {string}
|
|
1269
|
-
* @param {
|
|
1903
|
+
* @param {Object|string} options - Options object or description (legacy positional)
|
|
1904
|
+
* @param {string} options.description - Description of the image to find
|
|
1905
|
+
* @param {ClickAction} [options.action='click'] - Action to perform
|
|
1270
1906
|
* @returns {Promise<{x: number, y: number, centerX: number, centerY: number}>}
|
|
1271
1907
|
*/
|
|
1272
1908
|
doc: "Hover over an image on screen (deprecated - use find() instead)",
|
|
@@ -1275,9 +1911,10 @@ class TestDriverSDK {
|
|
|
1275
1911
|
name: "matchImage",
|
|
1276
1912
|
/**
|
|
1277
1913
|
* Match and interact with an image template
|
|
1278
|
-
* @param {string}
|
|
1279
|
-
* @param {
|
|
1280
|
-
* @param {
|
|
1914
|
+
* @param {Object|string} options - Options object or path (legacy positional)
|
|
1915
|
+
* @param {string} options.path - Path to the image template
|
|
1916
|
+
* @param {ClickAction} [options.action='click'] - Action to perform
|
|
1917
|
+
* @param {boolean} [options.invert=false] - Invert the match
|
|
1281
1918
|
* @returns {Promise<boolean>}
|
|
1282
1919
|
*/
|
|
1283
1920
|
doc: "Match and interact with an image template",
|
|
@@ -1286,17 +1923,20 @@ class TestDriverSDK {
|
|
|
1286
1923
|
name: "type",
|
|
1287
1924
|
/**
|
|
1288
1925
|
* Type text
|
|
1289
|
-
* @param {string
|
|
1290
|
-
* @param {
|
|
1926
|
+
* @param {string|number} text - Text to type
|
|
1927
|
+
* @param {Object} [options] - Additional options
|
|
1928
|
+
* @param {number} [options.delay=250] - Delay between keystrokes in milliseconds
|
|
1929
|
+
* @param {boolean} [options.secret=false] - If true, text is treated as sensitive (not logged or stored)
|
|
1291
1930
|
* @returns {Promise<void>}
|
|
1292
1931
|
*/
|
|
1293
|
-
doc: "Type text",
|
|
1932
|
+
doc: "Type text (use { secret: true } for passwords)",
|
|
1294
1933
|
},
|
|
1295
1934
|
"press-keys": {
|
|
1296
1935
|
name: "pressKeys",
|
|
1297
1936
|
/**
|
|
1298
1937
|
* Press keyboard keys
|
|
1299
1938
|
* @param {KeyboardKey[]} keys - Array of keys to press
|
|
1939
|
+
* @param {Object} [options] - Additional options (reserved for future use)
|
|
1300
1940
|
* @returns {Promise<void>}
|
|
1301
1941
|
*/
|
|
1302
1942
|
doc: "Press keyboard keys",
|
|
@@ -1305,9 +1945,10 @@ class TestDriverSDK {
|
|
|
1305
1945
|
name: "click",
|
|
1306
1946
|
/**
|
|
1307
1947
|
* Click at coordinates
|
|
1308
|
-
* @param {number}
|
|
1309
|
-
* @param {number}
|
|
1310
|
-
* @param {
|
|
1948
|
+
* @param {Object|number} options - Options object or x coordinate (legacy positional)
|
|
1949
|
+
* @param {number} options.x - X coordinate
|
|
1950
|
+
* @param {number} options.y - Y coordinate
|
|
1951
|
+
* @param {ClickAction} [options.action='click'] - Type of click action
|
|
1311
1952
|
* @returns {Promise<void>}
|
|
1312
1953
|
*/
|
|
1313
1954
|
doc: "Click at coordinates",
|
|
@@ -1316,8 +1957,9 @@ class TestDriverSDK {
|
|
|
1316
1957
|
name: "hover",
|
|
1317
1958
|
/**
|
|
1318
1959
|
* Hover at coordinates
|
|
1319
|
-
* @param {number}
|
|
1320
|
-
* @param {number}
|
|
1960
|
+
* @param {Object|number} options - Options object or x coordinate (legacy positional)
|
|
1961
|
+
* @param {number} options.x - X coordinate
|
|
1962
|
+
* @param {number} options.y - Y coordinate
|
|
1321
1963
|
* @returns {Promise<void>}
|
|
1322
1964
|
*/
|
|
1323
1965
|
doc: "Hover at coordinates",
|
|
@@ -1327,7 +1969,8 @@ class TestDriverSDK {
|
|
|
1327
1969
|
/**
|
|
1328
1970
|
* Scroll the page
|
|
1329
1971
|
* @param {ScrollDirection} [direction='down'] - Direction to scroll
|
|
1330
|
-
* @param {
|
|
1972
|
+
* @param {Object} [options] - Additional options
|
|
1973
|
+
* @param {number} [options.amount=300] - Amount to scroll in pixels
|
|
1331
1974
|
* @returns {Promise<void>}
|
|
1332
1975
|
*/
|
|
1333
1976
|
doc: "Scroll the page",
|
|
@@ -1338,6 +1981,7 @@ class TestDriverSDK {
|
|
|
1338
1981
|
* Wait for specified time
|
|
1339
1982
|
* @deprecated Consider using element polling with find() instead of arbitrary waits
|
|
1340
1983
|
* @param {number} [timeout=3000] - Time to wait in milliseconds
|
|
1984
|
+
* @param {Object} [options] - Additional options (reserved for future use)
|
|
1341
1985
|
* @returns {Promise<void>}
|
|
1342
1986
|
*/
|
|
1343
1987
|
doc: "Wait for specified time (deprecated - consider element polling instead)",
|
|
@@ -1347,10 +1991,9 @@ class TestDriverSDK {
|
|
|
1347
1991
|
/**
|
|
1348
1992
|
* Wait for text to appear on screen
|
|
1349
1993
|
* @deprecated Use find() in a polling loop instead
|
|
1350
|
-
* @param {string}
|
|
1351
|
-
* @param {
|
|
1352
|
-
* @param {
|
|
1353
|
-
* @param {boolean} [invert=false] - Invert the match (wait for text to disappear)
|
|
1994
|
+
* @param {Object|string} options - Options object or text (legacy positional)
|
|
1995
|
+
* @param {string} options.text - Text to wait for
|
|
1996
|
+
* @param {number} [options.timeout=5000] - Timeout in milliseconds
|
|
1354
1997
|
* @returns {Promise<void>}
|
|
1355
1998
|
*/
|
|
1356
1999
|
doc: "Wait for text to appear on screen (deprecated - use find() in a loop instead)",
|
|
@@ -1360,9 +2003,9 @@ class TestDriverSDK {
|
|
|
1360
2003
|
/**
|
|
1361
2004
|
* Wait for image to appear on screen
|
|
1362
2005
|
* @deprecated Use find() in a polling loop instead
|
|
1363
|
-
* @param {string}
|
|
1364
|
-
* @param {
|
|
1365
|
-
* @param {
|
|
2006
|
+
* @param {Object|string} options - Options object or description (legacy positional)
|
|
2007
|
+
* @param {string} options.description - Description of the image
|
|
2008
|
+
* @param {number} [options.timeout=10000] - Timeout in milliseconds
|
|
1366
2009
|
* @returns {Promise<void>}
|
|
1367
2010
|
*/
|
|
1368
2011
|
doc: "Wait for image to appear on screen (deprecated - use find() in a loop instead)",
|
|
@@ -1371,12 +2014,11 @@ class TestDriverSDK {
|
|
|
1371
2014
|
name: "scrollUntilText",
|
|
1372
2015
|
/**
|
|
1373
2016
|
* Scroll until text is found
|
|
1374
|
-
* @param {string}
|
|
1375
|
-
* @param {
|
|
1376
|
-
* @param {
|
|
1377
|
-
* @param {
|
|
1378
|
-
* @param {
|
|
1379
|
-
* @param {boolean} [invert=false] - Invert the match
|
|
2017
|
+
* @param {Object|string} options - Options object or text (legacy positional)
|
|
2018
|
+
* @param {string} options.text - Text to find
|
|
2019
|
+
* @param {ScrollDirection} [options.direction='down'] - Scroll direction
|
|
2020
|
+
* @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
|
|
2021
|
+
* @param {boolean} [options.invert=false] - Invert the match
|
|
1380
2022
|
* @returns {Promise<void>}
|
|
1381
2023
|
*/
|
|
1382
2024
|
doc: "Scroll until text is found",
|
|
@@ -1385,12 +2027,13 @@ class TestDriverSDK {
|
|
|
1385
2027
|
name: "scrollUntilImage",
|
|
1386
2028
|
/**
|
|
1387
2029
|
* Scroll until image is found
|
|
1388
|
-
* @param {string}
|
|
1389
|
-
* @param {
|
|
1390
|
-
* @param {
|
|
1391
|
-
* @param {
|
|
1392
|
-
* @param {string
|
|
1393
|
-
* @param {
|
|
2030
|
+
* @param {Object|string} [options] - Options object or description (legacy positional)
|
|
2031
|
+
* @param {string} [options.description] - Description of the image
|
|
2032
|
+
* @param {ScrollDirection} [options.direction='down'] - Scroll direction
|
|
2033
|
+
* @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
|
|
2034
|
+
* @param {string} [options.method='mouse'] - Scroll method
|
|
2035
|
+
* @param {string} [options.path] - Path to image template
|
|
2036
|
+
* @param {boolean} [options.invert=false] - Invert the match
|
|
1394
2037
|
* @returns {Promise<void>}
|
|
1395
2038
|
*/
|
|
1396
2039
|
doc: "Scroll until image is found",
|
|
@@ -1400,6 +2043,7 @@ class TestDriverSDK {
|
|
|
1400
2043
|
/**
|
|
1401
2044
|
* Focus an application by name
|
|
1402
2045
|
* @param {string} name - Application name
|
|
2046
|
+
* @param {Object} [options] - Additional options (reserved for future use)
|
|
1403
2047
|
* @returns {Promise<string>}
|
|
1404
2048
|
*/
|
|
1405
2049
|
doc: "Focus an application by name",
|
|
@@ -1408,7 +2052,8 @@ class TestDriverSDK {
|
|
|
1408
2052
|
name: "remember",
|
|
1409
2053
|
/**
|
|
1410
2054
|
* Extract and remember information from the screen using AI
|
|
1411
|
-
* @param {string}
|
|
2055
|
+
* @param {Object|string} options - Options object or description (legacy positional)
|
|
2056
|
+
* @param {string} options.description - What to remember
|
|
1412
2057
|
* @returns {Promise<string>}
|
|
1413
2058
|
*/
|
|
1414
2059
|
doc: "Extract and remember information from the screen",
|
|
@@ -1418,6 +2063,7 @@ class TestDriverSDK {
|
|
|
1418
2063
|
/**
|
|
1419
2064
|
* Make an AI-powered assertion
|
|
1420
2065
|
* @param {string} assertion - Assertion to check
|
|
2066
|
+
* @param {Object} [options] - Additional options (reserved for future use)
|
|
1421
2067
|
* @returns {Promise<boolean>}
|
|
1422
2068
|
*/
|
|
1423
2069
|
doc: "Make an AI-powered assertion",
|
|
@@ -1426,10 +2072,11 @@ class TestDriverSDK {
|
|
|
1426
2072
|
name: "exec",
|
|
1427
2073
|
/**
|
|
1428
2074
|
* Execute code in the sandbox
|
|
1429
|
-
* @param {ExecLanguage}
|
|
1430
|
-
* @param {
|
|
1431
|
-
* @param {
|
|
1432
|
-
* @param {
|
|
2075
|
+
* @param {Object|ExecLanguage} options - Options object or language (legacy positional)
|
|
2076
|
+
* @param {ExecLanguage} [options.language='pwsh'] - Language ('js', 'pwsh', or 'sh')
|
|
2077
|
+
* @param {string} options.code - Code to execute
|
|
2078
|
+
* @param {number} [options.timeout] - Timeout in milliseconds
|
|
2079
|
+
* @param {boolean} [options.silent=false] - Suppress output
|
|
1433
2080
|
* @returns {Promise<string>}
|
|
1434
2081
|
*/
|
|
1435
2082
|
doc: "Execute code in the sandbox",
|
|
@@ -1568,6 +2215,9 @@ class TestDriverSDK {
|
|
|
1568
2215
|
* @private
|
|
1569
2216
|
*/
|
|
1570
2217
|
_setupLogging() {
|
|
2218
|
+
// Track the last fatal error message to throw on exit
|
|
2219
|
+
let lastFatalError = null;
|
|
2220
|
+
|
|
1571
2221
|
// Set up markdown logger
|
|
1572
2222
|
createMarkdownLogger(this.emitter);
|
|
1573
2223
|
|
|
@@ -1580,6 +2230,9 @@ class TestDriverSDK {
|
|
|
1580
2230
|
? `[${this.testContext}] ${message}`
|
|
1581
2231
|
: message;
|
|
1582
2232
|
console.log(prefixedMessage);
|
|
2233
|
+
|
|
2234
|
+
// Also forward to sandbox for dashcam
|
|
2235
|
+
this._forwardLogToSandbox(prefixedMessage);
|
|
1583
2236
|
}
|
|
1584
2237
|
});
|
|
1585
2238
|
|
|
@@ -1587,6 +2240,11 @@ class TestDriverSDK {
|
|
|
1587
2240
|
if (this.loggingEnabled) {
|
|
1588
2241
|
const event = this.emitter.event;
|
|
1589
2242
|
console.error(event, ":", data);
|
|
2243
|
+
|
|
2244
|
+
// Capture fatal errors
|
|
2245
|
+
if (event === events.error.fatal) {
|
|
2246
|
+
lastFatalError = data;
|
|
2247
|
+
}
|
|
1590
2248
|
}
|
|
1591
2249
|
});
|
|
1592
2250
|
|
|
@@ -1613,6 +2271,19 @@ class TestDriverSDK {
|
|
|
1613
2271
|
}
|
|
1614
2272
|
});
|
|
1615
2273
|
|
|
2274
|
+
// Handle exit events - throw error with meaningful message instead of calling process.exit
|
|
2275
|
+
// This allows test frameworks like Vitest to properly catch and display the error
|
|
2276
|
+
this.emitter.on(events.exit, (exitCode) => {
|
|
2277
|
+
if (exitCode !== 0) {
|
|
2278
|
+
// Create an error with the fatal error message if available
|
|
2279
|
+
const errorMessage = lastFatalError || 'TestDriver fatal error';
|
|
2280
|
+
const error = new Error(errorMessage);
|
|
2281
|
+
error.name = 'TestDriverFatalError';
|
|
2282
|
+
error.exitCode = exitCode;
|
|
2283
|
+
throw error;
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
|
|
1616
2287
|
// Handle show window events for sandbox visualization
|
|
1617
2288
|
this.emitter.on("show-window", async (url) => {
|
|
1618
2289
|
if (this.loggingEnabled) {
|
|
@@ -1748,13 +2419,42 @@ class TestDriverSDK {
|
|
|
1748
2419
|
);
|
|
1749
2420
|
await sdk.auth();
|
|
1750
2421
|
|
|
1751
|
-
const platform = options.platform || this.config.TD_PLATFORM || "
|
|
2422
|
+
const platform = options.platform || this.config.TD_PLATFORM || "linux";
|
|
1752
2423
|
|
|
1753
2424
|
// Auto-detect sandbox ID from the active sandbox if not provided
|
|
1754
2425
|
const sandboxId = options.sandboxId || this.agent?.sandbox?.id || null;
|
|
1755
2426
|
|
|
1756
|
-
// Get session ID
|
|
1757
|
-
|
|
2427
|
+
// Get or create session ID using the agent's newSession method
|
|
2428
|
+
let sessionId = this.agent?.sessionInstance?.get() || null;
|
|
2429
|
+
|
|
2430
|
+
// If no session exists, create one using the agent's method
|
|
2431
|
+
if (!sessionId && this.agent?.newSession) {
|
|
2432
|
+
try {
|
|
2433
|
+
await this.agent.newSession();
|
|
2434
|
+
sessionId = this.agent.sessionInstance.get();
|
|
2435
|
+
|
|
2436
|
+
// Save session ID to file for reuse across test runs
|
|
2437
|
+
if (sessionId) {
|
|
2438
|
+
const sessionFile = path.join(os.homedir(), '.testdriverai-session');
|
|
2439
|
+
fs.writeFileSync(sessionFile, sessionId, { encoding: 'utf-8' });
|
|
2440
|
+
}
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
// Log but don't fail - tests can run without a session
|
|
2443
|
+
console.warn('Failed to create session:', error.message);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// If still no session, try reading from file (for reporter/separate processes)
|
|
2448
|
+
if (!sessionId) {
|
|
2449
|
+
try {
|
|
2450
|
+
const sessionFile = path.join(os.homedir(), '.testdriverai-session');
|
|
2451
|
+
if (fs.existsSync(sessionFile)) {
|
|
2452
|
+
sessionId = fs.readFileSync(sessionFile, 'utf-8').trim();
|
|
2453
|
+
}
|
|
2454
|
+
} catch (error) {
|
|
2455
|
+
// Ignore file read errors
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
1758
2458
|
|
|
1759
2459
|
const data = {
|
|
1760
2460
|
runId: options.runId,
|
|
@@ -1884,21 +2584,34 @@ class TestDriverSDK {
|
|
|
1884
2584
|
*
|
|
1885
2585
|
* @example
|
|
1886
2586
|
* // Simple execution
|
|
1887
|
-
* await client.
|
|
2587
|
+
* await client.act('Click the submit button');
|
|
1888
2588
|
*
|
|
1889
2589
|
* @example
|
|
1890
2590
|
* // With validation loop
|
|
1891
|
-
* const result = await client.
|
|
2591
|
+
* const result = await client.act('Fill out the contact form', { validateAndLoop: true });
|
|
1892
2592
|
* console.log(result); // AI's final assessment
|
|
1893
2593
|
*/
|
|
1894
|
-
async
|
|
2594
|
+
async act(task) {
|
|
1895
2595
|
this._ensureConnected();
|
|
1896
2596
|
|
|
1897
|
-
this.analytics.track("sdk.
|
|
2597
|
+
this.analytics.track("sdk.act", { task });
|
|
1898
2598
|
|
|
1899
2599
|
// Use the agent's exploratoryLoop method directly
|
|
1900
2600
|
return await this.agent.exploratoryLoop(task, false, true, false);
|
|
1901
2601
|
}
|
|
2602
|
+
|
|
2603
|
+
/**
|
|
2604
|
+
* @deprecated Use act() instead
|
|
2605
|
+
* Execute a natural language task using AI
|
|
2606
|
+
*
|
|
2607
|
+
* @param {string} task - Natural language description of what to do
|
|
2608
|
+
* @param {Object} options - Execution options
|
|
2609
|
+
* @param {boolean} [options.validateAndLoop=false] - Whether to validate completion and retry if incomplete
|
|
2610
|
+
* @returns {Promise<string|void>} Final AI response if validateAndLoop is true
|
|
2611
|
+
*/
|
|
2612
|
+
async ai(task) {
|
|
2613
|
+
return await this.act(task);
|
|
2614
|
+
}
|
|
1902
2615
|
}
|
|
1903
2616
|
|
|
1904
2617
|
module.exports = TestDriverSDK;
|