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/agent/lib/redraw.js
CHANGED
|
@@ -3,13 +3,28 @@ const fs = require("fs");
|
|
|
3
3
|
const { events } = require("../events");
|
|
4
4
|
const theme = require("./theme");
|
|
5
5
|
|
|
6
|
+
// Default redraw options
|
|
7
|
+
const DEFAULT_REDRAW_OPTIONS = {
|
|
8
|
+
enabled: true, // Master switch to enable/disable redraw detection
|
|
9
|
+
screenRedraw: true, // Enable screen redraw detection
|
|
10
|
+
networkMonitor: true, // Enable network activity monitoring
|
|
11
|
+
diffThreshold: 0.01, // Percentage threshold for screen diff (0.01 = 0.01%)
|
|
12
|
+
};
|
|
13
|
+
|
|
6
14
|
// Factory function that creates redraw functionality with the provided system instance
|
|
7
15
|
const createRedraw = (
|
|
8
16
|
emitter,
|
|
9
17
|
system,
|
|
10
18
|
sandbox,
|
|
11
|
-
|
|
19
|
+
defaultOptions = {},
|
|
12
20
|
) => {
|
|
21
|
+
// Merge default options with provided defaults
|
|
22
|
+
const baseOptions = { ...DEFAULT_REDRAW_OPTIONS, ...defaultOptions };
|
|
23
|
+
// Support legacy redrawThresholdPercent number argument
|
|
24
|
+
if (typeof defaultOptions === 'number') {
|
|
25
|
+
baseOptions.diffThreshold = defaultOptions;
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
const networkUpdateInterval = 15000;
|
|
14
29
|
|
|
15
30
|
let lastTxBytes = null;
|
|
@@ -116,16 +131,13 @@ const createRedraw = (
|
|
|
116
131
|
{ threshold: 0.1 },
|
|
117
132
|
);
|
|
118
133
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const diffPercentage = (differentPixels / totalPixels) * 100;
|
|
124
|
-
return diffPercentage.toFixed(1);
|
|
125
|
-
}
|
|
134
|
+
// Calculate percentage difference based on pixel differences
|
|
135
|
+
// Always return a number (0 if no difference)
|
|
136
|
+
const diffPercentage = (differentPixels / totalPixels) * 100;
|
|
137
|
+
return parseFloat(diffPercentage.toFixed(2));
|
|
126
138
|
} catch (error) {
|
|
127
139
|
console.error("Error comparing images:", error);
|
|
128
|
-
return false
|
|
140
|
+
return 0; // Return 0 on error instead of false
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
143
|
|
|
@@ -146,46 +158,104 @@ const createRedraw = (
|
|
|
146
158
|
}
|
|
147
159
|
}
|
|
148
160
|
|
|
149
|
-
|
|
161
|
+
// Current options for the active redraw cycle
|
|
162
|
+
let currentOptions = { ...baseOptions };
|
|
163
|
+
|
|
164
|
+
async function start(options = {}) {
|
|
165
|
+
// Merge base options with per-call options
|
|
166
|
+
currentOptions = { ...baseOptions, ...options };
|
|
167
|
+
|
|
168
|
+
console.log('[redraw] start() called with options:', JSON.stringify(currentOptions));
|
|
169
|
+
|
|
170
|
+
// If redraw is completely disabled, return early
|
|
171
|
+
if (!currentOptions.enabled) {
|
|
172
|
+
console.log('[redraw] start() - redraw disabled, returning null');
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// If both screenRedraw and networkMonitor are disabled, disable redraw
|
|
177
|
+
if (!currentOptions.screenRedraw && !currentOptions.networkMonitor) {
|
|
178
|
+
currentOptions.enabled = false;
|
|
179
|
+
console.log('[redraw] start() - both screenRedraw and networkMonitor disabled, returning null');
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
150
183
|
resetState();
|
|
151
|
-
|
|
152
|
-
|
|
184
|
+
|
|
185
|
+
// Only start network monitoring if enabled
|
|
186
|
+
if (currentOptions.networkMonitor) {
|
|
187
|
+
startNetworkMonitoring();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Only capture start image if screen redraw is enabled
|
|
191
|
+
if (currentOptions.screenRedraw) {
|
|
192
|
+
startImage = await system.captureScreenPNG(0.25, true);
|
|
193
|
+
console.log('[redraw] start() - captured startImage:', startImage);
|
|
194
|
+
}
|
|
195
|
+
|
|
153
196
|
return startImage;
|
|
154
197
|
}
|
|
155
198
|
|
|
156
|
-
async function checkCondition(resolve, startTime, timeoutMs) {
|
|
157
|
-
|
|
199
|
+
async function checkCondition(resolve, startTime, timeoutMs, options) {
|
|
200
|
+
const { enabled, screenRedraw, networkMonitor, diffThreshold } = options;
|
|
201
|
+
|
|
202
|
+
// If redraw is disabled, resolve immediately
|
|
203
|
+
if (!enabled) {
|
|
204
|
+
resolve("true");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let nowImage = screenRedraw ? await system.captureScreenPNG(0.25, true) : null;
|
|
158
209
|
let timeElapsed = Date.now() - startTime;
|
|
159
210
|
let diffPercent = 0;
|
|
160
211
|
let isTimeout = timeElapsed > timeoutMs;
|
|
161
212
|
|
|
162
|
-
if
|
|
213
|
+
// Check screen redraw if enabled and we have a start image to compare against
|
|
214
|
+
if (screenRedraw && !screenHasRedrawn && startImage && nowImage) {
|
|
215
|
+
console.log('[redraw] checkCondition() - comparing images:', { startImage, nowImage });
|
|
163
216
|
diffPercent = await imageDiffPercent(startImage, nowImage);
|
|
164
|
-
|
|
217
|
+
console.log('[redraw] checkCondition() - diffPercent:', diffPercent, 'threshold:', diffThreshold);
|
|
218
|
+
screenHasRedrawn = diffPercent > diffThreshold;
|
|
219
|
+
console.log('[redraw] checkCondition() - screenHasRedrawn:', screenHasRedrawn);
|
|
220
|
+
} else if (screenRedraw && !startImage) {
|
|
221
|
+
// If no start image was captured, capture one now and wait for next check
|
|
222
|
+
console.log('[redraw] checkCondition() - no startImage, capturing now');
|
|
223
|
+
startImage = await system.captureScreenPNG(0.25, true);
|
|
165
224
|
}
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
225
|
+
|
|
226
|
+
// If screen redraw is disabled, consider it as "redrawn"
|
|
227
|
+
const effectiveScreenRedrawn = screenRedraw ? screenHasRedrawn : true;
|
|
228
|
+
// If network monitor is disabled, consider it as "settled"
|
|
229
|
+
const effectiveNetworkSettled = networkMonitor ? networkSettled : true;
|
|
230
|
+
|
|
231
|
+
// Log redraw status
|
|
232
|
+
let redrawText = !screenRedraw
|
|
233
|
+
? theme.dim(`disabled`)
|
|
234
|
+
: effectiveScreenRedrawn
|
|
235
|
+
? theme.green(`y`)
|
|
236
|
+
: theme.dim(`${diffPercent}/${diffThreshold}%`);
|
|
237
|
+
let networkText = !networkMonitor
|
|
238
|
+
? theme.dim(`disabled`)
|
|
239
|
+
: effectiveNetworkSettled
|
|
240
|
+
? theme.green(`y`)
|
|
241
|
+
: theme.dim(
|
|
242
|
+
`${Math.trunc((diffRxBytes + diffTxBytes) / networkUpdateInterval)}b/s`,
|
|
243
|
+
);
|
|
176
244
|
let timeoutText = isTimeout
|
|
177
245
|
? theme.green(`y`)
|
|
178
246
|
: theme.dim(`${Math.floor(timeElapsed / 1000)}/${timeoutMs / 1000}s`);
|
|
179
247
|
|
|
180
248
|
emitter.emit(events.redraw.status, {
|
|
181
249
|
redraw: {
|
|
182
|
-
|
|
250
|
+
enabled: screenRedraw,
|
|
251
|
+
hasRedrawn: effectiveScreenRedrawn,
|
|
183
252
|
diffPercent,
|
|
184
|
-
threshold:
|
|
253
|
+
threshold: diffThreshold,
|
|
185
254
|
text: redrawText,
|
|
186
255
|
},
|
|
187
256
|
network: {
|
|
188
|
-
|
|
257
|
+
enabled: networkMonitor,
|
|
258
|
+
settled: effectiveNetworkSettled,
|
|
189
259
|
rxBytes: diffRxBytes,
|
|
190
260
|
txBytes: diffTxBytes,
|
|
191
261
|
text: networkText,
|
|
@@ -198,27 +268,42 @@ const createRedraw = (
|
|
|
198
268
|
},
|
|
199
269
|
});
|
|
200
270
|
|
|
201
|
-
if ((
|
|
271
|
+
if ((effectiveScreenRedrawn && effectiveNetworkSettled) || isTimeout) {
|
|
202
272
|
emitter.emit(events.redraw.complete, {
|
|
203
|
-
screenHasRedrawn,
|
|
204
|
-
networkSettled,
|
|
273
|
+
screenHasRedrawn: effectiveScreenRedrawn,
|
|
274
|
+
networkSettled: effectiveNetworkSettled,
|
|
205
275
|
isTimeout,
|
|
206
276
|
timeElapsed,
|
|
207
277
|
});
|
|
208
278
|
resolve("true");
|
|
209
279
|
} else {
|
|
210
280
|
setTimeout(() => {
|
|
211
|
-
checkCondition(resolve, startTime, timeoutMs);
|
|
281
|
+
checkCondition(resolve, startTime, timeoutMs, options);
|
|
212
282
|
}, 500);
|
|
213
283
|
}
|
|
214
284
|
}
|
|
215
285
|
|
|
216
|
-
function wait(timeoutMs) {
|
|
286
|
+
function wait(timeoutMs, options = {}) {
|
|
287
|
+
// Merge current options with any per-call overrides
|
|
288
|
+
const waitOptions = { ...currentOptions, ...options };
|
|
289
|
+
|
|
290
|
+
// If redraw is disabled, resolve immediately
|
|
291
|
+
if (!waitOptions.enabled) {
|
|
292
|
+
return Promise.resolve("true");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// If both are disabled, resolve immediately
|
|
296
|
+
if (!waitOptions.screenRedraw && !waitOptions.networkMonitor) {
|
|
297
|
+
return Promise.resolve("true");
|
|
298
|
+
}
|
|
299
|
+
|
|
217
300
|
return new Promise((resolve) => {
|
|
218
301
|
const startTime = Date.now();
|
|
219
|
-
// Start network monitoring if not already started
|
|
220
|
-
|
|
221
|
-
|
|
302
|
+
// Start network monitoring if not already started and enabled
|
|
303
|
+
if (waitOptions.networkMonitor) {
|
|
304
|
+
startNetworkMonitoring();
|
|
305
|
+
}
|
|
306
|
+
checkCondition(resolve, startTime, timeoutMs, waitOptions);
|
|
222
307
|
});
|
|
223
308
|
}
|
|
224
309
|
|
|
@@ -226,7 +311,7 @@ const createRedraw = (
|
|
|
226
311
|
stopNetworkMonitoring(networkInterval);
|
|
227
312
|
}
|
|
228
313
|
|
|
229
|
-
return { start, wait, cleanup };
|
|
314
|
+
return { start, wait, cleanup, DEFAULT_OPTIONS: DEFAULT_REDRAW_OPTIONS };
|
|
230
315
|
};
|
|
231
316
|
|
|
232
|
-
module.exports = { createRedraw };
|
|
317
|
+
module.exports = { createRedraw, DEFAULT_REDRAW_OPTIONS };
|
package/agent/lib/sandbox.js
CHANGED
|
@@ -2,7 +2,7 @@ const WebSocket = require("ws");
|
|
|
2
2
|
const marky = require("marky");
|
|
3
3
|
const { events } = require("../events");
|
|
4
4
|
|
|
5
|
-
const createSandbox = (emitter, analytics) => {
|
|
5
|
+
const createSandbox = (emitter, analytics, sessionInstance) => {
|
|
6
6
|
class Sandbox {
|
|
7
7
|
constructor() {
|
|
8
8
|
this.socket = null;
|
|
@@ -15,6 +15,7 @@ const createSandbox = (emitter, analytics) => {
|
|
|
15
15
|
this.messageId = 0;
|
|
16
16
|
this.uniqueId = Math.random().toString(36).substring(7);
|
|
17
17
|
this.os = null; // Store OS value to send with every message
|
|
18
|
+
this.sessionInstance = sessionInstance; // Store session instance to include in messages
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
send(message) {
|
|
@@ -35,6 +36,14 @@ const createSandbox = (emitter, analytics) => {
|
|
|
35
36
|
message.os = this.os;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
// Add session to every message if available (for interaction tracking)
|
|
40
|
+
if (this.sessionInstance && !message.session) {
|
|
41
|
+
const sessionId = this.sessionInstance.get();
|
|
42
|
+
if (sessionId) {
|
|
43
|
+
message.session = sessionId;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
// Start timing for this message
|
|
39
48
|
const timingKey = `sandbox-${message.type}`;
|
|
40
49
|
marky.mark(timingKey);
|
|
@@ -114,8 +123,8 @@ const createSandbox = (emitter, analytics) => {
|
|
|
114
123
|
this.socket.on("open", async () => {
|
|
115
124
|
this.apiSocketConnected = true;
|
|
116
125
|
|
|
117
|
-
setInterval(() => {
|
|
118
|
-
if (this.socket.readyState === WebSocket.OPEN) {
|
|
126
|
+
this.heartbeat = setInterval(() => {
|
|
127
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
119
128
|
this.socket.ping();
|
|
120
129
|
}
|
|
121
130
|
}, 5000);
|
|
@@ -166,6 +175,34 @@ const createSandbox = (emitter, analytics) => {
|
|
|
166
175
|
});
|
|
167
176
|
});
|
|
168
177
|
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Close the WebSocket connection and clean up resources
|
|
181
|
+
*/
|
|
182
|
+
close() {
|
|
183
|
+
if (this.heartbeat) {
|
|
184
|
+
clearInterval(this.heartbeat);
|
|
185
|
+
this.heartbeat = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (this.socket) {
|
|
189
|
+
try {
|
|
190
|
+
this.socket.close();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
// Ignore close errors
|
|
193
|
+
}
|
|
194
|
+
this.socket = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.apiSocketConnected = false;
|
|
198
|
+
this.instanceSocketConnected = false;
|
|
199
|
+
this.authenticated = false;
|
|
200
|
+
this.instance = null;
|
|
201
|
+
|
|
202
|
+
// Silently clear pending promises without rejecting
|
|
203
|
+
// (rejecting causes unhandled promise rejections during cleanup)
|
|
204
|
+
this.ps = {};
|
|
205
|
+
}
|
|
169
206
|
}
|
|
170
207
|
|
|
171
208
|
return new Sandbox();
|
package/agent/lib/sdk.js
CHANGED
|
@@ -194,6 +194,27 @@ const createSDK = (emitter, config, sessionInstance) => {
|
|
|
194
194
|
|
|
195
195
|
return value;
|
|
196
196
|
} catch (error) {
|
|
197
|
+
// Check if this is an API validation error with detailed problems
|
|
198
|
+
if (error.response?.data?.problems) {
|
|
199
|
+
const problems = error.response.data.problems;
|
|
200
|
+
const errorMessage = error.response.data.message || 'API validation error';
|
|
201
|
+
const detailedError = new Error(
|
|
202
|
+
`${errorMessage}\n\nDetails:\n${problems.map(p => ` - ${p}`).join('\n')}`
|
|
203
|
+
);
|
|
204
|
+
detailedError.originalError = error;
|
|
205
|
+
detailedError.problems = problems;
|
|
206
|
+
|
|
207
|
+
// Emit the formatted error
|
|
208
|
+
emitter.emit(events.error.sdk, {
|
|
209
|
+
message: detailedError.message,
|
|
210
|
+
code: error.response?.data?.code || error.code,
|
|
211
|
+
problems: problems,
|
|
212
|
+
fullError: error,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
throw detailedError;
|
|
216
|
+
}
|
|
217
|
+
|
|
197
218
|
outputError(error);
|
|
198
219
|
throw error; // Re-throw the error so calling code can handle it properly
|
|
199
220
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const semver = require("semver");
|
|
2
|
-
const
|
|
2
|
+
const packageJson = require("../../package.json");
|
|
3
3
|
|
|
4
4
|
// Function to check if the new version's minor version is >= current version's minor version
|
|
5
5
|
module.exports = (inputVersion) => {
|
|
6
|
-
const currentParsed = semver.parse(
|
|
6
|
+
const currentParsed = semver.parse(packageJson.version);
|
|
7
7
|
const inputParsed = semver.parse(inputVersion.replace("v", ""));
|
|
8
8
|
|
|
9
9
|
if (!currentParsed || !inputParsed) {
|
package/debugger/index.html
CHANGED
package/docs/docs.json
CHANGED
|
@@ -17,67 +17,7 @@
|
|
|
17
17
|
"tab": "Computer-Use SDK",
|
|
18
18
|
"versions": [
|
|
19
19
|
{
|
|
20
|
-
"version": "
|
|
21
|
-
"groups": [
|
|
22
|
-
{
|
|
23
|
-
"group": "Getting Started",
|
|
24
|
-
"pages": [
|
|
25
|
-
"/v7/getting-started/quickstart"
|
|
26
|
-
]
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"group": "API Reference",
|
|
30
|
-
"pages": [
|
|
31
|
-
{
|
|
32
|
-
"group": "Setup",
|
|
33
|
-
"icon": "gear",
|
|
34
|
-
"pages": [
|
|
35
|
-
"/v7/api/client",
|
|
36
|
-
"/v7/api/sandbox"
|
|
37
|
-
]
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"group": "Actions",
|
|
41
|
-
"icon": "wand-magic-sparkles",
|
|
42
|
-
"pages": [
|
|
43
|
-
"/v7/api/ai"
|
|
44
|
-
]
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"group": "Methods",
|
|
48
|
-
"icon": "code",
|
|
49
|
-
"pages": [
|
|
50
|
-
"/v7/api/find",
|
|
51
|
-
"/v7/api/click",
|
|
52
|
-
"/v7/api/type",
|
|
53
|
-
"/v7/api/pressKeys",
|
|
54
|
-
"/v7/api/scroll",
|
|
55
|
-
"/v7/api/hover",
|
|
56
|
-
"/v7/api/exec",
|
|
57
|
-
"/v7/api/focusApplication",
|
|
58
|
-
"/v7/api/assert"
|
|
59
|
-
]
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"group": "Reference",
|
|
63
|
-
"icon": "book",
|
|
64
|
-
"pages": [
|
|
65
|
-
"/v7/api/elements",
|
|
66
|
-
"/v7/api/assertions"
|
|
67
|
-
]
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"group": "Guides",
|
|
73
|
-
"pages": [
|
|
74
|
-
"/v7/guides/migration"
|
|
75
|
-
]
|
|
76
|
-
}
|
|
77
|
-
]
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
"version": "6.X.X (YAML)",
|
|
20
|
+
"version": "v6 (YAML)",
|
|
81
21
|
"groups": [
|
|
82
22
|
{
|
|
83
23
|
"group": "Overview",
|
|
@@ -249,18 +189,93 @@
|
|
|
249
189
|
]
|
|
250
190
|
}
|
|
251
191
|
]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"version": "v7 (JS Beta)",
|
|
195
|
+
"groups": [
|
|
196
|
+
{
|
|
197
|
+
"group": "Overview",
|
|
198
|
+
"icon": "circle-info",
|
|
199
|
+
"pages": [
|
|
200
|
+
"/v7/overview/what-is-testdriver",
|
|
201
|
+
{
|
|
202
|
+
"group": "Features",
|
|
203
|
+
"icon": "star",
|
|
204
|
+
"pages": [
|
|
205
|
+
"/v7/features/easy-to-write",
|
|
206
|
+
"/v7/features/fast",
|
|
207
|
+
"/v7/features/stable",
|
|
208
|
+
"/v7/features/scalable",
|
|
209
|
+
"/v7/features/ai-native",
|
|
210
|
+
"/v7/features/powerful",
|
|
211
|
+
"/v7/features/observable",
|
|
212
|
+
"/v7/features/enterprise"
|
|
213
|
+
]
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"group": "Getting Started",
|
|
219
|
+
"icon": "rocket",
|
|
220
|
+
"pages": [
|
|
221
|
+
"/v7/getting-started/installation",
|
|
222
|
+
"/v7/getting-started/writing-tests",
|
|
223
|
+
"/v7/getting-started/generating-tests",
|
|
224
|
+
"/v7/getting-started/running-and-debugging",
|
|
225
|
+
"/v7/getting-started/setting-up-in-ci"
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"group": "Examples",
|
|
230
|
+
"icon": "code",
|
|
231
|
+
"pages": [
|
|
232
|
+
"/v7/presets/chrome",
|
|
233
|
+
"/v7/presets/chrome-extension",
|
|
234
|
+
"/v7/presets/vscode",
|
|
235
|
+
"/v7/presets/electron",
|
|
236
|
+
"/v7/presets/webapp"
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"group": "API Reference",
|
|
241
|
+
"icon": "book",
|
|
242
|
+
"pages": [
|
|
243
|
+
"/v7/api/client",
|
|
244
|
+
"/v7/api/sandbox",
|
|
245
|
+
{
|
|
246
|
+
"group": "Interactions",
|
|
247
|
+
"icon": "bolt",
|
|
248
|
+
"pages": [
|
|
249
|
+
"/v7/api/find",
|
|
250
|
+
"/v7/api/elements",
|
|
251
|
+
"/v7/api/click",
|
|
252
|
+
"/v7/api/doubleClick",
|
|
253
|
+
"/v7/api/rightClick",
|
|
254
|
+
"/v7/api/hover",
|
|
255
|
+
"/v7/api/mouseDown",
|
|
256
|
+
"/v7/api/mouseUp",
|
|
257
|
+
"/v7/api/type",
|
|
258
|
+
"/v7/api/pressKeys",
|
|
259
|
+
"/v7/api/scroll",
|
|
260
|
+
"/v7/api/focusApplication"
|
|
261
|
+
]
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"group": "AI & Assertions",
|
|
265
|
+
"icon": "wand-magic-sparkles",
|
|
266
|
+
"pages": [
|
|
267
|
+
"/v7/api/act",
|
|
268
|
+
"/v7/api/assert",
|
|
269
|
+
"/v7/api/assertions"
|
|
270
|
+
]
|
|
271
|
+
},
|
|
272
|
+
"/v7/api/exec",
|
|
273
|
+
"/v7/api/dashcam"
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
]
|
|
252
277
|
}
|
|
253
278
|
]
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
"tab": "Playwright SDK",
|
|
257
|
-
"pages": ["v7/playwright"]
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
"tab": "Playwright Studio"
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
"tab": "Dashcam CLI"
|
|
264
279
|
}
|
|
265
280
|
]
|
|
266
281
|
},
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Best Practices: Element Polling
|
|
2
|
+
|
|
3
|
+
**⚠️ CRITICAL: Never use `wait()` for waiting for elements to appear.**
|
|
4
|
+
|
|
5
|
+
## Why Avoid `wait()`?
|
|
6
|
+
|
|
7
|
+
Arbitrary waits with `wait()` have several problems:
|
|
8
|
+
|
|
9
|
+
1. **Brittle**: Fixed timeouts may be too short (causing flaky tests) or too long (wasting time)
|
|
10
|
+
2. **Slow**: You always wait the full duration, even if the element appears sooner
|
|
11
|
+
3. **Unreliable**: Network conditions, system load, and other factors affect timing
|
|
12
|
+
4. **Hard to debug**: When tests fail, you don't know if it was a timing issue or actual failure
|
|
13
|
+
|
|
14
|
+
## The Right Way: Element Polling with `find()`
|
|
15
|
+
|
|
16
|
+
TestDriver's `find()` method is designed for element detection. Use it in a polling loop to wait for elements:
|
|
17
|
+
|
|
18
|
+
### Basic Polling Pattern
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
// ❌ WRONG: Using wait()
|
|
22
|
+
await testdriver.wait(2000);
|
|
23
|
+
const button = await testdriver.find("Submit button");
|
|
24
|
+
|
|
25
|
+
// ✅ CORRECT: Polling with find()
|
|
26
|
+
let button;
|
|
27
|
+
for (let i = 0; i < 10; i++) {
|
|
28
|
+
try {
|
|
29
|
+
button = await testdriver.find("Submit button");
|
|
30
|
+
if (button.found()) break;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
if (i === 9) throw e; // Re-throw on last attempt
|
|
33
|
+
}
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Helper Function for Polling
|
|
39
|
+
|
|
40
|
+
Create a reusable helper function:
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
async function waitForElement(testdriver, description, maxAttempts = 10, delayMs = 1000) {
|
|
44
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
45
|
+
try {
|
|
46
|
+
const element = await testdriver.find(description);
|
|
47
|
+
if (element.found()) {
|
|
48
|
+
return element;
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {
|
|
51
|
+
if (i === maxAttempts - 1) throw e;
|
|
52
|
+
}
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Element not found after ${maxAttempts} attempts: ${description}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Usage
|
|
59
|
+
const emailField = await waitForElement(testdriver, "Email input field");
|
|
60
|
+
await emailField.click();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## When to Use Polling
|
|
64
|
+
|
|
65
|
+
Use element polling in these scenarios:
|
|
66
|
+
|
|
67
|
+
- **After navigation**: Waiting for a new page to load
|
|
68
|
+
- **After user action**: Waiting for UI updates (form submission, modal opening, etc.)
|
|
69
|
+
- **Dynamic content**: Waiting for AJAX-loaded elements
|
|
70
|
+
- **State transitions**: Waiting for loading spinners to disappear or success messages to appear
|
|
71
|
+
|
|
72
|
+
## Example: Complete Login Flow
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
import { chrome } from "testdriverai/presets";
|
|
76
|
+
|
|
77
|
+
// Helper function
|
|
78
|
+
async function waitForElement(testdriver, description, maxAttempts = 10, delayMs = 1000) {
|
|
79
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
80
|
+
try {
|
|
81
|
+
const element = await testdriver.find(description);
|
|
82
|
+
if (element.found()) return element;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
if (i === maxAttempts - 1) throw e;
|
|
85
|
+
}
|
|
86
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Element not found after ${maxAttempts} attempts: ${description}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
it("should log in successfully", async (context) => {
|
|
92
|
+
const { testdriver } = await chrome(context, {
|
|
93
|
+
url: 'https://example.com/login',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Wait for login page to load
|
|
97
|
+
const emailField = await waitForElement(testdriver, "Email input field");
|
|
98
|
+
await emailField.click();
|
|
99
|
+
await testdriver.type("user@example.com");
|
|
100
|
+
|
|
101
|
+
const passwordField = await testdriver.find("Password input field");
|
|
102
|
+
await passwordField.click();
|
|
103
|
+
await testdriver.type("password123");
|
|
104
|
+
|
|
105
|
+
const loginButton = await testdriver.find("Login button");
|
|
106
|
+
await loginButton.click();
|
|
107
|
+
|
|
108
|
+
// Wait for dashboard to load after login
|
|
109
|
+
await waitForElement(testdriver, "Dashboard welcome message");
|
|
110
|
+
|
|
111
|
+
// Verify login successful
|
|
112
|
+
const isLoggedIn = await testdriver.assert("user is logged in to dashboard");
|
|
113
|
+
expect(isLoggedIn).toBeTruthy();
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Advanced: Conditional Polling
|
|
118
|
+
|
|
119
|
+
For elements that may or may not appear (like dialogs or notifications):
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
// Try to find and dismiss optional dialog
|
|
123
|
+
try {
|
|
124
|
+
const dialog = await waitForElement(testdriver, "Cookie consent dialog", 3, 500);
|
|
125
|
+
const acceptButton = await testdriver.find("Accept button");
|
|
126
|
+
await acceptButton.click();
|
|
127
|
+
console.log("Dismissed cookie dialog");
|
|
128
|
+
} catch {
|
|
129
|
+
console.log("No cookie dialog found, continuing...");
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
Adjust polling parameters based on your needs:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
// Quick polling for fast UI updates (check every 300ms, up to 3 seconds)
|
|
139
|
+
await waitForElement(testdriver, "Success message", 10, 300);
|
|
140
|
+
|
|
141
|
+
// Patient polling for slow operations (check every 2s, up to 20 seconds)
|
|
142
|
+
await waitForElement(testdriver, "Processing complete indicator", 10, 2000);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Summary
|
|
146
|
+
|
|
147
|
+
| Pattern | Use Case |
|
|
148
|
+
|---------|----------|
|
|
149
|
+
| **Polling with `find()`** | ✅ Waiting for UI elements to appear or disappear |
|
|
150
|
+
| **`wait()`** | ❌ NEVER use for element waiting |
|
|
151
|
+
| **Helper function** | ✅ Recommended for cleaner, reusable code |
|
|
152
|
+
| **Conditional polling** | ✅ For optional elements (dialogs, notifications) |
|
|
153
|
+
|
|
154
|
+
Remember: **If you're waiting for something to appear on screen, use `find()` in a polling loop, not `wait()`.**
|