@testdriverai/agent 7.8.0-test.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +7 -0
- package/.env.example +4 -0
- package/.prettierignore +4 -0
- package/.prettierrc +1 -0
- package/CHANGELOG.md +953 -0
- package/README.md +81 -0
- package/agent/events.js +135 -0
- package/agent/index.js +2450 -0
- package/agent/interface.js +35 -0
- package/agent/lib/analytics.js +22 -0
- package/agent/lib/censorship.js +75 -0
- package/agent/lib/commander.js +246 -0
- package/agent/lib/commands.js +1684 -0
- package/agent/lib/config.js +60 -0
- package/agent/lib/generator.js +91 -0
- package/agent/lib/http.js +144 -0
- package/agent/lib/logger.js +56 -0
- package/agent/lib/outputs.js +29 -0
- package/agent/lib/parser.js +209 -0
- package/agent/lib/redraw.js +386 -0
- package/agent/lib/resources/cursor-2.png +0 -0
- package/agent/lib/sandbox.js +1104 -0
- package/agent/lib/sdk.js +633 -0
- package/agent/lib/session.js +25 -0
- package/agent/lib/source-mapper.js +342 -0
- package/agent/lib/subimage/index.js +77 -0
- package/agent/lib/subimage/opencv.js +69 -0
- package/agent/lib/system.js +204 -0
- package/agent/lib/theme.js +14 -0
- package/agent/lib/valid-version.js +21 -0
- package/agent/lib/validation.js +169 -0
- package/ai/.claude-plugin/plugin.json +9 -0
- package/ai/agents/testdriver.md +638 -0
- package/ai/skills/testdriver-ai/SKILL.md +204 -0
- package/ai/skills/testdriver-assert/SKILL.md +315 -0
- package/ai/skills/testdriver-aws-setup/SKILL.md +448 -0
- package/ai/skills/testdriver-cache/SKILL.md +221 -0
- package/ai/skills/testdriver-caching/SKILL.md +124 -0
- package/ai/skills/testdriver-captcha/SKILL.md +158 -0
- package/ai/skills/testdriver-ci-cd/SKILL.md +602 -0
- package/ai/skills/testdriver-click/SKILL.md +286 -0
- package/ai/skills/testdriver-client/SKILL.md +477 -0
- package/ai/skills/testdriver-cloud/SKILL.md +119 -0
- package/ai/skills/testdriver-customizing-devices/SKILL.md +319 -0
- package/ai/skills/testdriver-dashcam/SKILL.md +418 -0
- package/ai/skills/testdriver-debugging-with-screenshots/SKILL.md +401 -0
- package/ai/skills/testdriver-device-config/SKILL.md +317 -0
- package/ai/skills/testdriver-double-click/SKILL.md +102 -0
- package/ai/skills/testdriver-elements/SKILL.md +605 -0
- package/ai/skills/testdriver-enterprise/SKILL.md +114 -0
- package/ai/skills/testdriver-errors/SKILL.md +246 -0
- package/ai/skills/testdriver-events/SKILL.md +356 -0
- package/ai/skills/testdriver-examples/SKILL.md +7 -0
- package/ai/skills/testdriver-exec/SKILL.md +317 -0
- package/ai/skills/testdriver-find/SKILL.md +829 -0
- package/ai/skills/testdriver-focus-application/SKILL.md +293 -0
- package/ai/skills/testdriver-generating-tests/SKILL.md +36 -0
- package/ai/skills/testdriver-hover/SKILL.md +278 -0
- package/ai/skills/testdriver-locating-elements/SKILL.md +71 -0
- package/ai/skills/testdriver-making-assertions/SKILL.md +32 -0
- package/ai/skills/testdriver-mcp/SKILL.md +7 -0
- package/ai/skills/testdriver-mcp-workflow/SKILL.md +410 -0
- package/ai/skills/testdriver-mouse-down/SKILL.md +161 -0
- package/ai/skills/testdriver-mouse-up/SKILL.md +164 -0
- package/ai/skills/testdriver-parse/SKILL.md +236 -0
- package/ai/skills/testdriver-performing-actions/SKILL.md +54 -0
- package/ai/skills/testdriver-press-keys/SKILL.md +348 -0
- package/ai/skills/testdriver-provision/SKILL.md +331 -0
- package/ai/skills/testdriver-quickstart/SKILL.md +144 -0
- package/ai/skills/testdriver-redraw/SKILL.md +214 -0
- package/ai/skills/testdriver-reusable-code/SKILL.md +249 -0
- package/ai/skills/testdriver-right-click/SKILL.md +123 -0
- package/ai/skills/testdriver-running-tests/SKILL.md +185 -0
- package/ai/skills/testdriver-screenshot/SKILL.md +248 -0
- package/ai/skills/testdriver-screenshots/SKILL.md +184 -0
- package/ai/skills/testdriver-scroll/SKILL.md +335 -0
- package/ai/skills/testdriver-secrets/SKILL.md +115 -0
- package/ai/skills/testdriver-self-hosted/SKILL.md +65 -0
- package/ai/skills/testdriver-test-writer/SKILL.md +448 -0
- package/ai/skills/testdriver-testdriver/SKILL.md +628 -0
- package/ai/skills/testdriver-testdriver-mechanic/SKILL.md +165 -0
- package/ai/skills/testdriver-type/SKILL.md +357 -0
- package/ai/skills/testdriver-variables/SKILL.md +111 -0
- package/ai/skills/testdriver-wait/SKILL.md +50 -0
- package/ai/skills/testdriver-waiting-for-elements/SKILL.md +90 -0
- package/ai/skills/testdriver-what-is-testdriver/SKILL.md +54 -0
- package/bin/testdriverai.js +22 -0
- package/debugger/bg.png +0 -0
- package/debugger/icon.png +0 -0
- package/debugger/index.html +469 -0
- package/debugger/td.png +0 -0
- package/debugger/tray-buffered.png +0 -0
- package/debugger/tray.png +0 -0
- package/docs/GITHUB_COMMENTS.md +330 -0
- package/docs/GITHUB_COMMENTS_ANNOUNCEMENT.md +167 -0
- package/docs/QUICK-START-GITHUB-COMMENTS.md +84 -0
- package/docs/TEST-GITHUB-COMMENTS.md +129 -0
- package/docs/_data/examples-manifest.json +177 -0
- package/docs/_data/examples-manifest.schema.json +41 -0
- package/docs/_scripts/extract-example-urls.js +165 -0
- package/docs/_scripts/generate-examples.js +560 -0
- package/docs/_scripts/generate-skills.js +154 -0
- package/docs/_scripts/link-replacer.js +164 -0
- package/docs/_scripts/upload-docs-to-openai.js +284 -0
- package/docs/changelog.mdx +161 -0
- package/docs/claude-mcp-plugin.mdx +160 -0
- package/docs/docs.json +442 -0
- package/docs/github-integration-setup.md +266 -0
- package/docs/guide/best-practices-polling.mdx +174 -0
- package/docs/images/content/account/newprojectsettings.png +0 -0
- package/docs/images/content/account/projectpage.png +0 -0
- package/docs/images/content/account/projectreplays.png +0 -0
- package/docs/images/content/account/team-manage.png +0 -0
- package/docs/images/content/account/teampage.png +0 -0
- package/docs/images/content/extension/cursor.svg +1 -0
- package/docs/images/content/extension/vscode.svg +57 -0
- package/docs/images/content/extension/windsurf.svg +3 -0
- package/docs/images/content/parse/output.png +0 -0
- package/docs/images/content/self-hosted/launchtemplateid.png +0 -0
- package/docs/images/content/side-by-side.png +0 -0
- package/docs/images/content/vscode/ide-full.png +0 -0
- package/docs/images/content/vscode/running.png +0 -0
- package/docs/images/content/vscode/v7-chat.png +0 -0
- package/docs/images/content/vscode/v7-choose-agent.png +0 -0
- package/docs/images/content/vscode/v7-full.png +0 -0
- package/docs/images/content/vscode/v7-onboarding.png +0 -0
- package/docs/images/content/vscode/vscode-2-assert.png +0 -0
- package/docs/images/content/vscode/vscode-agent-preview.png +0 -0
- package/docs/images/content/vscode/vscode-copilot-ask.png +0 -0
- package/docs/images/content/vscode/vscode-file-creation.png +0 -0
- package/docs/images/content/vscode/vscode-install.png +0 -0
- package/docs/images/content/vscode/vscode-overview.png +0 -0
- package/docs/images/content/vscode/vscode-setup-walkthrough.png +0 -0
- package/docs/images/content/vscode/vscode-stopchat.png +0 -0
- package/docs/images/content/vscode/vscode-stoptest.png +0 -0
- package/docs/images/content/vscode/vscode-tdservice.png +0 -0
- package/docs/images/content/vscode/vscode-test-output.png +0 -0
- package/docs/images/content/vscode/vscode-testhistory.png +0 -0
- package/docs/images/content/vscode/vscode-testpane-runtests.png +0 -0
- package/docs/images/content/vscode/vscode-testpane.png +0 -0
- package/docs/images/template/dark.png +0 -0
- package/docs/images/template/icon.png +0 -0
- package/docs/images/template/light.png +0 -0
- package/docs/snippets/calendar-link.mdx +4 -0
- package/docs/snippets/gitignore-warning.mdx +7 -0
- package/docs/snippets/lifecycle-warning.mdx +6 -0
- package/docs/snippets/test-prereqs.mdx +12 -0
- package/docs/snippets/tests/assert-replay.mdx +7 -0
- package/docs/snippets/tests/assert-yaml.mdx +8 -0
- package/docs/snippets/tests/exec-js-replay.mdx +7 -0
- package/docs/snippets/tests/exec-js-yaml.mdx +32 -0
- package/docs/snippets/tests/exec-shell-replay.mdx +7 -0
- package/docs/snippets/tests/exec-shell-yaml.mdx +15 -0
- package/docs/snippets/tests/hover-image-replay.mdx +7 -0
- package/docs/snippets/tests/hover-image-yaml.mdx +17 -0
- package/docs/snippets/tests/hover-text-replay.mdx +7 -0
- package/docs/snippets/tests/hover-text-with-description-replay.mdx +7 -0
- package/docs/snippets/tests/hover-text-with-description-yaml.mdx +24 -0
- package/docs/snippets/tests/hover-text-yaml.mdx +14 -0
- package/docs/snippets/tests/match-image-replay.mdx +7 -0
- package/docs/snippets/tests/match-image-yaml.mdx +17 -0
- package/docs/snippets/tests/press-keys-replay.mdx +7 -0
- package/docs/snippets/tests/press-keys-yaml.mdx +36 -0
- package/docs/snippets/tests/remember-replay.mdx +7 -0
- package/docs/snippets/tests/remember-yaml.mdx +28 -0
- package/docs/snippets/tests/scroll-replay.mdx +7 -0
- package/docs/snippets/tests/scroll-until-image-replay.mdx +7 -0
- package/docs/snippets/tests/scroll-until-image-yaml.mdx +14 -0
- package/docs/snippets/tests/scroll-until-text-replay.mdx +7 -0
- package/docs/snippets/tests/scroll-until-text-yaml.mdx +17 -0
- package/docs/snippets/tests/scroll-yaml.mdx +30 -0
- package/docs/snippets/tests/type-repeated-replay.mdx +7 -0
- package/docs/snippets/tests/type-repeated-yaml.mdx +22 -0
- package/docs/snippets/tests/type-replay.mdx +7 -0
- package/docs/snippets/tests/type-yaml.mdx +28 -0
- package/docs/snippets/tests/wait-for-image-replay.mdx +7 -0
- package/docs/snippets/tests/wait-for-image-yaml.mdx +18 -0
- package/docs/snippets/tests/wait-for-text-replay.mdx +7 -0
- package/docs/snippets/tests/wait-for-text-yaml.mdx +18 -0
- package/docs/snippets/tests/wait-replay.mdx +7 -0
- package/docs/snippets/tests/wait-yaml.mdx +13 -0
- package/docs/styles.css +65 -0
- package/docs/v6/account/dashboard.mdx +16 -0
- package/docs/v6/account/enterprise.mdx +110 -0
- package/docs/v6/account/pricing.mdx +33 -0
- package/docs/v6/account/projects.mdx +33 -0
- package/docs/v6/account/team.mdx +35 -0
- package/docs/v6/action/ami.mdx +109 -0
- package/docs/v6/action/performance.mdx +105 -0
- package/docs/v6/action/secrets.mdx +93 -0
- package/docs/v6/apps/chrome-extensions.mdx +48 -0
- package/docs/v6/apps/desktop-apps.mdx +93 -0
- package/docs/v6/apps/mobile-apps.mdx +26 -0
- package/docs/v6/apps/static-websites.mdx +54 -0
- package/docs/v6/apps/tauri-apps.mdx +361 -0
- package/docs/v6/bugs/jira.mdx +232 -0
- package/docs/v6/cli/overview.mdx +66 -0
- package/docs/v6/commands/assert.mdx +45 -0
- package/docs/v6/commands/exec.mdx +276 -0
- package/docs/v6/commands/focus-application.mdx +44 -0
- package/docs/v6/commands/hover-image.mdx +69 -0
- package/docs/v6/commands/hover-text.mdx +47 -0
- package/docs/v6/commands/if.mdx +53 -0
- package/docs/v6/commands/match-image.mdx +67 -0
- package/docs/v6/commands/press-keys.mdx +87 -0
- package/docs/v6/commands/remember.mdx +49 -0
- package/docs/v6/commands/run.mdx +44 -0
- package/docs/v6/commands/scroll-until-image.mdx +66 -0
- package/docs/v6/commands/scroll-until-text.mdx +60 -0
- package/docs/v6/commands/scroll.mdx +69 -0
- package/docs/v6/commands/type.mdx +45 -0
- package/docs/v6/commands/wait-for-image.mdx +54 -0
- package/docs/v6/commands/wait-for-text.mdx +48 -0
- package/docs/v6/commands/wait.mdx +45 -0
- package/docs/v6/exporting/junit.mdx +218 -0
- package/docs/v6/exporting/playwright.mdx +197 -0
- package/docs/v6/features/auto-healing.mdx +144 -0
- package/docs/v6/features/generation.mdx +116 -0
- package/docs/v6/features/parallel-testing.mdx +151 -0
- package/docs/v6/features/reusable-snippets.mdx +131 -0
- package/docs/v6/features/selectorless.mdx +80 -0
- package/docs/v6/features/visual-assertions.mdx +139 -0
- package/docs/v6/getting-started/ci.mdx +146 -0
- package/docs/v6/getting-started/cli.mdx +91 -0
- package/docs/v6/getting-started/editing.mdx +100 -0
- package/docs/v6/getting-started/playwright.mdx +342 -0
- package/docs/v6/getting-started/running.mdx +48 -0
- package/docs/v6/getting-started/self-hosting.mdx +408 -0
- package/docs/v6/getting-started/vscode.mdx +88 -0
- package/docs/v6/guide/assertions.mdx +189 -0
- package/docs/v6/guide/authentication.mdx +136 -0
- package/docs/v6/guide/code.mdx +65 -0
- package/docs/v6/guide/dashcam.mdx +118 -0
- package/docs/v6/guide/environment-variables.mdx +26 -0
- package/docs/v6/guide/lifecycle.mdx +242 -0
- package/docs/v6/guide/locating.mdx +141 -0
- package/docs/v6/guide/protips.mdx +43 -0
- package/docs/v6/guide/variables.mdx +143 -0
- package/docs/v6/guide/waiting.mdx +130 -0
- package/docs/v6/importing/csv.mdx +196 -0
- package/docs/v6/importing/gherkin.mdx +143 -0
- package/docs/v6/importing/jira.mdx +164 -0
- package/docs/v6/importing/testrail.mdx +162 -0
- package/docs/v6/integrations/electron.mdx +146 -0
- package/docs/v6/integrations/netlify.mdx +100 -0
- package/docs/v6/integrations/vercel.mdx +125 -0
- package/docs/v6/interactive/explore.mdx +99 -0
- package/docs/v6/interactive/run.mdx +52 -0
- package/docs/v6/interactive/save.mdx +63 -0
- package/docs/v6/overview/comparison.mdx +101 -0
- package/docs/v6/overview/faq.mdx +162 -0
- package/docs/v6/overview/performance.mdx +52 -0
- package/docs/v6/overview/quickstart.mdx +137 -0
- package/docs/v6/overview/what-is-testdriver.mdx +85 -0
- package/docs/v6/scenarios/ai-chatbot.mdx +28 -0
- package/docs/v6/scenarios/cookie-banner.mdx +32 -0
- package/docs/v6/scenarios/file-upload.mdx +33 -0
- package/docs/v6/scenarios/form-filling.mdx +32 -0
- package/docs/v6/scenarios/log-in.mdx +75 -0
- package/docs/v6/scenarios/pdf-generation.mdx +25 -0
- package/docs/v6/scenarios/spell-check.mdx +22 -0
- package/docs/v6/security/action.mdx +84 -0
- package/docs/v6/security/agent.mdx +73 -0
- package/docs/v6/security/platform.mdx +77 -0
- package/docs/v6/tutorials/advanced-test.mdx +81 -0
- package/docs/v6/tutorials/basic-test.mdx +45 -0
- package/docs/v7/_drafts/agents.mdx +843 -0
- package/docs/v7/_drafts/architecture.mdx +399 -0
- package/docs/v7/_drafts/auto-cache-key.mdx +167 -0
- package/docs/v7/_drafts/awesome-logs-quick-ref.mdx +100 -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 +424 -0
- package/docs/v7/_drafts/caching.mdx +366 -0
- package/docs/v7/_drafts/cli-to-sdk-migration.mdx +425 -0
- package/docs/v7/_drafts/commands/assert.mdx +45 -0
- package/docs/v7/_drafts/commands/exec.mdx +276 -0
- package/docs/v7/_drafts/commands/focus-application.mdx +44 -0
- package/docs/v7/_drafts/commands/hover-image.mdx +69 -0
- package/docs/v7/_drafts/commands/hover-text.mdx +47 -0
- package/docs/v7/_drafts/commands/if.mdx +53 -0
- package/docs/v7/_drafts/commands/match-image.mdx +67 -0
- package/docs/v7/_drafts/commands/press-keys.mdx +87 -0
- package/docs/v7/_drafts/commands/remember.mdx +49 -0
- package/docs/v7/_drafts/commands/run.mdx +44 -0
- package/docs/v7/_drafts/commands/scroll-until-image.mdx +66 -0
- package/docs/v7/_drafts/commands/scroll-until-text.mdx +60 -0
- package/docs/v7/_drafts/commands/scroll.mdx +69 -0
- package/docs/v7/_drafts/commands/type.mdx +45 -0
- package/docs/v7/_drafts/commands/wait-for-image.mdx +54 -0
- package/docs/v7/_drafts/commands/wait-for-text.mdx +48 -0
- package/docs/v7/_drafts/commands/wait.mdx +45 -0
- package/docs/v7/_drafts/configuration.mdx +378 -0
- package/docs/v7/_drafts/contributing.mdx +174 -0
- package/docs/v7/_drafts/core.mdx +458 -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/init-command.mdx +95 -0
- package/docs/v7/_drafts/installation.mdx +420 -0
- package/docs/v7/_drafts/migration.mdx +562 -0
- package/docs/v7/_drafts/observable.mdx +604 -0
- package/docs/v7/_drafts/playwright.mdx +342 -0
- package/docs/v7/_drafts/plugin-migration.mdx +220 -0
- package/docs/v7/_drafts/powerful.mdx +419 -0
- package/docs/v7/_drafts/presets.mdx +210 -0
- package/docs/v7/_drafts/progressive-disclosure.mdx +230 -0
- package/docs/v7/_drafts/prompt-cache.mdx +200 -0
- package/docs/v7/_drafts/provision.mdx +390 -0
- package/docs/v7/_drafts/quick-start-test-recording.mdx +214 -0
- package/docs/v7/_drafts/readme.mdx +135 -0
- package/docs/v7/_drafts/reports.mdx +414 -0
- package/docs/v7/_drafts/scalable.mdx +763 -0
- package/docs/v7/_drafts/screenshot.mdx +155 -0
- package/docs/v7/_drafts/sdk-awesome-logs.mdx +468 -0
- package/docs/v7/_drafts/sdk-browser-rendering.mdx +167 -0
- package/docs/v7/_drafts/sdk-migration.mdx +474 -0
- package/docs/v7/_drafts/sdk-v7-complete.mdx +345 -0
- package/docs/v7/_drafts/self-hosting.mdx +369 -0
- package/docs/v7/_drafts/test-recording.mdx +382 -0
- 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/_drafts/writing-tests.mdx +25 -0
- package/docs/v7/ai.mdx +205 -0
- package/docs/v7/assert.mdx +316 -0
- package/docs/v7/aws-setup.mdx +449 -0
- package/docs/v7/cache.mdx +223 -0
- package/docs/v7/caching.mdx +128 -0
- package/docs/v7/captcha.mdx +159 -0
- package/docs/v7/ci-cd.mdx +603 -0
- package/docs/v7/click.mdx +287 -0
- package/docs/v7/client.mdx +478 -0
- package/docs/v7/copilot/auto-healing.mdx +265 -0
- package/docs/v7/copilot/creating-tests.mdx +156 -0
- package/docs/v7/copilot/github.mdx +143 -0
- package/docs/v7/copilot/running-tests.mdx +149 -0
- package/docs/v7/copilot/setup.mdx +143 -0
- package/docs/v7/customizing-devices.mdx +319 -0
- package/docs/v7/dashcam.mdx +419 -0
- package/docs/v7/debugging-with-screenshots.mdx +402 -0
- package/docs/v7/device-config.mdx +317 -0
- package/docs/v7/double-click.mdx +102 -0
- package/docs/v7/elements.mdx +606 -0
- package/docs/v7/enterprise.mdx +9 -0
- package/docs/v7/errors.mdx +248 -0
- package/docs/v7/events.mdx +358 -0
- package/docs/v7/examples/ai.mdx +72 -0
- package/docs/v7/examples/assert.mdx +72 -0
- package/docs/v7/examples/captcha-api.mdx +92 -0
- package/docs/v7/examples/chrome-extension.mdx +132 -0
- package/docs/v7/examples/drag-and-drop.mdx +100 -0
- package/docs/v7/examples/element-not-found.mdx +67 -0
- package/docs/v7/examples/exec-output.mdx +85 -0
- package/docs/v7/examples/exec-pwsh.mdx +83 -0
- package/docs/v7/examples/focus-window.mdx +62 -0
- package/docs/v7/examples/hover-image.mdx +94 -0
- package/docs/v7/examples/hover-text.mdx +69 -0
- package/docs/v7/examples/installer.mdx +91 -0
- package/docs/v7/examples/launch-vscode-linux.mdx +101 -0
- package/docs/v7/examples/match-image.mdx +96 -0
- package/docs/v7/examples/press-keys.mdx +92 -0
- package/docs/v7/examples/scroll-keyboard.mdx +79 -0
- package/docs/v7/examples/scroll-until-image.mdx +81 -0
- package/docs/v7/examples/scroll-until-text.mdx +109 -0
- package/docs/v7/examples/scroll.mdx +81 -0
- package/docs/v7/examples/type.mdx +92 -0
- package/docs/v7/examples/windows-installer.mdx +89 -0
- package/docs/v7/exec.mdx +318 -0
- package/docs/v7/find.mdx +830 -0
- package/docs/v7/focus-application.mdx +294 -0
- package/docs/v7/generating-tests.mdx +36 -0
- package/docs/v7/hosted.mdx +158 -0
- package/docs/v7/hover.mdx +279 -0
- package/docs/v7/locating-elements.mdx +71 -0
- package/docs/v7/making-assertions.mdx +32 -0
- package/docs/v7/mcp.mdx +9 -0
- package/docs/v7/mouse-down.mdx +161 -0
- package/docs/v7/mouse-up.mdx +164 -0
- package/docs/v7/parse.mdx +237 -0
- package/docs/v7/performing-actions.mdx +54 -0
- package/docs/v7/press-keys.mdx +349 -0
- package/docs/v7/provision.mdx +333 -0
- package/docs/v7/quickstart.mdx +173 -0
- package/docs/v7/redraw.mdx +216 -0
- package/docs/v7/reusable-code.mdx +249 -0
- package/docs/v7/right-click.mdx +123 -0
- package/docs/v7/running-tests.mdx +185 -0
- package/docs/v7/screenshot.mdx +249 -0
- package/docs/v7/screenshots.mdx +186 -0
- package/docs/v7/scroll.mdx +336 -0
- package/docs/v7/secrets.mdx +115 -0
- package/docs/v7/self-hosted.mdx +149 -0
- package/docs/v7/type.mdx +358 -0
- package/docs/v7/variables.mdx +111 -0
- package/docs/v7/wait.mdx +52 -0
- package/docs/v7/waiting-for-elements.mdx +90 -0
- package/docs/v7/what-is-testdriver.mdx +54 -0
- package/eslint.config.js +67 -0
- package/examples/ai.test.mjs +31 -0
- package/examples/assert.test.mjs +47 -0
- package/examples/chrome-extension.test.mjs +97 -0
- package/examples/config.mjs +5 -0
- package/examples/element-not-found.test.mjs +27 -0
- package/examples/exec-output.test.mjs +60 -0
- package/examples/exec-pwsh.test.mjs +58 -0
- package/examples/findall-coffee-icons.test.mjs +42 -0
- package/examples/focus-window.test.mjs +37 -0
- package/examples/formatted-logging.test.mjs +27 -0
- package/examples/hover-image.test.mjs +53 -0
- package/examples/hover-text-with-description.test.mjs +57 -0
- package/examples/hover-text.test.mjs +28 -0
- package/examples/installer.test.mjs +50 -0
- package/examples/launch-vscode-linux.test.mjs +55 -0
- package/examples/match-image.test.mjs +55 -0
- package/examples/parse.test.mjs +19 -0
- package/examples/press-keys.test.mjs +44 -0
- package/examples/prompt.test.mjs +34 -0
- package/examples/scroll-keyboard.test.mjs +38 -0
- package/examples/scroll-until-image.test.mjs +40 -0
- package/examples/scroll.test.mjs +42 -0
- package/examples/type.test.mjs +46 -0
- package/examples/windows-installer.test.mjs +54 -0
- package/index.js +2 -0
- package/interfaces/cli/commands/init.js +438 -0
- package/interfaces/cli/commands/setup.js +382 -0
- package/interfaces/cli/lib/base.js +285 -0
- package/interfaces/cli.js +20 -0
- package/interfaces/junit-reporter.js +290 -0
- package/interfaces/logger.js +388 -0
- package/interfaces/readline.js +234 -0
- package/interfaces/shared-test-state.mjs +64 -0
- package/interfaces/vitest-plugin.d.ts +115 -0
- package/interfaces/vitest-plugin.mjs +1698 -0
- package/lib/captcha/solver.js +358 -0
- package/lib/core/Dashcam.js +533 -0
- package/lib/core/index.d.ts +172 -0
- package/lib/core/index.js +12 -0
- package/lib/environments.json +18 -0
- package/lib/github-comment-formatter.js +263 -0
- package/lib/github-comment.mjs +452 -0
- package/lib/init-project.js +575 -0
- package/lib/presets/index.mjs +331 -0
- package/lib/resolve-channel.js +46 -0
- package/lib/sentry.js +417 -0
- package/lib/vitest/hooks.d.ts +57 -0
- package/lib/vitest/hooks.mjs +674 -0
- package/lib/vitest/setup-aws.mjs +247 -0
- package/lib/vitest/setup-self-hosted.mjs +151 -0
- package/lib/vitest/setup.mjs +46 -0
- package/manual/captcha-api.test.mjs +51 -0
- package/manual/drag-and-drop.test.mjs +59 -0
- package/manual/flake-diffthreshold-001.test.mjs +9 -0
- package/manual/flake-diffthreshold-01.test.mjs +9 -0
- package/manual/flake-diffthreshold-05.test.mjs +9 -0
- package/manual/flake-noredraw-cache.test.mjs +9 -0
- package/manual/flake-noredraw-nocache.test.mjs +9 -0
- package/manual/flake-redraw-cache.test.mjs +9 -0
- package/manual/flake-redraw-nocache.test.mjs +9 -0
- package/manual/flake-rocket-match.test.mjs +30 -0
- package/manual/flake-shared.mjs +51 -0
- package/manual/no-provision.test.mjs +31 -0
- package/manual/packer-hover-image.test.mjs +176 -0
- package/manual/scroll-until-text.test.mjs +68 -0
- package/manual/test-init-command.js +223 -0
- package/mcp-server/README.md +322 -0
- package/mcp-server/dist/codegen.d.ts +9 -0
- package/mcp-server/dist/codegen.js +165 -0
- package/mcp-server/dist/mcp-app.html +114 -0
- package/mcp-server/dist/package.json +1 -0
- package/mcp-server/dist/provision-types.d.ts +290 -0
- package/mcp-server/dist/provision-types.js +174 -0
- package/mcp-server/dist/server.d.ts +6 -0
- package/mcp-server/dist/server.mjs +1925 -0
- package/mcp-server/dist/session.d.ts +85 -0
- package/mcp-server/dist/session.js +152 -0
- package/mcp-server/mcp-app.html +28 -0
- package/mcp-server/mcp-config.example.json +19 -0
- package/mcp-server/package-lock.json +4027 -0
- package/mcp-server/package.json +31 -0
- package/mcp-server/src/codegen.ts +189 -0
- package/mcp-server/src/mcp-app.css +360 -0
- package/mcp-server/src/mcp-app.ts +547 -0
- package/mcp-server/src/provision-types.ts +209 -0
- package/mcp-server/src/server.ts +2391 -0
- package/mcp-server/src/session.ts +194 -0
- package/mcp-server/tsconfig.json +16 -0
- package/mcp-server/vite.config.ts +23 -0
- package/package.json +158 -0
- package/schema.json +1046 -0
- package/scripts/generate-skills.js +94 -0
- package/sdk-log-formatter.js +1157 -0
- package/sdk.d.ts +1486 -0
- package/sdk.js +4336 -0
- package/setup/aws/cloudformation.yaml +463 -0
- package/setup/aws/disable-defender.sh +42 -0
- package/setup/aws/install-dev-runner.sh +79 -0
- package/setup/aws/spawn-runner.sh +289 -0
- package/test/captcha-solver.test.mjs +152 -0
- package/test/chrome-remote-debugging.test.mjs +66 -0
- package/test/duckduckgo/experiment.test.mjs +28 -0
- package/test/duckduckgo/setup.test.mjs +29 -0
- package/test/manual/debug-locate-response.js +82 -0
- package/test/manual/reconnect-provision.test.mjs +49 -0
- package/test/manual/test-console-logs.test.mjs +42 -0
- package/test/manual/test-find-api.js +73 -0
- package/test/manual/test-init.sh +54 -0
- package/test/manual/test-prompt-cache.js +97 -0
- package/test/manual/test-provision-auth.mjs +22 -0
- package/test/manual/test-sandbox-render.js +29 -0
- package/test/manual/test-sdk-methods.js +15 -0
- package/test/manual/test-sdk-refactor.js +53 -0
- package/test/manual/test-stack-trace.mjs +57 -0
- package/test/manual/verify-element-api.js +89 -0
- package/test/manual/verify-types.js +0 -0
- package/test/manual-unawaited-promise.test.mjs +31 -0
- package/vitest.config.mjs +58 -0
- package/vitest.runner.config.mjs +33 -0
- package/vscode-extension/.vscodeignore +12 -0
- package/vscode-extension/README.md +94 -0
- package/vscode-extension/media/icon.png +0 -0
- package/vscode-extension/package-lock.json +4126 -0
- package/vscode-extension/package.json +86 -0
- package/vscode-extension/src/extension.ts +829 -0
- package/vscode-extension/testdriverai-0.1.0.vsix +0 -0
- package/vscode-extension/tsconfig.json +16 -0
|
@@ -0,0 +1,1684 @@
|
|
|
1
|
+
// the actual commands to interact with the system
|
|
2
|
+
const { createSDK } = require("./sdk.js");
|
|
3
|
+
const vm = require("vm");
|
|
4
|
+
const theme = require("./theme.js");
|
|
5
|
+
|
|
6
|
+
const fs = require("fs").promises; // Using the promises version for async operations
|
|
7
|
+
const { findTemplateImage } = require("./subimage/index");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const Jimp = require("jimp");
|
|
10
|
+
const os = require("os");
|
|
11
|
+
const cliProgress = require("cli-progress");
|
|
12
|
+
const { createRedraw } = require("./redraw.js");
|
|
13
|
+
|
|
14
|
+
const { events } = require("../events.js");
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Helper to detect if arguments are using object-based API or positional API
|
|
18
|
+
* @param {Array} args - The arguments passed to a command
|
|
19
|
+
* @param {Array<string>} knownKeys - Keys that would be present in object-based call
|
|
20
|
+
* @returns {boolean} True if using object-based API
|
|
21
|
+
*/
|
|
22
|
+
const isObjectArgs = (args, knownKeys) => {
|
|
23
|
+
if (args.length === 0) return false;
|
|
24
|
+
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && !Array.isArray(args[0])) {
|
|
25
|
+
// Check if it has at least one known key
|
|
26
|
+
return knownKeys.some(key => key in args[0]);
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Error When a match is not found
|
|
33
|
+
* these should be recoverable by --heal
|
|
34
|
+
**/
|
|
35
|
+
class MatchError extends Error {
|
|
36
|
+
constructor(message, fatal = false) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.fatal = fatal;
|
|
39
|
+
this.attachScreenshot = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Error when something is wrong with th command
|
|
45
|
+
**/
|
|
46
|
+
class CommandError extends Error {
|
|
47
|
+
constructor(message) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.fatal = true;
|
|
50
|
+
this.attachScreenshot = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalize redraw options from new thresholds format or legacy format.
|
|
56
|
+
* New: { enabled: true, thresholds: { screen: 0.05, network: true } }
|
|
57
|
+
* Legacy: { enabled: true, diffThreshold: 0.1, screenRedraw: true, networkMonitor: true }
|
|
58
|
+
* @param {Object} opts - Raw redraw options object
|
|
59
|
+
* @returns {Object} Normalised { enabled, screenRedraw, networkMonitor }
|
|
60
|
+
*/
|
|
61
|
+
const normalizeRedrawOpts = (opts) => {
|
|
62
|
+
if (!opts || typeof opts !== 'object') return { enabled: !!opts };
|
|
63
|
+
const result = { enabled: opts.enabled !== false };
|
|
64
|
+
if (opts.thresholds && typeof opts.thresholds === 'object') {
|
|
65
|
+
result.screenRedraw = opts.thresholds.screen !== false;
|
|
66
|
+
result.networkMonitor = !!opts.thresholds.network;
|
|
67
|
+
} else {
|
|
68
|
+
result.screenRedraw = opts.screenRedraw !== undefined ? opts.screenRedraw : true;
|
|
69
|
+
result.networkMonitor = opts.networkMonitor !== undefined ? opts.networkMonitor : false;
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extract redraw options from command options
|
|
76
|
+
* @param {Object} options - Command options that may contain redraw settings
|
|
77
|
+
* @returns {Object} Redraw options object
|
|
78
|
+
*/
|
|
79
|
+
const extractRedrawOptions = (options = {}) => {
|
|
80
|
+
// Support nested redraw object (new or legacy format)
|
|
81
|
+
if (options.redraw && typeof options.redraw === 'object') {
|
|
82
|
+
return normalizeRedrawOpts(options.redraw);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Support flat options for convenience (legacy)
|
|
86
|
+
const redrawOpts = {};
|
|
87
|
+
if ('redrawEnabled' in options) redrawOpts.enabled = options.redrawEnabled;
|
|
88
|
+
if ('redrawScreenRedraw' in options) redrawOpts.screenRedraw = options.redrawScreenRedraw;
|
|
89
|
+
if ('redrawNetworkMonitor' in options) redrawOpts.networkMonitor = options.redrawNetworkMonitor;
|
|
90
|
+
if ('redrawDiffThreshold' in options) redrawOpts.screenRedraw = true;
|
|
91
|
+
|
|
92
|
+
return redrawOpts;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Factory function that creates commands with the provided emitter
|
|
96
|
+
const createCommands = (
|
|
97
|
+
emitter,
|
|
98
|
+
system,
|
|
99
|
+
sandbox,
|
|
100
|
+
config,
|
|
101
|
+
sessionInstance,
|
|
102
|
+
getCurrentFilePath,
|
|
103
|
+
redrawThreshold = 0.01,
|
|
104
|
+
getDashcamElapsedTime = null,
|
|
105
|
+
getSoftAssertMode = () => false, // getter for soft assert mode (used by act())
|
|
106
|
+
) => {
|
|
107
|
+
// Create SDK instance with emitter, config, and session
|
|
108
|
+
const sdk = createSDK(emitter, config, sessionInstance);
|
|
109
|
+
// Create redraw instance with the system - support both number and object for backward compatibility
|
|
110
|
+
const defaultRedrawOptions = typeof redrawThreshold === 'number'
|
|
111
|
+
? { diffThreshold: redrawThreshold }
|
|
112
|
+
: redrawThreshold;
|
|
113
|
+
const redraw = createRedraw(emitter, system, sandbox, defaultRedrawOptions);
|
|
114
|
+
|
|
115
|
+
// Helper method to resolve file paths relative to the current file
|
|
116
|
+
const resolveRelativePath = (relativePath) => {
|
|
117
|
+
// If it's already an absolute path, return as-is
|
|
118
|
+
if (path.isAbsolute(relativePath)) {
|
|
119
|
+
return relativePath;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Get the current file path dynamically
|
|
123
|
+
const currentFilePath = getCurrentFilePath();
|
|
124
|
+
|
|
125
|
+
// For relative paths, resolve relative to the current file's directory
|
|
126
|
+
if (currentFilePath) {
|
|
127
|
+
return path.resolve(path.dirname(currentFilePath), relativePath);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fallback to workingDir
|
|
131
|
+
return path.resolve(config.TD_WORKING_DIR || process.cwd(), relativePath);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const niceSeconds = (ms) => {
|
|
135
|
+
return Math.round(ms / 1000);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const delay = (t) => new Promise((resolve) => setTimeout(resolve, t));
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Track an interaction via HTTP API (fire-and-forget)
|
|
142
|
+
* @param {Object} data - Interaction data
|
|
143
|
+
* @param {string} data.interactionType - Type of interaction (click, type, assert, etc.)
|
|
144
|
+
* @param {string} [data.prompt] - Description/prompt for the interaction
|
|
145
|
+
* @param {Object} [data.input] - Input data (varies by interaction type)
|
|
146
|
+
* @param {Object} [data.coordinates] - Coordinates {x, y}
|
|
147
|
+
* @param {number} data.timestamp - Absolute epoch timestamp
|
|
148
|
+
* @param {number} [data.duration] - Duration in ms
|
|
149
|
+
* @param {boolean} data.success - Whether the interaction succeeded
|
|
150
|
+
* @param {string} [data.error] - Error message if failed
|
|
151
|
+
* @param {boolean} [data.cacheHit] - Whether cache was used
|
|
152
|
+
* @param {string} [data.selector] - Selector ID
|
|
153
|
+
* @param {boolean} [data.selectorUsed] - Whether selector was used
|
|
154
|
+
* @param {number} [data.confidence] - AI confidence score
|
|
155
|
+
* @param {string} [data.reasoning] - AI reasoning
|
|
156
|
+
* @param {number} [data.similarity] - Cache similarity score
|
|
157
|
+
* @param {string} [data.screenshotUrl] - S3 key for screenshot
|
|
158
|
+
* @param {boolean} [data.isSecret] - Whether interaction contains sensitive data
|
|
159
|
+
*/
|
|
160
|
+
const trackInteraction = (data) => {
|
|
161
|
+
const sessionId = sessionInstance?.get();
|
|
162
|
+
if (!sessionId) return;
|
|
163
|
+
|
|
164
|
+
sdk.req("/api/v7.0.0/testdriver/interaction/track", {
|
|
165
|
+
session: sessionId,
|
|
166
|
+
type: data.interactionType,
|
|
167
|
+
coordinates: data.coordinates,
|
|
168
|
+
input: data.input,
|
|
169
|
+
prompt: data.prompt,
|
|
170
|
+
selectorUsed: data.selectorUsed,
|
|
171
|
+
selector: data.selector,
|
|
172
|
+
cacheHit: data.cacheHit,
|
|
173
|
+
status: "completed",
|
|
174
|
+
success: data.success,
|
|
175
|
+
error: data.error,
|
|
176
|
+
duration: data.duration,
|
|
177
|
+
timestamp: data.timestamp,
|
|
178
|
+
isSecret: data.isSecret,
|
|
179
|
+
confidence: data.confidence,
|
|
180
|
+
reasoning: data.reasoning,
|
|
181
|
+
similarity: data.similarity,
|
|
182
|
+
screenshotUrl: data.screenshotUrl,
|
|
183
|
+
}).catch((err) => {
|
|
184
|
+
console.warn(`Failed to track ${data.interactionType} interaction:`, err.message);
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const findImageOnScreen = async (
|
|
189
|
+
relativePath,
|
|
190
|
+
haystack,
|
|
191
|
+
restrictToWindow,
|
|
192
|
+
) => {
|
|
193
|
+
// add .png to relative path if not already there
|
|
194
|
+
if (!relativePath.endsWith(".png")) {
|
|
195
|
+
relativePath = relativePath + ".png";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let needle = relativePath;
|
|
199
|
+
|
|
200
|
+
// check if the file exists
|
|
201
|
+
if (!fs.access(needle)) {
|
|
202
|
+
throw new CommandError(
|
|
203
|
+
`Image does not exist or do not have access: ${needle}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const bar1 = new cliProgress.SingleBar(
|
|
208
|
+
{},
|
|
209
|
+
cliProgress.Presets.shades_classic,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
let thresholds = [0.9, 0.8, 0.7];
|
|
213
|
+
|
|
214
|
+
let scaleFactors = [1, 0.5, 2, 0.75, 1.25, 1.5];
|
|
215
|
+
|
|
216
|
+
let result = null;
|
|
217
|
+
let highestThreshold = 0;
|
|
218
|
+
|
|
219
|
+
let totalOperations = thresholds.length * scaleFactors.length;
|
|
220
|
+
bar1.start(totalOperations, 0);
|
|
221
|
+
|
|
222
|
+
for (let scaleFactor of scaleFactors) {
|
|
223
|
+
let needleSize = 1 / scaleFactor;
|
|
224
|
+
|
|
225
|
+
const scaledNeedle = await Jimp.read(path.join(needle));
|
|
226
|
+
scaledNeedle.scale(needleSize);
|
|
227
|
+
|
|
228
|
+
const haystackImage = await Jimp.read(haystack);
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
scaledNeedle.bitmap.width > haystackImage.bitmap.width ||
|
|
232
|
+
scaledNeedle.bitmap.height > haystackImage.bitmap.height
|
|
233
|
+
) {
|
|
234
|
+
// Needle is larger than haystack, skip this scale factor
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "scaledNeedle_"));
|
|
239
|
+
const scaledNeedlePath = path.join(
|
|
240
|
+
tempDir,
|
|
241
|
+
`scaledNeedle_${needleSize}.png`,
|
|
242
|
+
);
|
|
243
|
+
await scaledNeedle.writeAsync(scaledNeedlePath);
|
|
244
|
+
|
|
245
|
+
for (let threshold of thresholds) {
|
|
246
|
+
if (threshold >= highestThreshold) {
|
|
247
|
+
let results = await findTemplateImage(
|
|
248
|
+
haystack,
|
|
249
|
+
scaledNeedlePath,
|
|
250
|
+
threshold,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// throw away any results that are not within the active window
|
|
254
|
+
let activeWindow = await system.activeWin();
|
|
255
|
+
|
|
256
|
+
// filter out text that is not in the active window
|
|
257
|
+
if (restrictToWindow) {
|
|
258
|
+
results = results.filter((el) => {
|
|
259
|
+
return (
|
|
260
|
+
el.centerX > activeWindow.bounds.x &&
|
|
261
|
+
el.centerX <
|
|
262
|
+
activeWindow.bounds.x + activeWindow.bounds.width &&
|
|
263
|
+
el.centerY > activeWindow.bounds.y &&
|
|
264
|
+
el.centerY < activeWindow.bounds.y + activeWindow.bounds.height
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (results.length) {
|
|
270
|
+
result = { ...results[0], threshold, scaleFactor, needleSize };
|
|
271
|
+
highestThreshold = threshold;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
bar1.increment();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
bar1.stop();
|
|
281
|
+
|
|
282
|
+
return result;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const assert = async (assertion, shouldThrow = false, options = {}) => {
|
|
286
|
+
// Log asserting action
|
|
287
|
+
const { formatter } = require("../../sdk-log-formatter.js");
|
|
288
|
+
const assertingMessage = formatter.formatAsserting(assertion);
|
|
289
|
+
emitter.emit(events.log.log, assertingMessage);
|
|
290
|
+
|
|
291
|
+
// Capture absolute timestamp at the very start of the command
|
|
292
|
+
// Frontend will calculate relative time using: timestamp - replay.clientStartDate
|
|
293
|
+
const assertTimestamp = Date.now();
|
|
294
|
+
const assertStartTime = assertTimestamp;
|
|
295
|
+
|
|
296
|
+
// Extract cache options
|
|
297
|
+
const { threshold = 0.05, cacheKey, os, resolution, ai } = options;
|
|
298
|
+
|
|
299
|
+
// Debug log cache settings
|
|
300
|
+
emitter.emit(
|
|
301
|
+
events.log.debug,
|
|
302
|
+
`🔍 assert() threshold: ${threshold} (cache ${threshold < 0 ? "DISABLED" : "ENABLED"}${cacheKey ? `, cacheKey: ${cacheKey.substring(0, 8)}...` : ""})`,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Use v7 endpoint for assert with caching support
|
|
306
|
+
let response = await sdk.req("assert", {
|
|
307
|
+
expect: assertion,
|
|
308
|
+
image: await system.captureScreenBase64(),
|
|
309
|
+
threshold,
|
|
310
|
+
cacheKey,
|
|
311
|
+
os,
|
|
312
|
+
resolution,
|
|
313
|
+
ai,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const assertDuration = Date.now() - assertStartTime;
|
|
317
|
+
|
|
318
|
+
// Handle both old (string) and new (object) response formats
|
|
319
|
+
// New v7 API returns: { data: { passed, reasoning, content, cacheHit... }, cacheHit, similarity }
|
|
320
|
+
// Old API returns: { data: "The task passed/failed..." }
|
|
321
|
+
let passed;
|
|
322
|
+
let responseText;
|
|
323
|
+
let cacheHit = false;
|
|
324
|
+
let similarity = null;
|
|
325
|
+
|
|
326
|
+
let confidence = null;
|
|
327
|
+
let reasoning = null;
|
|
328
|
+
|
|
329
|
+
if (typeof response.data === 'object' && response.data !== null) {
|
|
330
|
+
// New structured response
|
|
331
|
+
passed = response.data.passed;
|
|
332
|
+
responseText = response.data.content || response.data.reasoning || '';
|
|
333
|
+
cacheHit = response.cacheHit || response.data.cacheHit || false;
|
|
334
|
+
similarity = response.similarity || response.data.cacheSimilarity;
|
|
335
|
+
confidence = response.confidence != null ? response.confidence : (response.data.confidence != null ? response.data.confidence : null);
|
|
336
|
+
reasoning = response.data.reasoning || null;
|
|
337
|
+
} else {
|
|
338
|
+
// Old string response (backward compatibility)
|
|
339
|
+
responseText = response.data || '';
|
|
340
|
+
passed = responseText.indexOf("The task passed") > -1;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Log the result with cache info
|
|
344
|
+
emitter.emit(events.log.narration, formatter.formatAssertResult(passed, responseText, assertDuration, cacheHit), true);
|
|
345
|
+
|
|
346
|
+
// Track interaction with success/failure (fire-and-forget)
|
|
347
|
+
trackInteraction({
|
|
348
|
+
interactionType: "assert",
|
|
349
|
+
prompt: assertion,
|
|
350
|
+
timestamp: assertTimestamp,
|
|
351
|
+
duration: assertDuration,
|
|
352
|
+
success: passed,
|
|
353
|
+
error: passed ? undefined : responseText,
|
|
354
|
+
cacheHit: cacheHit,
|
|
355
|
+
confidence: confidence,
|
|
356
|
+
reasoning: reasoning,
|
|
357
|
+
similarity: similarity,
|
|
358
|
+
screenshotUrl: response?.screenshotKey ?? null,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (passed) {
|
|
362
|
+
return true;
|
|
363
|
+
} else {
|
|
364
|
+
if (shouldThrow) {
|
|
365
|
+
// Is fatal, otherwise it just changes the assertion to be true
|
|
366
|
+
const errorMessage = `AI Assertion failed: ${assertion}\n${responseText}`;
|
|
367
|
+
throw new MatchError(errorMessage, true);
|
|
368
|
+
} else {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Scroll the screen in a direction
|
|
376
|
+
* @param {string} [direction='down'] - Direction to scroll ('up', 'down', 'left', 'right')
|
|
377
|
+
* @param {Object} [options] - Additional options
|
|
378
|
+
* @param {number} [options.amount=300] - Amount to scroll in pixels
|
|
379
|
+
* @param {Object} [options.redraw] - Redraw detection options
|
|
380
|
+
* @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
|
|
381
|
+
* @param {Object} [options.redraw.thresholds] - Threshold configuration
|
|
382
|
+
* @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
|
|
383
|
+
* @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
|
|
384
|
+
*/
|
|
385
|
+
const scroll = async (direction = 'down', options = {}) => {
|
|
386
|
+
// Capture absolute timestamp at the very start of the command
|
|
387
|
+
// Frontend will calculate relative time using: timestamp - replay.clientStartDate
|
|
388
|
+
const scrollTimestamp = Date.now();
|
|
389
|
+
const scrollStartTime = scrollTimestamp;
|
|
390
|
+
// Convert number to object format
|
|
391
|
+
if (typeof options === 'number') {
|
|
392
|
+
options = { amount: options };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let { amount = 300 } = options;
|
|
396
|
+
const redrawOptions = extractRedrawOptions(options);
|
|
397
|
+
|
|
398
|
+
await redraw.start(redrawOptions);
|
|
399
|
+
|
|
400
|
+
amount = parseInt(amount, 10);
|
|
401
|
+
|
|
402
|
+
const before = await system.captureScreenBase64();
|
|
403
|
+
let scrollSuccess = true;
|
|
404
|
+
let scrollError;
|
|
405
|
+
let actionEndTime;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
switch (direction) {
|
|
409
|
+
case "up":
|
|
410
|
+
await sandbox.send({
|
|
411
|
+
type: "scroll",
|
|
412
|
+
amount,
|
|
413
|
+
direction,
|
|
414
|
+
});
|
|
415
|
+
actionEndTime = Date.now();
|
|
416
|
+
break;
|
|
417
|
+
case "down":
|
|
418
|
+
await sandbox.send({
|
|
419
|
+
type: "scroll",
|
|
420
|
+
amount,
|
|
421
|
+
direction,
|
|
422
|
+
});
|
|
423
|
+
actionEndTime = Date.now();
|
|
424
|
+
break;
|
|
425
|
+
case "left":
|
|
426
|
+
console.error("Not Supported");
|
|
427
|
+
scrollSuccess = false;
|
|
428
|
+
scrollError = "Left scroll not supported";
|
|
429
|
+
break;
|
|
430
|
+
case "right":
|
|
431
|
+
console.error("Not Supported");
|
|
432
|
+
scrollSuccess = false;
|
|
433
|
+
scrollError = "Right scroll not supported";
|
|
434
|
+
break;
|
|
435
|
+
default:
|
|
436
|
+
scrollSuccess = false;
|
|
437
|
+
scrollError = "Direction not found";
|
|
438
|
+
throw new CommandError("Direction not found");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const actionDuration = actionEndTime ? actionEndTime - scrollStartTime : Date.now() - scrollStartTime;
|
|
442
|
+
|
|
443
|
+
// Log nested scroll action completion
|
|
444
|
+
const { formatter } = require("../../sdk-log-formatter.js");
|
|
445
|
+
emitter.emit(
|
|
446
|
+
events.log.narration,
|
|
447
|
+
formatter.formatScrollResult(direction, amount, actionDuration),
|
|
448
|
+
true,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Wait for redraw and track duration
|
|
452
|
+
// Increase timeout for scroll operations as they can take 1-2 seconds to complete
|
|
453
|
+
const redrawStartTime = Date.now();
|
|
454
|
+
await redraw.wait(5000, redrawOptions);
|
|
455
|
+
const redrawDuration = Date.now() - redrawStartTime;
|
|
456
|
+
|
|
457
|
+
const after = await system.captureScreenBase64();
|
|
458
|
+
|
|
459
|
+
if (before === after) {
|
|
460
|
+
emitter.emit(
|
|
461
|
+
events.log.warn,
|
|
462
|
+
"Attempted to scroll, but the screen did not change. You may need to click a non-interactive element to focus the scrollable area first.",
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Log nested redraw completion
|
|
467
|
+
emitter.emit(
|
|
468
|
+
events.log.narration,
|
|
469
|
+
formatter.formatRedrawComplete(redrawDuration),
|
|
470
|
+
true,
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// Track interaction success (fire-and-forget)
|
|
474
|
+
const scrollDuration = Date.now() - scrollStartTime;
|
|
475
|
+
trackInteraction({
|
|
476
|
+
interactionType: "scroll",
|
|
477
|
+
input: { direction, amount },
|
|
478
|
+
timestamp: scrollTimestamp,
|
|
479
|
+
duration: scrollDuration,
|
|
480
|
+
success: scrollSuccess,
|
|
481
|
+
error: scrollError,
|
|
482
|
+
});
|
|
483
|
+
} catch (error) {
|
|
484
|
+
// Track interaction failure (fire-and-forget)
|
|
485
|
+
const scrollDuration = Date.now() - scrollStartTime;
|
|
486
|
+
trackInteraction({
|
|
487
|
+
interactionType: "scroll",
|
|
488
|
+
input: { direction, amount },
|
|
489
|
+
timestamp: scrollTimestamp,
|
|
490
|
+
duration: scrollDuration,
|
|
491
|
+
success: false,
|
|
492
|
+
error: error.message,
|
|
493
|
+
});
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Perform a mouse click action
|
|
500
|
+
* @param {Object|number} options - Options object or x coordinate (for backward compatibility)
|
|
501
|
+
* @param {number} options.x - X coordinate
|
|
502
|
+
* @param {number} options.y - Y coordinate
|
|
503
|
+
* @param {string} [options.action='click'] - Click action ('click', 'right-click', 'double-click', 'hover', 'mouseDown', 'mouseUp')
|
|
504
|
+
* @param {string} [options.prompt] - Prompt for tracking
|
|
505
|
+
* @param {boolean} [options.cacheHit] - Whether cache was hit
|
|
506
|
+
* @param {string} [options.selector] - Selector used
|
|
507
|
+
* @param {boolean} [options.selectorUsed] - Whether selector was used
|
|
508
|
+
* @param {Object} [options.redraw] - Redraw detection options
|
|
509
|
+
* @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
|
|
510
|
+
* @param {Object} [options.redraw.thresholds] - Threshold configuration
|
|
511
|
+
* @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
|
|
512
|
+
* @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
|
|
513
|
+
*/
|
|
514
|
+
const click = async (...args) => {
|
|
515
|
+
// Capture absolute timestamp at the very start of the command
|
|
516
|
+
// Frontend will calculate relative time using: timestamp - replay.clientStartDate
|
|
517
|
+
const clickTimestamp = Date.now();
|
|
518
|
+
const clickStartTime = clickTimestamp;
|
|
519
|
+
let x, y, action, elementData, redrawOptions;
|
|
520
|
+
|
|
521
|
+
// Handle both object and positional argument styles
|
|
522
|
+
if (isObjectArgs(args, ['x', 'y', 'action', 'prompt', 'cacheHit', 'selector'])) {
|
|
523
|
+
const { x: xPos, y: yPos, action: actionArg = 'click', redraw: redrawOpts, ...rest } = args[0];
|
|
524
|
+
x = xPos;
|
|
525
|
+
y = yPos;
|
|
526
|
+
action = actionArg;
|
|
527
|
+
elementData = rest;
|
|
528
|
+
redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
|
|
529
|
+
} else {
|
|
530
|
+
// Legacy positional: click(x, y, action, elementData)
|
|
531
|
+
[x, y, action = 'click', elementData = {}] = args;
|
|
532
|
+
redrawOptions = extractRedrawOptions(elementData);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
await redraw.start(redrawOptions);
|
|
537
|
+
|
|
538
|
+
let button = "left";
|
|
539
|
+
let double = false;
|
|
540
|
+
|
|
541
|
+
if (action === "right-click") {
|
|
542
|
+
button = "right";
|
|
543
|
+
}
|
|
544
|
+
if (action === "double-click") {
|
|
545
|
+
double = true;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Show nested action details
|
|
549
|
+
const actionText = action.split("-").join("");
|
|
550
|
+
const clickActionLogStart = Date.now();
|
|
551
|
+
|
|
552
|
+
x = parseInt(x);
|
|
553
|
+
y = parseInt(y);
|
|
554
|
+
|
|
555
|
+
// Add absolute timestamp for sandbox events
|
|
556
|
+
elementData.timestamp = Date.now();
|
|
557
|
+
|
|
558
|
+
// Update the action log with duration
|
|
559
|
+
const clickMoveEndTime = Date.now();
|
|
560
|
+
const { formatter } = require("../../sdk-log-formatter.js");
|
|
561
|
+
emitter.emit(
|
|
562
|
+
events.log.narration,
|
|
563
|
+
formatter.formatClickResult(button, x, y, clickMoveEndTime - clickActionLogStart),
|
|
564
|
+
true,
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
if (action !== "hover") {
|
|
568
|
+
// Update timestamp for the actual click action
|
|
569
|
+
elementData.timestamp = Date.now();
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
if (action === "click" || action === "left-click") {
|
|
573
|
+
await sandbox.send({ type: "leftClick", x, y, ...elementData });
|
|
574
|
+
} else if (action === "right-click") {
|
|
575
|
+
await sandbox.send({ type: "rightClick", x, y, ...elementData });
|
|
576
|
+
} else if (action === "middle-click") {
|
|
577
|
+
await sandbox.send({ type: "middleClick", x, y, ...elementData });
|
|
578
|
+
} else if (action === "double-click") {
|
|
579
|
+
await sandbox.send({ type: "doubleClick", x, y, ...elementData });
|
|
580
|
+
} else if (action === "mouseDown") {
|
|
581
|
+
await sandbox.send({ type: "mousePress", button: "left", x, y, ...elementData });
|
|
582
|
+
} else if (action === "mouseUp") {
|
|
583
|
+
// Move first to create drag motion, then release
|
|
584
|
+
// (pyautogui.mouseUp with x/y teleports instead of dragging)
|
|
585
|
+
await sandbox.send({ type: "moveMouse", x, y, ...elementData });
|
|
586
|
+
await sandbox.send({
|
|
587
|
+
type: "mouseRelease",
|
|
588
|
+
button: "left",
|
|
589
|
+
...elementData
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
emitter.emit(events.mouseClick, { x, y, button, click, double });
|
|
594
|
+
|
|
595
|
+
// Track action duration (before redraw wait)
|
|
596
|
+
const actionEndTime = Date.now();
|
|
597
|
+
const actionDuration = actionEndTime - clickStartTime;
|
|
598
|
+
|
|
599
|
+
// Track interaction (fire-and-forget)
|
|
600
|
+
if (elementData.prompt) {
|
|
601
|
+
trackInteraction({
|
|
602
|
+
interactionType: "click",
|
|
603
|
+
prompt: elementData.prompt,
|
|
604
|
+
input: { x, y, action },
|
|
605
|
+
timestamp: clickTimestamp,
|
|
606
|
+
duration: actionDuration,
|
|
607
|
+
success: true,
|
|
608
|
+
cacheHit: elementData.cacheHit,
|
|
609
|
+
selector: elementData.selector,
|
|
610
|
+
selectorUsed: elementData.selectorUsed,
|
|
611
|
+
confidence: elementData.confidence ?? null,
|
|
612
|
+
reasoning: elementData.reasoning ?? null,
|
|
613
|
+
similarity: elementData.similarity ?? null,
|
|
614
|
+
screenshotUrl: elementData.screenshotUrl ?? null,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Wait for redraw and track duration
|
|
619
|
+
const redrawStartTime = Date.now();
|
|
620
|
+
await redraw.wait(5000, redrawOptions);
|
|
621
|
+
const redrawDuration = Date.now() - redrawStartTime;
|
|
622
|
+
|
|
623
|
+
// Log nested redraw completion
|
|
624
|
+
emitter.emit(
|
|
625
|
+
events.log.narration,
|
|
626
|
+
formatter.formatRedrawComplete(redrawDuration),
|
|
627
|
+
true,
|
|
628
|
+
);
|
|
629
|
+
} else {
|
|
630
|
+
// For hover action (within click function)
|
|
631
|
+
const redrawStartTime = Date.now();
|
|
632
|
+
await redraw.wait(5000, redrawOptions);
|
|
633
|
+
const redrawDuration = Date.now() - redrawStartTime;
|
|
634
|
+
const actionDuration = Date.now() - clickStartTime - redrawDuration;
|
|
635
|
+
|
|
636
|
+
// Log nested redraw completion
|
|
637
|
+
emitter.emit(
|
|
638
|
+
events.log.narration,
|
|
639
|
+
formatter.formatRedrawComplete(redrawDuration),
|
|
640
|
+
true,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return;
|
|
645
|
+
} catch (error) {
|
|
646
|
+
// Track interaction failure (fire-and-forget)
|
|
647
|
+
if (elementData.prompt) {
|
|
648
|
+
const clickDuration = Date.now() - clickStartTime;
|
|
649
|
+
trackInteraction({
|
|
650
|
+
interactionType: "click",
|
|
651
|
+
prompt: elementData.prompt,
|
|
652
|
+
input: { x, y, action },
|
|
653
|
+
timestamp: clickTimestamp,
|
|
654
|
+
duration: clickDuration,
|
|
655
|
+
success: false,
|
|
656
|
+
error: error.message,
|
|
657
|
+
cacheHit: elementData.cacheHit,
|
|
658
|
+
selector: elementData.selector,
|
|
659
|
+
selectorUsed: elementData.selectorUsed,
|
|
660
|
+
confidence: elementData.confidence ?? null,
|
|
661
|
+
reasoning: elementData.reasoning ?? null,
|
|
662
|
+
similarity: elementData.similarity ?? null,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
throw error;
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Hover at coordinates
|
|
671
|
+
* @param {Object|number} options - Options object or x coordinate (for backward compatibility)
|
|
672
|
+
* @param {number} options.x - X coordinate
|
|
673
|
+
* @param {number} options.y - Y coordinate
|
|
674
|
+
* @param {string} [options.prompt] - Prompt for tracking
|
|
675
|
+
* @param {boolean} [options.cacheHit] - Whether cache was hit
|
|
676
|
+
* @param {string} [options.selector] - Selector used
|
|
677
|
+
* @param {boolean} [options.selectorUsed] - Whether selector was used
|
|
678
|
+
*/
|
|
679
|
+
const hover = async (...args) => {
|
|
680
|
+
// Capture absolute timestamp at the very start of the command
|
|
681
|
+
// Frontend will calculate relative time using: timestamp - replay.clientStartDate
|
|
682
|
+
const hoverTimestamp = Date.now();
|
|
683
|
+
const hoverStartTime = hoverTimestamp;
|
|
684
|
+
let x, y, elementData, redrawOptions;
|
|
685
|
+
|
|
686
|
+
// Handle both object and positional argument styles
|
|
687
|
+
if (isObjectArgs(args, ['x', 'y', 'prompt', 'cacheHit', 'selector'])) {
|
|
688
|
+
const { x: xPos, y: yPos, redraw: redrawOpts, ...rest } = args[0];
|
|
689
|
+
x = xPos;
|
|
690
|
+
y = yPos;
|
|
691
|
+
elementData = rest;
|
|
692
|
+
redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
|
|
693
|
+
} else {
|
|
694
|
+
// Legacy positional: hover(x, y, elementData)
|
|
695
|
+
[x, y, elementData = {}] = args;
|
|
696
|
+
redrawOptions = extractRedrawOptions(elementData);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
emitter.emit(events.log.narration, theme.dim(`hovering at ${x}, ${y}...`));
|
|
701
|
+
|
|
702
|
+
await redraw.start(redrawOptions);
|
|
703
|
+
|
|
704
|
+
x = parseInt(x);
|
|
705
|
+
y = parseInt(y);
|
|
706
|
+
|
|
707
|
+
// Add absolute timestamp for sandbox events
|
|
708
|
+
elementData.timestamp = Date.now();
|
|
709
|
+
|
|
710
|
+
await sandbox.send({ type: "moveMouse", x, y, ...elementData });
|
|
711
|
+
|
|
712
|
+
// Track interaction (fire-and-forget)
|
|
713
|
+
const actionEndTime = Date.now();
|
|
714
|
+
const actionDuration = actionEndTime - hoverStartTime;
|
|
715
|
+
|
|
716
|
+
if (elementData.prompt) {
|
|
717
|
+
trackInteraction({
|
|
718
|
+
interactionType: "hover",
|
|
719
|
+
prompt: elementData.prompt,
|
|
720
|
+
input: { x, y },
|
|
721
|
+
timestamp: hoverTimestamp,
|
|
722
|
+
duration: actionDuration,
|
|
723
|
+
success: true,
|
|
724
|
+
cacheHit: elementData.cacheHit,
|
|
725
|
+
selector: elementData.selector,
|
|
726
|
+
selectorUsed: elementData.selectorUsed,
|
|
727
|
+
confidence: elementData.confidence ?? null,
|
|
728
|
+
reasoning: elementData.reasoning ?? null,
|
|
729
|
+
similarity: elementData.similarity ?? null,
|
|
730
|
+
screenshotUrl: elementData.screenshotUrl ?? null,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Wait for redraw and track duration
|
|
735
|
+
const redrawStartTime = Date.now();
|
|
736
|
+
await redraw.wait(2500, redrawOptions);
|
|
737
|
+
const redrawDuration = Date.now() - redrawStartTime;
|
|
738
|
+
|
|
739
|
+
// Log action completion with separate durations
|
|
740
|
+
const { formatter } = require("../../sdk-log-formatter.js");
|
|
741
|
+
const completionMessage = formatter.formatActionComplete("hover", elementData.prompt, {
|
|
742
|
+
actionDuration,
|
|
743
|
+
redrawDuration,
|
|
744
|
+
cacheHit: elementData.cacheHit,
|
|
745
|
+
});
|
|
746
|
+
emitter.emit(events.log.log, completionMessage);
|
|
747
|
+
|
|
748
|
+
return;
|
|
749
|
+
} catch (error) {
|
|
750
|
+
// Track interaction failure (fire-and-forget)
|
|
751
|
+
if (elementData.prompt) {
|
|
752
|
+
const hoverDuration = Date.now() - hoverStartTime;
|
|
753
|
+
trackInteraction({
|
|
754
|
+
interactionType: "hover",
|
|
755
|
+
prompt: elementData.prompt,
|
|
756
|
+
input: { x, y },
|
|
757
|
+
timestamp: hoverTimestamp,
|
|
758
|
+
duration: hoverDuration,
|
|
759
|
+
success: false,
|
|
760
|
+
error: error.message,
|
|
761
|
+
cacheHit: elementData.cacheHit,
|
|
762
|
+
selector: elementData.selector,
|
|
763
|
+
selectorUsed: elementData.selectorUsed,
|
|
764
|
+
confidence: elementData.confidence ?? null,
|
|
765
|
+
reasoning: elementData.reasoning ?? null,
|
|
766
|
+
similarity: elementData.similarity ?? null,
|
|
767
|
+
screenshotUrl: elementData.screenshotUrl ?? null,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
let commands = {
|
|
775
|
+
scroll: scroll,
|
|
776
|
+
click: click,
|
|
777
|
+
hover: hover,
|
|
778
|
+
/**
|
|
779
|
+
* Hover over text on screen
|
|
780
|
+
* @param {Object|string} options - Options object or description (for backward compatibility)
|
|
781
|
+
* @param {string} options.description - Description of the element to find
|
|
782
|
+
* @param {string} [options.action='click'] - Action to perform
|
|
783
|
+
* @param {number} [options.timeout=5000] - Timeout in milliseconds
|
|
784
|
+
*/
|
|
785
|
+
"hover-text": async (...args) => {
|
|
786
|
+
let description, text, action, timeout;
|
|
787
|
+
|
|
788
|
+
// Handle both object and positional argument styles
|
|
789
|
+
if (isObjectArgs(args, ['description', 'text', 'action', 'timeout'])) {
|
|
790
|
+
({ description, text, action = 'click', timeout = 5000 } = args[0]);
|
|
791
|
+
} else {
|
|
792
|
+
// Legacy positional: hoverText(description, action, timeout)
|
|
793
|
+
[description, action = 'click', timeout = 5000] = args;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Use text if provided, otherwise fall back to description
|
|
797
|
+
// This handles both the new spec (text + description) and legacy usage (just description)
|
|
798
|
+
description = text || description;
|
|
799
|
+
|
|
800
|
+
if (!description) {
|
|
801
|
+
throw new CommandError("hover-text requires either a text or description parameter");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
description = description.toString();
|
|
805
|
+
|
|
806
|
+
emitter.emit(
|
|
807
|
+
events.log.narration,
|
|
808
|
+
theme.dim(`searching for "${description}"...`),
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
// wait for the text to appear on screen
|
|
812
|
+
await commands["wait-for-text"]({ text: description, timeout });
|
|
813
|
+
|
|
814
|
+
emitter.emit(events.log.narration, theme.dim("thinking..."), true);
|
|
815
|
+
|
|
816
|
+
let response = await sdk.req("find", {
|
|
817
|
+
element: description,
|
|
818
|
+
image: await system.captureScreenBase64(),
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
if (!response || !response.coordinates) {
|
|
822
|
+
throw new MatchError("No text on screen matches description");
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Perform the action using the located coordinates
|
|
826
|
+
if (action === "hover") {
|
|
827
|
+
await commands.hover({ x: response.coordinates.x, y: response.coordinates.y });
|
|
828
|
+
} else {
|
|
829
|
+
await click({ x: response.coordinates.x, y: response.coordinates.y, action });
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return response;
|
|
833
|
+
},
|
|
834
|
+
/**
|
|
835
|
+
* Hover over an image on screen
|
|
836
|
+
* @param {Object|string} options - Options object or description (for backward compatibility)
|
|
837
|
+
* @param {string} options.description - Description of the image to find
|
|
838
|
+
* @param {string} [options.action='click'] - Action to perform
|
|
839
|
+
*/
|
|
840
|
+
"hover-image": async (...args) => {
|
|
841
|
+
let description, action;
|
|
842
|
+
|
|
843
|
+
// Handle both object and positional argument styles
|
|
844
|
+
if (isObjectArgs(args, ['description', 'action'])) {
|
|
845
|
+
({ description, action = 'click' } = args[0]);
|
|
846
|
+
} else {
|
|
847
|
+
// Legacy positional: hoverImage(description, action)
|
|
848
|
+
[description, action = 'click'] = args;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
emitter.emit(
|
|
852
|
+
events.log.narration,
|
|
853
|
+
theme.dim(`searching for image: "${description}"...`),
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
let response = await sdk.req("find", {
|
|
857
|
+
element: description,
|
|
858
|
+
image: await system.captureScreenBase64(),
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
if (!response || !response.coordinates) {
|
|
862
|
+
throw new MatchError("No image or icon on screen matches description");
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Perform the action using the located coordinates
|
|
866
|
+
if (action === "hover") {
|
|
867
|
+
await commands.hover({ x: response.coordinates.x, y: response.coordinates.y });
|
|
868
|
+
} else {
|
|
869
|
+
await click({ x: response.coordinates.x, y: response.coordinates.y, action });
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return response;
|
|
873
|
+
},
|
|
874
|
+
/**
|
|
875
|
+
* Match and interact with an image template
|
|
876
|
+
* @param {Object|string} options - Options object or path (for backward compatibility)
|
|
877
|
+
* @param {string} options.path - Path to the image template
|
|
878
|
+
* @param {string} [options.action='click'] - Action to perform
|
|
879
|
+
* @param {boolean} [options.invert=false] - Invert the match
|
|
880
|
+
*/
|
|
881
|
+
"match-image": async (...args) => {
|
|
882
|
+
let relativePath, action, invert;
|
|
883
|
+
|
|
884
|
+
// Handle both object and positional argument styles
|
|
885
|
+
if (isObjectArgs(args, ['path', 'action', 'invert'])) {
|
|
886
|
+
({ path: relativePath, action = 'click', invert = false } = args[0]);
|
|
887
|
+
} else {
|
|
888
|
+
// Legacy positional: matchImage(relativePath, action, invert)
|
|
889
|
+
[relativePath, action = 'click', invert = false] = args;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
emitter.emit(
|
|
893
|
+
events.log.narration,
|
|
894
|
+
theme.dim(`${action} on image template "${relativePath}"...`),
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
// Resolve the image path relative to the current file
|
|
898
|
+
const resolvedPath = resolveRelativePath(relativePath);
|
|
899
|
+
|
|
900
|
+
let image = await system.captureScreenPNG();
|
|
901
|
+
|
|
902
|
+
let result = await findImageOnScreen(resolvedPath, image);
|
|
903
|
+
|
|
904
|
+
if (invert) {
|
|
905
|
+
result = !result;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (!result) {
|
|
909
|
+
throw new CommandError(`Image not found: ${resolvedPath}`);
|
|
910
|
+
} else {
|
|
911
|
+
if (action === "click") {
|
|
912
|
+
await click({ x: result.centerX, y: result.centerY, action });
|
|
913
|
+
} else if (action === "hover") {
|
|
914
|
+
await hover({ x: result.centerX, y: result.centerY });
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return true;
|
|
919
|
+
},
|
|
920
|
+
/**
|
|
921
|
+
* Type text
|
|
922
|
+
* @param {string|number} text - Text to type
|
|
923
|
+
* @param {Object} [options] - Additional options
|
|
924
|
+
* @param {number} [options.delay=250] - Delay between keystrokes in milliseconds
|
|
925
|
+
* @param {boolean} [options.secret=false] - If true, text is treated as sensitive (not logged or stored)
|
|
926
|
+
* @param {Object} [options.redraw] - Redraw detection options
|
|
927
|
+
* @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
|
|
928
|
+
* @param {Object} [options.redraw.thresholds] - Threshold configuration
|
|
929
|
+
* @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
|
|
930
|
+
* @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
|
|
931
|
+
*/
|
|
932
|
+
"type": async (text, options = {}) => {
|
|
933
|
+
const { formatter } = require("../../sdk-log-formatter.js");
|
|
934
|
+
// Capture absolute timestamp at the very start of the command
|
|
935
|
+
// Frontend will calculate relative time using: timestamp - replay.clientStartDate
|
|
936
|
+
const typeTimestamp = Date.now();
|
|
937
|
+
const typeStartTime = typeTimestamp;
|
|
938
|
+
const { delay = 250, secret = false, redraw: redrawOpts, ...elementData } = options;
|
|
939
|
+
const redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...options });
|
|
940
|
+
|
|
941
|
+
// Validate that text parameter is provided
|
|
942
|
+
if (text === undefined || text === null) {
|
|
943
|
+
throw new CommandError("type() requires a text parameter. Received: " + text);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Log parent action with text
|
|
947
|
+
if (secret) {
|
|
948
|
+
emitter.emit(events.log.narration, formatter.getPrefix("type") + " " + theme.yellow.bold("Type") + " " + theme.dim(`secret "****"`));
|
|
949
|
+
} else {
|
|
950
|
+
emitter.emit(events.log.narration, formatter.getPrefix("type") + " " + theme.yellow.bold("Type") + " " + theme.cyan(`"${text}"`));
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
await redraw.start(redrawOptions);
|
|
954
|
+
|
|
955
|
+
text = text.toString();
|
|
956
|
+
|
|
957
|
+
// Add absolute timestamp for sandbox events
|
|
958
|
+
elementData.timestamp = Date.now();
|
|
959
|
+
|
|
960
|
+
// Actually type the text in the sandbox
|
|
961
|
+
await sandbox.send({ type: "write", text, delay, ...elementData });
|
|
962
|
+
|
|
963
|
+
// Update the action log with duration
|
|
964
|
+
const typeActionEndTime = Date.now();
|
|
965
|
+
emitter.emit(events.log.narration, formatter.formatTypeResult(text, secret, typeActionEndTime - typeStartTime), true);
|
|
966
|
+
|
|
967
|
+
// Track interaction (fire-and-forget)
|
|
968
|
+
const typeDuration = Date.now() - typeStartTime;
|
|
969
|
+
trackInteraction({
|
|
970
|
+
interactionType: "type",
|
|
971
|
+
// Store masked text if secret, otherwise store actual text
|
|
972
|
+
input: { text: secret ? "****" : text, delay },
|
|
973
|
+
timestamp: typeTimestamp,
|
|
974
|
+
duration: typeDuration,
|
|
975
|
+
success: true,
|
|
976
|
+
isSecret: secret, // Flag this interaction if it contains a secret
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const redrawStartTime = Date.now();
|
|
980
|
+
await redraw.wait(5000, redrawOptions);
|
|
981
|
+
const redrawDuration = Date.now() - redrawStartTime;
|
|
982
|
+
|
|
983
|
+
// Log nested redraw completion
|
|
984
|
+
emitter.emit(
|
|
985
|
+
events.log.narration,
|
|
986
|
+
formatter.formatRedrawComplete(redrawDuration),
|
|
987
|
+
true,
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
return;
|
|
991
|
+
},
|
|
992
|
+
/**
|
|
993
|
+
* Press keyboard keys
|
|
994
|
+
* @param {Array} keys - Array of keys to press
|
|
995
|
+
* @param {Object} [options] - Additional options
|
|
996
|
+
* @param {Object} [options.redraw] - Redraw detection options
|
|
997
|
+
* @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
|
|
998
|
+
* @param {Object} [options.redraw.thresholds] - Threshold configuration
|
|
999
|
+
* @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
|
|
1000
|
+
* @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
|
|
1001
|
+
*/
|
|
1002
|
+
"press-keys": async (keys, options = {}) => {
|
|
1003
|
+
const { formatter } = require("../../sdk-log-formatter.js");
|
|
1004
|
+
// Capture absolute timestamp at the very start of the command
|
|
1005
|
+
// Frontend will calculate relative time using: timestamp - replay.clientStartDate
|
|
1006
|
+
const pressKeysTimestamp = Date.now();
|
|
1007
|
+
const pressKeysStartTime = pressKeysTimestamp;
|
|
1008
|
+
const redrawOptions = extractRedrawOptions(options);
|
|
1009
|
+
const keysDisplay = Array.isArray(keys) ? keys.join(", ") : keys;
|
|
1010
|
+
|
|
1011
|
+
// Log parent action
|
|
1012
|
+
emitter.emit(
|
|
1013
|
+
events.log.narration,
|
|
1014
|
+
formatter.getPrefix("pressKeys") + " " + theme.yellow.bold("PressKeys") + " " + theme.cyan(`${keysDisplay}`),
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
await redraw.start(redrawOptions);
|
|
1018
|
+
|
|
1019
|
+
// Log nested action details
|
|
1020
|
+
const pressKeysActionLogStart = Date.now();
|
|
1021
|
+
|
|
1022
|
+
// finally, press the keys
|
|
1023
|
+
await sandbox.send({ type: "press", keys });
|
|
1024
|
+
|
|
1025
|
+
// Update the action log with duration
|
|
1026
|
+
const pressKeysActionEndTime = Date.now();
|
|
1027
|
+
emitter.emit(
|
|
1028
|
+
events.log.narration,
|
|
1029
|
+
formatter.formatPressKeysResult(keysDisplay, pressKeysActionEndTime - pressKeysActionLogStart),
|
|
1030
|
+
true,
|
|
1031
|
+
);
|
|
1032
|
+
|
|
1033
|
+
// Track interaction (fire-and-forget)
|
|
1034
|
+
const pressKeysDuration = Date.now() - pressKeysStartTime;
|
|
1035
|
+
trackInteraction({
|
|
1036
|
+
interactionType: "pressKeys",
|
|
1037
|
+
input: { keys },
|
|
1038
|
+
timestamp: pressKeysTimestamp,
|
|
1039
|
+
duration: pressKeysDuration,
|
|
1040
|
+
success: true,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
const redrawStartTime = Date.now();
|
|
1044
|
+
await redraw.wait(5000, redrawOptions);
|
|
1045
|
+
const redrawDuration = Date.now() - redrawStartTime;
|
|
1046
|
+
|
|
1047
|
+
// Log nested redraw completion
|
|
1048
|
+
emitter.emit(
|
|
1049
|
+
events.log.narration,
|
|
1050
|
+
formatter.formatRedrawComplete(redrawDuration),
|
|
1051
|
+
true,
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
return;
|
|
1055
|
+
},
|
|
1056
|
+
/**
|
|
1057
|
+
* Wait for specified time
|
|
1058
|
+
* @param {number} [timeout=3000] - Time to wait in milliseconds
|
|
1059
|
+
* @param {Object} [options] - Additional options (reserved for future use)
|
|
1060
|
+
*/
|
|
1061
|
+
"wait": async (timeout = 3000, options = {}) => {
|
|
1062
|
+
// Capture absolute timestamp at the very start of the command
|
|
1063
|
+
// Frontend will calculate relative time using: timestamp - replay.clientStartDate
|
|
1064
|
+
const waitTimestamp = Date.now();
|
|
1065
|
+
const waitStartTime = waitTimestamp;
|
|
1066
|
+
emitter.emit(events.log.narration, theme.dim(`waiting ${timeout}ms...`));
|
|
1067
|
+
const result = await delay(timeout);
|
|
1068
|
+
|
|
1069
|
+
// Track interaction (fire-and-forget)
|
|
1070
|
+
const waitDuration = Date.now() - waitStartTime;
|
|
1071
|
+
trackInteraction({
|
|
1072
|
+
interactionType: "wait",
|
|
1073
|
+
input: { timeout },
|
|
1074
|
+
timestamp: waitTimestamp,
|
|
1075
|
+
duration: waitDuration,
|
|
1076
|
+
success: true,
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
return result;
|
|
1080
|
+
},
|
|
1081
|
+
/**
|
|
1082
|
+
* Wait for image to appear on screen
|
|
1083
|
+
* @param {Object|string} options - Options object or description (for backward compatibility)
|
|
1084
|
+
* @param {string} options.description - Description of the image
|
|
1085
|
+
* @param {number} [options.timeout=10000] - Timeout in milliseconds
|
|
1086
|
+
*/
|
|
1087
|
+
"wait-for-image": async (...args) => {
|
|
1088
|
+
// Capture absolute timestamp at the very start of the command
|
|
1089
|
+
// Frontend will calculate relative time using: timestamp - replay.clientStartDate
|
|
1090
|
+
const waitForImageTimestamp = Date.now();
|
|
1091
|
+
let description, timeout;
|
|
1092
|
+
|
|
1093
|
+
// Handle both object and positional argument styles
|
|
1094
|
+
if (isObjectArgs(args, ['description', 'timeout'])) {
|
|
1095
|
+
({ description, timeout = 10000 } = args[0]);
|
|
1096
|
+
} else {
|
|
1097
|
+
// Legacy positional: waitForImage(description, timeout)
|
|
1098
|
+
[description, timeout = 10000] = args;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
emitter.emit(
|
|
1102
|
+
events.log.narration,
|
|
1103
|
+
theme.dim(
|
|
1104
|
+
`waiting for an image matching description "${description}"...`,
|
|
1105
|
+
),
|
|
1106
|
+
true,
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
let startTime = new Date().getTime();
|
|
1110
|
+
let durationPassed = 0;
|
|
1111
|
+
let passed = false;
|
|
1112
|
+
|
|
1113
|
+
while (durationPassed < timeout && !passed) {
|
|
1114
|
+
passed = await assert(
|
|
1115
|
+
`An image matching the description "${description}" appears on screen.`,
|
|
1116
|
+
false,
|
|
1117
|
+
);
|
|
1118
|
+
|
|
1119
|
+
durationPassed = new Date().getTime() - startTime;
|
|
1120
|
+
if (!passed) {
|
|
1121
|
+
emitter.emit(
|
|
1122
|
+
events.log.narration,
|
|
1123
|
+
theme.dim(
|
|
1124
|
+
`${niceSeconds(durationPassed)} seconds have passed without finding an image matching the description "${description}"`,
|
|
1125
|
+
),
|
|
1126
|
+
true,
|
|
1127
|
+
);
|
|
1128
|
+
await delay(2500);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (passed) {
|
|
1133
|
+
emitter.emit(
|
|
1134
|
+
events.log.narration,
|
|
1135
|
+
theme.dim(
|
|
1136
|
+
`An image matching the description \"${description}\" found!`,
|
|
1137
|
+
),
|
|
1138
|
+
true,
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
// Track interaction success (fire-and-forget)
|
|
1142
|
+
const waitForImageDuration = Date.now() - startTime;
|
|
1143
|
+
trackInteraction({
|
|
1144
|
+
interactionType: "waitForImage",
|
|
1145
|
+
prompt: description,
|
|
1146
|
+
input: { timeout },
|
|
1147
|
+
timestamp: waitForImageTimestamp,
|
|
1148
|
+
duration: waitForImageDuration,
|
|
1149
|
+
success: true,
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
return;
|
|
1153
|
+
} else {
|
|
1154
|
+
// Track interaction failure (fire-and-forget)
|
|
1155
|
+
const errorMsg = `Timed out (${niceSeconds(timeout)} seconds) while searching for an image matching the description \"${description}\"`;
|
|
1156
|
+
const waitForImageDuration = Date.now() - startTime;
|
|
1157
|
+
trackInteraction({
|
|
1158
|
+
interactionType: "waitForImage",
|
|
1159
|
+
prompt: description,
|
|
1160
|
+
input: { timeout },
|
|
1161
|
+
timestamp: waitForImageTimestamp,
|
|
1162
|
+
duration: waitForImageDuration,
|
|
1163
|
+
success: false,
|
|
1164
|
+
error: errorMsg,
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
throw new MatchError(errorMsg);
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
/**
|
|
1171
|
+
* Wait for text to appear on screen
|
|
1172
|
+
* @param {Object|string} options - Options object or text (for backward compatibility)
|
|
1173
|
+
* @param {string} options.text - Text to wait for
|
|
1174
|
+
* @param {number} [options.timeout=5000] - Timeout in milliseconds
|
|
1175
|
+
* @param {Object} [options.redraw] - Redraw detection options
|
|
1176
|
+
* @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
|
|
1177
|
+
* @param {Object} [options.redraw.thresholds] - Threshold configuration
|
|
1178
|
+
* @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
|
|
1179
|
+
* @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
|
|
1180
|
+
*/
|
|
1181
|
+
"wait-for-text": async (...args) => {
|
|
1182
|
+
// Capture absolute timestamp at the very start of the command
|
|
1183
|
+
// Frontend will calculate relative time using: timestamp - replay.clientStartDate
|
|
1184
|
+
const waitForTextTimestamp = Date.now();
|
|
1185
|
+
let text, timeout, redrawOptions;
|
|
1186
|
+
|
|
1187
|
+
// Handle both object and positional argument styles
|
|
1188
|
+
if (isObjectArgs(args, ['text', 'timeout'])) {
|
|
1189
|
+
const { redraw: redrawOpts, ...rest } = args[0];
|
|
1190
|
+
({ text, timeout = 5000 } = rest);
|
|
1191
|
+
redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
|
|
1192
|
+
} else {
|
|
1193
|
+
// Legacy positional: waitForText(text, timeout)
|
|
1194
|
+
[text, timeout = 5000] = args;
|
|
1195
|
+
redrawOptions = {};
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
await redraw.start(redrawOptions);
|
|
1199
|
+
|
|
1200
|
+
emitter.emit(
|
|
1201
|
+
events.log.narration,
|
|
1202
|
+
theme.dim(`waiting for text: "${text}"...`),
|
|
1203
|
+
true,
|
|
1204
|
+
);
|
|
1205
|
+
|
|
1206
|
+
let startTime = new Date().getTime();
|
|
1207
|
+
let durationPassed = 0;
|
|
1208
|
+
|
|
1209
|
+
let passed = false;
|
|
1210
|
+
|
|
1211
|
+
while (durationPassed < timeout && !passed) {
|
|
1212
|
+
const response = await sdk.req("find", {
|
|
1213
|
+
element: text,
|
|
1214
|
+
image: await system.captureScreenBase64(),
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
passed = !!(response && response.coordinates);
|
|
1218
|
+
|
|
1219
|
+
durationPassed = new Date().getTime() - startTime;
|
|
1220
|
+
|
|
1221
|
+
if (!passed) {
|
|
1222
|
+
emitter.emit(
|
|
1223
|
+
events.log.narration,
|
|
1224
|
+
theme.dim(
|
|
1225
|
+
`${niceSeconds(durationPassed)} seconds have passed without finding "${text}"`,
|
|
1226
|
+
),
|
|
1227
|
+
true,
|
|
1228
|
+
);
|
|
1229
|
+
await delay(2500);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (passed) {
|
|
1234
|
+
emitter.emit(events.log.narration, theme.dim(`"${text}" found!`), true);
|
|
1235
|
+
|
|
1236
|
+
// Track interaction success (fire-and-forget)
|
|
1237
|
+
const waitForTextDuration = Date.now() - startTime;
|
|
1238
|
+
trackInteraction({
|
|
1239
|
+
interactionType: "waitForText",
|
|
1240
|
+
prompt: text,
|
|
1241
|
+
input: { timeout },
|
|
1242
|
+
timestamp: waitForTextTimestamp,
|
|
1243
|
+
duration: waitForTextDuration,
|
|
1244
|
+
success: true,
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
return;
|
|
1248
|
+
} else {
|
|
1249
|
+
// Track interaction failure (fire-and-forget)
|
|
1250
|
+
const errorMsg = `Timed out (${niceSeconds(timeout)} seconds) while searching for "${text}"`;
|
|
1251
|
+
const waitForTextDuration = Date.now() - startTime;
|
|
1252
|
+
trackInteraction({
|
|
1253
|
+
interactionType: "waitForText",
|
|
1254
|
+
prompt: text,
|
|
1255
|
+
input: { timeout },
|
|
1256
|
+
timestamp: waitForTextTimestamp,
|
|
1257
|
+
duration: waitForTextDuration,
|
|
1258
|
+
success: false,
|
|
1259
|
+
error: errorMsg,
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
throw new MatchError(errorMsg);
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
/**
|
|
1266
|
+
* Scroll until text is found
|
|
1267
|
+
* @param {Object|string} options - Options object or text (for backward compatibility)
|
|
1268
|
+
* @param {string} options.text - Text to find
|
|
1269
|
+
* @param {string} [options.direction='down'] - Scroll direction
|
|
1270
|
+
* @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
|
|
1271
|
+
* @param {boolean} [options.invert=false] - Invert the match
|
|
1272
|
+
* @param {Object} [options.redraw] - Redraw detection options
|
|
1273
|
+
* @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
|
|
1274
|
+
* @param {Object} [options.redraw.thresholds] - Threshold configuration
|
|
1275
|
+
* @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
|
|
1276
|
+
* @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
|
|
1277
|
+
*/
|
|
1278
|
+
"scroll-until-text": async (...args) => {
|
|
1279
|
+
let text, direction, maxDistance, invert, redrawOptions;
|
|
1280
|
+
|
|
1281
|
+
// Handle both object and positional argument styles
|
|
1282
|
+
if (isObjectArgs(args, ['text', 'direction', 'maxDistance', 'invert'])) {
|
|
1283
|
+
const { redraw: redrawOpts, ...rest } = args[0];
|
|
1284
|
+
({ text, direction = 'down', maxDistance = 10000, invert = false } = rest);
|
|
1285
|
+
redrawOptions = extractRedrawOptions({ redraw: redrawOpts, ...rest });
|
|
1286
|
+
} else {
|
|
1287
|
+
// Legacy positional: scrollUntilText(text, direction, maxDistance, invert)
|
|
1288
|
+
[text, direction = 'down', maxDistance = 10000, invert = false] = args;
|
|
1289
|
+
redrawOptions = {};
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
await redraw.start(redrawOptions);
|
|
1293
|
+
|
|
1294
|
+
emitter.emit(
|
|
1295
|
+
events.log.narration,
|
|
1296
|
+
theme.dim(`scrolling for text: "${text}"...`),
|
|
1297
|
+
true,
|
|
1298
|
+
);
|
|
1299
|
+
|
|
1300
|
+
let scrollDistance = 0;
|
|
1301
|
+
let incrementDistance = 500;
|
|
1302
|
+
let passed = false;
|
|
1303
|
+
|
|
1304
|
+
while (scrollDistance <= maxDistance && !passed) {
|
|
1305
|
+
const response = await sdk.req("find", {
|
|
1306
|
+
element: text,
|
|
1307
|
+
image: await system.captureScreenBase64(),
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
passed = !!(response && response.coordinates);
|
|
1311
|
+
|
|
1312
|
+
if (invert) {
|
|
1313
|
+
passed = !passed;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (!passed) {
|
|
1317
|
+
emitter.emit(
|
|
1318
|
+
events.log.narration,
|
|
1319
|
+
theme.dim(
|
|
1320
|
+
`scrolling ${direction} ${incrementDistance}px. ${scrollDistance + incrementDistance}/${maxDistance}px scrolled...`,
|
|
1321
|
+
),
|
|
1322
|
+
true,
|
|
1323
|
+
);
|
|
1324
|
+
await scroll(direction, { amount: incrementDistance });
|
|
1325
|
+
scrollDistance = scrollDistance + incrementDistance;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (passed) {
|
|
1330
|
+
emitter.emit(events.log.narration, theme.dim(`"${text}" found!`), true);
|
|
1331
|
+
return;
|
|
1332
|
+
} else {
|
|
1333
|
+
throw new MatchError(
|
|
1334
|
+
`Scrolled ${scrollDistance} pixels without finding "${text}"`,
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
},
|
|
1338
|
+
/**
|
|
1339
|
+
* Scroll until image is found
|
|
1340
|
+
* @param {Object|string} options - Options object or description (for backward compatibility)
|
|
1341
|
+
* @param {string} [options.description] - Description of the image
|
|
1342
|
+
* @param {string} [options.direction='down'] - Scroll direction
|
|
1343
|
+
* @param {number} [options.maxDistance=10000] - Maximum distance to scroll in pixels
|
|
1344
|
+
* @param {string} [options.method='mouse'] - Scroll method
|
|
1345
|
+
* @param {string} [options.path] - Path to image template
|
|
1346
|
+
* @param {boolean} [options.invert=false] - Invert the match
|
|
1347
|
+
*/
|
|
1348
|
+
"scroll-until-image": async (...args) => {
|
|
1349
|
+
let description, direction, maxDistance, method, imagePath, invert;
|
|
1350
|
+
|
|
1351
|
+
// Handle both object and positional argument styles
|
|
1352
|
+
if (isObjectArgs(args, ['description', 'direction', 'maxDistance', 'method', 'path', 'invert'])) {
|
|
1353
|
+
({ description, direction = 'down', maxDistance = 10000, method = 'mouse', path: imagePath, invert = false } = args[0]);
|
|
1354
|
+
} else {
|
|
1355
|
+
// Legacy positional: scrollUntilImage(description, direction, maxDistance, method, path, invert)
|
|
1356
|
+
[description, direction = 'down', maxDistance = 10000, method = 'mouse', imagePath, invert = false] = args;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const needle = description || imagePath;
|
|
1360
|
+
|
|
1361
|
+
if (!needle) {
|
|
1362
|
+
throw new CommandError("No description or path provided");
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
if (description && imagePath) {
|
|
1366
|
+
throw new CommandError(
|
|
1367
|
+
"Only one of description or path can be provided",
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
emitter.emit(
|
|
1372
|
+
events.log.narration,
|
|
1373
|
+
theme.dim(`scrolling for an image matching "${needle}"...`),
|
|
1374
|
+
true,
|
|
1375
|
+
);
|
|
1376
|
+
|
|
1377
|
+
let scrollDistance = 0;
|
|
1378
|
+
let incrementDistance = 500;
|
|
1379
|
+
let passed = false;
|
|
1380
|
+
|
|
1381
|
+
while (scrollDistance <= maxDistance && !passed) {
|
|
1382
|
+
if (description) {
|
|
1383
|
+
passed = await assert(
|
|
1384
|
+
`An image matching the description "${description}" appears on screen.`,
|
|
1385
|
+
false,
|
|
1386
|
+
false,
|
|
1387
|
+
invert,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
if (imagePath) {
|
|
1392
|
+
// Don't throw if not found. We only want to know if it's found or not.
|
|
1393
|
+
passed = await commands["match-image"]({ path: imagePath }).catch(
|
|
1394
|
+
console.warn,
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (!passed) {
|
|
1399
|
+
emitter.emit(
|
|
1400
|
+
events.log.narration,
|
|
1401
|
+
theme.dim(`scrolling ${direction} ${incrementDistance} pixels...`),
|
|
1402
|
+
true,
|
|
1403
|
+
);
|
|
1404
|
+
await scroll(direction, { amount: incrementDistance });
|
|
1405
|
+
scrollDistance = scrollDistance + incrementDistance;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (passed) {
|
|
1410
|
+
emitter.emit(
|
|
1411
|
+
events.log.narration,
|
|
1412
|
+
theme.dim(`"${needle}" found!`),
|
|
1413
|
+
true,
|
|
1414
|
+
);
|
|
1415
|
+
return;
|
|
1416
|
+
} else {
|
|
1417
|
+
throw new CommandError(
|
|
1418
|
+
`Scrolled ${scrollDistance} pixels without finding an image matching "${needle}"`,
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
1422
|
+
/**
|
|
1423
|
+
* Focus an application by name
|
|
1424
|
+
* @param {string} name - Application name
|
|
1425
|
+
* @param {Object} [options] - Additional options
|
|
1426
|
+
* @param {Object} [options.redraw] - Redraw detection options
|
|
1427
|
+
* @param {boolean} [options.redraw.enabled=true] - Enable/disable redraw detection
|
|
1428
|
+
* @param {Object} [options.redraw.thresholds] - Threshold configuration
|
|
1429
|
+
* @param {number|boolean} [options.redraw.thresholds.screen=0.05] - Screen diff threshold (false to disable)
|
|
1430
|
+
* @param {boolean} [options.redraw.thresholds.network=false] - Enable/disable network monitoring
|
|
1431
|
+
*/
|
|
1432
|
+
"focus-application": async (name, options = {}) => {
|
|
1433
|
+
const redrawOptions = extractRedrawOptions(options);
|
|
1434
|
+
await redraw.start(redrawOptions);
|
|
1435
|
+
|
|
1436
|
+
await sandbox.send({
|
|
1437
|
+
type: "commands.focus-application",
|
|
1438
|
+
name,
|
|
1439
|
+
});
|
|
1440
|
+
await redraw.wait(1000, redrawOptions);
|
|
1441
|
+
return "The application was focused.";
|
|
1442
|
+
},
|
|
1443
|
+
/**
|
|
1444
|
+
* Extract information from the screen using AI
|
|
1445
|
+
* @param {Object|string} options - Options object or description (for backward compatibility)
|
|
1446
|
+
* @param {string} options.description - What to extract
|
|
1447
|
+
*/
|
|
1448
|
+
"extract": async (...args) => {
|
|
1449
|
+
// Capture absolute timestamp at the very start of the command
|
|
1450
|
+
// Frontend will calculate relative time using: timestamp - replay.clientStartDate
|
|
1451
|
+
const rememberTimestamp = Date.now();
|
|
1452
|
+
const rememberStartTime = rememberTimestamp;
|
|
1453
|
+
let description;
|
|
1454
|
+
|
|
1455
|
+
// Handle both object and positional argument styles
|
|
1456
|
+
if (isObjectArgs(args, ['description'])) {
|
|
1457
|
+
({ description } = args[0]);
|
|
1458
|
+
} else {
|
|
1459
|
+
// Legacy positional: remember(description)
|
|
1460
|
+
[description] = args;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
try {
|
|
1464
|
+
let result = await sdk.req("remember", {
|
|
1465
|
+
image: await system.captureScreenBase64(),
|
|
1466
|
+
description,
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// Track interaction success
|
|
1470
|
+
const rememberDuration = Date.now() - rememberStartTime;
|
|
1471
|
+
trackInteraction({
|
|
1472
|
+
interactionType: "extract",
|
|
1473
|
+
prompt: description,
|
|
1474
|
+
timestamp: rememberTimestamp,
|
|
1475
|
+
duration: rememberDuration,
|
|
1476
|
+
success: true,
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
return result.data;
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
// Track interaction failure
|
|
1482
|
+
const rememberDuration = Date.now() - rememberStartTime;
|
|
1483
|
+
trackInteraction({
|
|
1484
|
+
interactionType: "extract",
|
|
1485
|
+
prompt: description,
|
|
1486
|
+
timestamp: rememberTimestamp,
|
|
1487
|
+
duration: rememberDuration,
|
|
1488
|
+
success: false,
|
|
1489
|
+
error: error.message,
|
|
1490
|
+
});
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
/**
|
|
1495
|
+
* Make an AI-powered assertion
|
|
1496
|
+
* @param {string} assertion - Assertion to check
|
|
1497
|
+
* @param {Object} [options] - Additional options
|
|
1498
|
+
* @param {number} [options.threshold] - Cache threshold (0-1). Lower values require closer matches. Set to -1 to disable cache.
|
|
1499
|
+
* @param {string} [options.cacheKey] - Cache key for grouping cached assertions (enables caching when provided)
|
|
1500
|
+
* @param {string} [options.os] - Operating system identifier for cache partitioning
|
|
1501
|
+
* @param {string} [options.resolution] - Screen resolution for cache partitioning
|
|
1502
|
+
*/
|
|
1503
|
+
"assert": async (assertion, options = {}) => {
|
|
1504
|
+
// In soft assert mode (during act()), don't throw on failure
|
|
1505
|
+
const shouldThrow = !getSoftAssertMode();
|
|
1506
|
+
let response = await assert(assertion, shouldThrow, options);
|
|
1507
|
+
|
|
1508
|
+
return response;
|
|
1509
|
+
},
|
|
1510
|
+
/**
|
|
1511
|
+
* Execute code in the sandbox
|
|
1512
|
+
* @param {Object|string} options - Options object or language (for backward compatibility)
|
|
1513
|
+
* @param {string} [options.language='pwsh'] - Language ('js', 'pwsh', or 'sh')
|
|
1514
|
+
* @param {string} options.code - Code to execute
|
|
1515
|
+
* @param {number} [options.timeout] - Timeout in milliseconds
|
|
1516
|
+
* @param {boolean} [options.silent=false] - Suppress output
|
|
1517
|
+
*/
|
|
1518
|
+
"exec": async (...args) => {
|
|
1519
|
+
const { formatter } = require("../../sdk-log-formatter.js");
|
|
1520
|
+
let language, code, timeout, silent;
|
|
1521
|
+
|
|
1522
|
+
// Handle both object and positional argument styles
|
|
1523
|
+
if (isObjectArgs(args, ['language', 'code', 'timeout', 'silent'])) {
|
|
1524
|
+
({ language = 'pwsh', code, timeout, silent = false } = args[0]);
|
|
1525
|
+
} else {
|
|
1526
|
+
// Legacy positional: exec(language, code, timeout, silent)
|
|
1527
|
+
[language = 'pwsh', code, timeout, silent = false] = args;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// Log parent action
|
|
1531
|
+
emitter.emit(events.log.narration, formatter.getPrefix("action") + " " + theme.cyan.bold("Exec") + " " + theme.magenta(`[${language}]`), true);
|
|
1532
|
+
|
|
1533
|
+
// Log nested command details (truncate to first line)
|
|
1534
|
+
const firstLine = code.split('\n')[0];
|
|
1535
|
+
const codeDisplay = code.includes('\n') ? firstLine + '...' : firstLine;
|
|
1536
|
+
emitter.emit(events.log.log, formatter.formatCodeLine(codeDisplay));
|
|
1537
|
+
|
|
1538
|
+
let plat = system.platform();
|
|
1539
|
+
|
|
1540
|
+
if (language == "pwsh" || language == "sh") {
|
|
1541
|
+
if (language === "pwsh" && sandbox.os === "linux") {
|
|
1542
|
+
emitter.emit(
|
|
1543
|
+
events.log.log,
|
|
1544
|
+
theme.yellow(
|
|
1545
|
+
`⚠️ Warning: You are using 'pwsh' exec command on a Linux sandbox. This may fail. Consider using 'bash' or 'sh' for Linux environments.`,
|
|
1546
|
+
),
|
|
1547
|
+
true,
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
if (language === "sh" && sandbox.os === "windows") {
|
|
1552
|
+
emitter.emit(
|
|
1553
|
+
events.log.log,
|
|
1554
|
+
theme.yellow(
|
|
1555
|
+
`⚠️ Warning: You are using 'sh' exec command on a Windows sandbox. This will fail. Automatically switching to 'pwsh' for Windows environments.`,
|
|
1556
|
+
),
|
|
1557
|
+
true,
|
|
1558
|
+
);
|
|
1559
|
+
// Automatically switch to pwsh for Windows
|
|
1560
|
+
language = "pwsh";
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const execActionLogStart = Date.now();
|
|
1564
|
+
|
|
1565
|
+
let result = null;
|
|
1566
|
+
|
|
1567
|
+
const execTimeout = timeout || 300000;
|
|
1568
|
+
result = await sandbox.send({
|
|
1569
|
+
type: "commands.run",
|
|
1570
|
+
command: code,
|
|
1571
|
+
timeout: execTimeout,
|
|
1572
|
+
}, execTimeout);
|
|
1573
|
+
|
|
1574
|
+
const execActionEndTime = Date.now();
|
|
1575
|
+
const execDuration = execActionEndTime - execActionLogStart;
|
|
1576
|
+
|
|
1577
|
+
// const debugMode = process.env.VERBOSE || process.env.TD_DEBUG;
|
|
1578
|
+
// if (debugMode) {
|
|
1579
|
+
// console.log(result);
|
|
1580
|
+
// }
|
|
1581
|
+
|
|
1582
|
+
if (result.out && result.out.returncode !== 0) {
|
|
1583
|
+
emitter.emit(
|
|
1584
|
+
events.log.narration,
|
|
1585
|
+
formatter.formatExecComplete(result.out.returncode, execDuration),
|
|
1586
|
+
true,
|
|
1587
|
+
);
|
|
1588
|
+
throw new MatchError(
|
|
1589
|
+
`Command failed with exit code ${result.out.returncode}: ${result.out.stderr}`,
|
|
1590
|
+
);
|
|
1591
|
+
} else {
|
|
1592
|
+
emitter.emit(
|
|
1593
|
+
events.log.narration,
|
|
1594
|
+
formatter.formatExecComplete(0, execDuration),
|
|
1595
|
+
true,
|
|
1596
|
+
);
|
|
1597
|
+
|
|
1598
|
+
if (!silent && result.out?.stdout) {
|
|
1599
|
+
emitter.emit(events.log.log, theme.dim(` stdout:`), true);
|
|
1600
|
+
emitter.emit(events.log.log, theme.dim(` ${result.out.stdout}`), true);
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (!silent && result.out.stderr) {
|
|
1604
|
+
emitter.emit(events.log.log, theme.dim(` stderr:`), true);
|
|
1605
|
+
emitter.emit(events.log.log, theme.dim(` ${result.out.stderr}`), true);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
return result.out?.stdout?.trim();
|
|
1609
|
+
}
|
|
1610
|
+
} else if (language == "js") {
|
|
1611
|
+
emitter.emit(events.log.narration, theme.dim(`running js...`), true);
|
|
1612
|
+
|
|
1613
|
+
emitter.emit(
|
|
1614
|
+
events.log.narration,
|
|
1615
|
+
theme.dim(`running value of \`${plat}\` in local JS vm...`),
|
|
1616
|
+
true,
|
|
1617
|
+
);
|
|
1618
|
+
|
|
1619
|
+
emitter.emit(events.log.log, "");
|
|
1620
|
+
emitter.emit(events.log.log, "------");
|
|
1621
|
+
|
|
1622
|
+
const context = vm.createContext({
|
|
1623
|
+
require,
|
|
1624
|
+
console,
|
|
1625
|
+
fs,
|
|
1626
|
+
process,
|
|
1627
|
+
fetch,
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
let scriptCode = "(async function() {\n" + code + "\n})();";
|
|
1631
|
+
|
|
1632
|
+
const script = new vm.Script(scriptCode);
|
|
1633
|
+
|
|
1634
|
+
try {
|
|
1635
|
+
await script.runInNewContext(context);
|
|
1636
|
+
} catch (e) {
|
|
1637
|
+
// Log the error to the emitter instead of console.error to maintain consistency
|
|
1638
|
+
emitter.emit(
|
|
1639
|
+
events.log.debug,
|
|
1640
|
+
`JavaScript execution error: ${e.message}`,
|
|
1641
|
+
);
|
|
1642
|
+
// Wait a tick to allow any promise rejections to be handled
|
|
1643
|
+
throw new CommandError(`Error running script: ${e.message}`);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// wait for context.result to resolve
|
|
1647
|
+
let stepResult = await context.result;
|
|
1648
|
+
|
|
1649
|
+
// conver it to string
|
|
1650
|
+
if (typeof stepResult === "object") {
|
|
1651
|
+
stepResult = JSON.stringify(stepResult, null, 2);
|
|
1652
|
+
} else if (typeof stepResult === "function") {
|
|
1653
|
+
stepResult = stepResult.toString();
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
emitter.emit(events.log.log, "------");
|
|
1657
|
+
emitter.emit(events.log.log, "");
|
|
1658
|
+
|
|
1659
|
+
if (!stepResult) {
|
|
1660
|
+
emitter.emit(events.log.log, `No result returned from script`, true);
|
|
1661
|
+
} else {
|
|
1662
|
+
/* The above JavaScript code is checking if the variable `silent` is falsy (not true) and if
|
|
1663
|
+
so, it emits log events using an emitter. The emitted log events include the
|
|
1664
|
+
theme.dim(`Result:`) and the value of the `stepResult` variable. */
|
|
1665
|
+
// if (!silent) {
|
|
1666
|
+
// emitter.emit(events.log.log, theme.dim(`Result:`), true);
|
|
1667
|
+
// emitter.emit(events.log.log, stepResult, true);
|
|
1668
|
+
// }
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
return stepResult;
|
|
1672
|
+
// }
|
|
1673
|
+
} else {
|
|
1674
|
+
throw new CommandError(`Language not supported: ${language}`);
|
|
1675
|
+
}
|
|
1676
|
+
},
|
|
1677
|
+
};
|
|
1678
|
+
|
|
1679
|
+
// Return the commands, assert function, and redraw instance
|
|
1680
|
+
return { commands, assert, redraw };
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
// Export the factory function
|
|
1684
|
+
module.exports = { createCommands };
|