@testdriverai/agent 7.8.0-canary.10
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
package/agent/lib/sdk.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
const { events } = require("../events");
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
|
|
4
|
+
// get the version from package.json
|
|
5
|
+
const { version } = require("../../package.json");
|
|
6
|
+
const axios = require("axios");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default retry configuration
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
12
|
+
maxRetries: 10,
|
|
13
|
+
baseDelayMs: 3000,
|
|
14
|
+
maxDelayMs: 30000,
|
|
15
|
+
// Error codes that should trigger a retry
|
|
16
|
+
retryableNetworkCodes: [
|
|
17
|
+
'ECONNRESET',
|
|
18
|
+
'ECONNREFUSED',
|
|
19
|
+
'ETIMEDOUT',
|
|
20
|
+
'ENOTFOUND',
|
|
21
|
+
'ENETUNREACH',
|
|
22
|
+
'ERR_NETWORK',
|
|
23
|
+
'ECONNABORTED',
|
|
24
|
+
'EPIPE',
|
|
25
|
+
'EAI_AGAIN',
|
|
26
|
+
],
|
|
27
|
+
// HTTP status codes that should trigger a retry
|
|
28
|
+
retryableStatusCodes: [429, 500, 502, 503, 504],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Determines if an error is retryable
|
|
33
|
+
* @param {Error} error - The axios error
|
|
34
|
+
* @param {Object} config - Retry configuration
|
|
35
|
+
* @returns {boolean} Whether the request should be retried
|
|
36
|
+
*/
|
|
37
|
+
function isRetryableError(error, config = DEFAULT_RETRY_CONFIG) {
|
|
38
|
+
// Network-level errors (no response received)
|
|
39
|
+
if (!error.response) {
|
|
40
|
+
return config.retryableNetworkCodes.includes(error.code);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// HTTP status code based retries
|
|
44
|
+
const status = error.response?.status;
|
|
45
|
+
return config.retryableStatusCodes.includes(status);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate delay for next retry using exponential backoff with jitter
|
|
50
|
+
* @param {number} attempt - Current attempt number (0-indexed)
|
|
51
|
+
* @param {Error} error - The error that triggered the retry
|
|
52
|
+
* @param {Object} config - Retry configuration
|
|
53
|
+
* @returns {number} Delay in milliseconds
|
|
54
|
+
*/
|
|
55
|
+
function calculateRetryDelay(attempt, error, config = DEFAULT_RETRY_CONFIG) {
|
|
56
|
+
// Respect Retry-After header for rate limiting
|
|
57
|
+
if (error.response?.status === 429) {
|
|
58
|
+
const retryAfter = error.response.headers?.['retry-after'];
|
|
59
|
+
if (retryAfter) {
|
|
60
|
+
const retryAfterMs = parseInt(retryAfter, 10) * 1000;
|
|
61
|
+
if (!isNaN(retryAfterMs) && retryAfterMs > 0) {
|
|
62
|
+
return Math.min(retryAfterMs, config.maxDelayMs);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Exponential backoff: baseDelay * 2^attempt + random jitter
|
|
68
|
+
const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt);
|
|
69
|
+
const jitter = Math.random() * config.baseDelayMs * 0.5;
|
|
70
|
+
return Math.min(exponentialDelay + jitter, config.maxDelayMs);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sleep for a specified duration
|
|
75
|
+
* @param {number} ms - Milliseconds to sleep
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Execute an async function with retry logic
|
|
82
|
+
* @param {Function} fn - Async function to execute
|
|
83
|
+
* @param {Object} options - Options
|
|
84
|
+
* @param {Object} options.retryConfig - Retry configuration (uses defaults if not provided)
|
|
85
|
+
* @param {Function} options.onRetry - Callback called before each retry (attempt, error, delayMs)
|
|
86
|
+
* @returns {Promise<*>} Result of the function
|
|
87
|
+
*/
|
|
88
|
+
async function withRetry(fn, options = {}) {
|
|
89
|
+
const config = { ...DEFAULT_RETRY_CONFIG, ...options.retryConfig };
|
|
90
|
+
let lastError;
|
|
91
|
+
|
|
92
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
93
|
+
try {
|
|
94
|
+
return await fn();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
lastError = error;
|
|
97
|
+
|
|
98
|
+
// Don't retry if we've exhausted attempts or error isn't retryable
|
|
99
|
+
if (attempt >= config.maxRetries || !isRetryableError(error, config)) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const delayMs = calculateRetryDelay(attempt, error, config);
|
|
104
|
+
|
|
105
|
+
// Call onRetry callback if provided
|
|
106
|
+
if (options.onRetry) {
|
|
107
|
+
options.onRetry(attempt + 1, error, delayMs);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await sleep(delayMs);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw lastError;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate Sentry trace headers for distributed tracing
|
|
119
|
+
* Uses the same trace ID derivation as the API (MD5 hash of session ID)
|
|
120
|
+
* @param {string} sessionId - The session ID
|
|
121
|
+
* @returns {Object} Headers object with sentry-trace and baggage
|
|
122
|
+
*/
|
|
123
|
+
function getSentryTraceHeaders(sessionId) {
|
|
124
|
+
if (!sessionId) return {};
|
|
125
|
+
|
|
126
|
+
// Same logic as API: derive trace ID from session ID
|
|
127
|
+
const traceId = crypto.createHash('md5').update(sessionId).digest('hex');
|
|
128
|
+
const spanId = crypto.randomBytes(8).toString('hex');
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
'sentry-trace': `${traceId}-${spanId}-1`,
|
|
132
|
+
'baggage': `sentry-trace_id=${traceId},sentry-sample_rate=1.0,sentry-sampled=true`
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Factory function that creates SDK with the provided emitter, config, and session
|
|
137
|
+
let token = null;
|
|
138
|
+
const createSDK = (emitter, config, sessionInstance) => {
|
|
139
|
+
// Config is required - no fallback to avoid process.env usage
|
|
140
|
+
if (!config) {
|
|
141
|
+
throw new Error("Config must be provided to createSDK");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Session is required
|
|
145
|
+
if (!sessionInstance) {
|
|
146
|
+
throw new Error("Session instance must be provided to createSDK");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const outputError = (error) => {
|
|
150
|
+
emitter.emit(events.error.sdk, {
|
|
151
|
+
message: error.status || error.reason || error.message,
|
|
152
|
+
code: error.response?.data?.raw || error.statusText || error.code,
|
|
153
|
+
fullError: error,
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const parseBody = async (response, body) => {
|
|
158
|
+
const contentType = response.headers.get("Content-Type")?.toLowerCase();
|
|
159
|
+
try {
|
|
160
|
+
if (body === null || body === undefined) {
|
|
161
|
+
if (!contentType.includes("json") && !contentType.includes("text")) {
|
|
162
|
+
return await response.arrayBuffer();
|
|
163
|
+
}
|
|
164
|
+
body = response.data;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (typeof body === "string") {
|
|
168
|
+
if (contentType.includes("jsonl")) {
|
|
169
|
+
const result = body
|
|
170
|
+
.split("\n")
|
|
171
|
+
.filter((line) => line.trim().length)
|
|
172
|
+
.map((line) => JSON.parse(line))
|
|
173
|
+
.reduce((result, { type, data }) => {
|
|
174
|
+
if (result[type]) {
|
|
175
|
+
if (typeof result[type] === "string") {
|
|
176
|
+
result[type] += data;
|
|
177
|
+
} else {
|
|
178
|
+
result[type].push(data);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
result[type] = typeof data === "string" ? data : [data];
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}, {});
|
|
185
|
+
for (const key of Object.keys(result)) {
|
|
186
|
+
if (Array.isArray(result[key]) && result[key].length === 1) {
|
|
187
|
+
result[key] = result[key][0];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
if (contentType.includes("json")) {
|
|
193
|
+
return JSON.parse(body);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return body;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
emitter.emit(events.error.sdk, {
|
|
199
|
+
error: err,
|
|
200
|
+
message: "Parsing Error",
|
|
201
|
+
});
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const auth = async () => {
|
|
207
|
+
if (!config["TD_API_KEY"]) {
|
|
208
|
+
const error = new Error(
|
|
209
|
+
"TD_API_KEY is not configured. Get your API key at https://console.testdriver.ai/team"
|
|
210
|
+
);
|
|
211
|
+
error.code = "MISSING_API_KEY";
|
|
212
|
+
error.isAuthError = true;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const url = [config["TD_API_ROOT"], "auth/exchange-api-key"].join("/");
|
|
217
|
+
const c = {
|
|
218
|
+
method: "post",
|
|
219
|
+
headers: {
|
|
220
|
+
"Content-Type": "application/json",
|
|
221
|
+
"User-Agent": `TestDriverSDK/${version} (Node.js ${process.version})`,
|
|
222
|
+
},
|
|
223
|
+
timeout: 15000, // 15 second timeout for auth requests
|
|
224
|
+
data: {
|
|
225
|
+
apiKey: config["TD_API_KEY"],
|
|
226
|
+
version,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
let res = await withRetry(
|
|
232
|
+
() => axios(url, c),
|
|
233
|
+
{
|
|
234
|
+
retryConfig: { maxRetries: 2 },
|
|
235
|
+
onRetry: (attempt, error, delayMs) => {
|
|
236
|
+
emitter.emit(events.sdk.retry, {
|
|
237
|
+
path: 'auth/exchange-api-key',
|
|
238
|
+
attempt,
|
|
239
|
+
error: error.message || error.code,
|
|
240
|
+
delayMs,
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
token = res.data.token;
|
|
247
|
+
return token;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// Classify the error for better user feedback
|
|
250
|
+
const classifiedError = classifyAuthError(error, config["TD_API_ROOT"]);
|
|
251
|
+
outputError(classifiedError);
|
|
252
|
+
throw classifiedError;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Classify authentication errors into user-friendly categories
|
|
258
|
+
* @param {Error} error - The original axios error
|
|
259
|
+
* @param {string} apiRoot - The API root URL for context
|
|
260
|
+
* @returns {Error} A classified error with code and helpful message
|
|
261
|
+
*/
|
|
262
|
+
function classifyAuthError(error, apiRoot) {
|
|
263
|
+
const status = error.response?.status;
|
|
264
|
+
const data = error.response?.data;
|
|
265
|
+
|
|
266
|
+
// Check for network-level errors (no response received)
|
|
267
|
+
if (!error.response) {
|
|
268
|
+
const networkError = new Error(
|
|
269
|
+
`Unable to reach TestDriver API at ${apiRoot}. ` +
|
|
270
|
+
getNetworkErrorHint(error.code)
|
|
271
|
+
);
|
|
272
|
+
networkError.code = "NETWORK_ERROR";
|
|
273
|
+
networkError.isNetworkError = true;
|
|
274
|
+
networkError.originalError = error;
|
|
275
|
+
return networkError;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Invalid API key (401)
|
|
279
|
+
if (status === 401) {
|
|
280
|
+
const authError = new Error(
|
|
281
|
+
data?.message ||
|
|
282
|
+
"Invalid API key. Please check your TD_API_KEY and try again. " +
|
|
283
|
+
"Get your API key at https://console.testdriver.ai/team"
|
|
284
|
+
);
|
|
285
|
+
authError.code = data?.error || "INVALID_API_KEY";
|
|
286
|
+
authError.isAuthError = true;
|
|
287
|
+
authError.originalError = error;
|
|
288
|
+
return authError;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Server errors (5xx) - API is down or having issues
|
|
292
|
+
if (status >= 500) {
|
|
293
|
+
const serverError = new Error(
|
|
294
|
+
data?.message ||
|
|
295
|
+
`TestDriver API is currently unavailable (HTTP ${status}). Please try again later.`
|
|
296
|
+
);
|
|
297
|
+
serverError.code = data?.error || "API_UNAVAILABLE";
|
|
298
|
+
serverError.isServerError = true;
|
|
299
|
+
serverError.originalError = error;
|
|
300
|
+
return serverError;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Rate limiting (429)
|
|
304
|
+
if (status === 429) {
|
|
305
|
+
const rateLimitError = new Error(
|
|
306
|
+
"Too many requests to TestDriver API. Please wait a moment and try again."
|
|
307
|
+
);
|
|
308
|
+
rateLimitError.code = "RATE_LIMITED";
|
|
309
|
+
rateLimitError.isRateLimitError = true;
|
|
310
|
+
rateLimitError.originalError = error;
|
|
311
|
+
return rateLimitError;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Forbidden (403) - likely Cloudflare or WAF blocking the request
|
|
315
|
+
if (status === 403) {
|
|
316
|
+
const forbiddenError = new Error(
|
|
317
|
+
"Request blocked (HTTP 403). This may be caused by a firewall or bot protection. " +
|
|
318
|
+
"If this persists, please contact support."
|
|
319
|
+
);
|
|
320
|
+
forbiddenError.code = "REQUEST_BLOCKED";
|
|
321
|
+
forbiddenError.isForbiddenError = true;
|
|
322
|
+
forbiddenError.originalError = error;
|
|
323
|
+
return forbiddenError;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Other HTTP errors - return with context
|
|
327
|
+
const url = error.config?.url || apiRoot;
|
|
328
|
+
const genericError = new Error(
|
|
329
|
+
`Authentication failed: ${status} ${error.response?.statusText || "Unknown error"} (${url})`
|
|
330
|
+
);
|
|
331
|
+
genericError.code = "AUTH_FAILED";
|
|
332
|
+
genericError.originalError = error;
|
|
333
|
+
return genericError;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get a helpful hint based on the network error code
|
|
338
|
+
* @param {string} code - The error code (ECONNREFUSED, ETIMEDOUT, etc.)
|
|
339
|
+
* @returns {string} A helpful message for the user
|
|
340
|
+
*/
|
|
341
|
+
function getNetworkErrorHint(code) {
|
|
342
|
+
const hints = {
|
|
343
|
+
ECONNREFUSED: "The server refused the connection. Check if the API is running.",
|
|
344
|
+
ETIMEDOUT: "The connection timed out. Check your internet connection.",
|
|
345
|
+
ENOTFOUND: "Could not resolve the hostname. Check your internet connection or DNS settings.",
|
|
346
|
+
ENETUNREACH: "Network is unreachable. Check your internet connection.",
|
|
347
|
+
ECONNRESET: "Connection was reset. This may be a temporary network issue.",
|
|
348
|
+
ERR_NETWORK: "A network error occurred. Check your internet connection.",
|
|
349
|
+
ECONNABORTED: "The request was aborted due to a timeout.",
|
|
350
|
+
};
|
|
351
|
+
return hints[code] || "Check your internet connection and try again.";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const req = async (path, data, onChunk) => {
|
|
355
|
+
// for each value of data, if it is null/undefined remove it
|
|
356
|
+
// Note: use == null to match both null and undefined, but preserve
|
|
357
|
+
// other falsy values like 0, false, and "" which may be intentional
|
|
358
|
+
for (let key in data) {
|
|
359
|
+
if (data[key] == null) {
|
|
360
|
+
delete data[key];
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── S3 upload: replace large inline base64 images with S3 keys ──────
|
|
365
|
+
// If data.image is a large base64 string (>50KB), upload the raw PNG
|
|
366
|
+
// to S3 via a presigned URL and send only the imageKey instead.
|
|
367
|
+
// This reduces JSON body size from ~1.3MB to ~60 bytes.
|
|
368
|
+
const MIN_IMAGE_SIZE = 50_000; // 50KB base64 chars
|
|
369
|
+
if (
|
|
370
|
+
data &&
|
|
371
|
+
typeof data.image === "string" &&
|
|
372
|
+
data.image.length > MIN_IMAGE_SIZE
|
|
373
|
+
) {
|
|
374
|
+
try {
|
|
375
|
+
const apiRoot = config["TD_API_ROOT"];
|
|
376
|
+
const uploadUrlEndpoint = [apiRoot, "api", version, "testdriver", "upload-url"].join("/");
|
|
377
|
+
|
|
378
|
+
// Step 1: Get presigned upload URL from API
|
|
379
|
+
const uploadRes = await axios(uploadUrlEndpoint, {
|
|
380
|
+
method: "post",
|
|
381
|
+
headers: {
|
|
382
|
+
"Content-Type": "application/json",
|
|
383
|
+
"User-Agent": `TestDriverSDK/${version} (Node.js ${process.version})`,
|
|
384
|
+
...(token && { Authorization: `Bearer ${token}` }),
|
|
385
|
+
...getSentryTraceHeaders(sessionInstance.get()),
|
|
386
|
+
},
|
|
387
|
+
timeout: 15000,
|
|
388
|
+
data: {
|
|
389
|
+
session: sessionInstance.get(),
|
|
390
|
+
contentType: "image/png",
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const { uploadUrl, imageKey } = uploadRes.data;
|
|
395
|
+
|
|
396
|
+
if (uploadUrl && imageKey) {
|
|
397
|
+
// Step 2: Upload raw PNG bytes to S3 via presigned PUT URL
|
|
398
|
+
const base64Data = data.image.replace(/^data:image\/\w+;base64,/, "");
|
|
399
|
+
const pngBuffer = Buffer.from(base64Data, "base64");
|
|
400
|
+
|
|
401
|
+
await axios(uploadUrl, {
|
|
402
|
+
method: "put",
|
|
403
|
+
headers: {
|
|
404
|
+
"Content-Type": "image/png",
|
|
405
|
+
"Content-Length": pngBuffer.length,
|
|
406
|
+
},
|
|
407
|
+
data: pngBuffer,
|
|
408
|
+
timeout: 30000,
|
|
409
|
+
maxBodyLength: Infinity,
|
|
410
|
+
maxContentLength: Infinity,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Step 3: Replace image with imageKey in the request data
|
|
414
|
+
const savedKB = (data.image.length / 1024).toFixed(0);
|
|
415
|
+
delete data.image;
|
|
416
|
+
data.imageKey = imageKey;
|
|
417
|
+
emitter.emit(events.log?.debug || events.sdk.request, {
|
|
418
|
+
path,
|
|
419
|
+
message: `[sdk] uploaded screenshot to S3 (saved ${savedKB}KB inline), imageKey=${imageKey}`,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
} catch (uploadErr) {
|
|
423
|
+
// Non-fatal: fall back to sending base64 inline
|
|
424
|
+
// This ensures old API servers without the upload-url endpoint still work
|
|
425
|
+
emitter.emit(events.log?.debug || events.sdk.request, {
|
|
426
|
+
path,
|
|
427
|
+
message: `[sdk] S3 upload failed, falling back to inline base64: ${uploadErr.message}`,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// ── End S3 upload ───────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
emitter.emit(events.sdk.request, {
|
|
434
|
+
path,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const url = path.startsWith("/api")
|
|
438
|
+
? [config["TD_API_ROOT"], path].join("")
|
|
439
|
+
: [config["TD_API_ROOT"], "api", version, "testdriver", path].join("/");
|
|
440
|
+
|
|
441
|
+
// Get session ID for Sentry trace headers
|
|
442
|
+
const sessionId = sessionInstance.get();
|
|
443
|
+
const sentryHeaders = getSentryTraceHeaders(sessionId);
|
|
444
|
+
|
|
445
|
+
const c = {
|
|
446
|
+
method: "post",
|
|
447
|
+
headers: {
|
|
448
|
+
"Content-Type": "application/json",
|
|
449
|
+
"User-Agent": `TestDriverSDK/${version} (Node.js ${process.version})`,
|
|
450
|
+
...(token && { Authorization: `Bearer ${token}` }), // Add the authorization bearer token only if token is set
|
|
451
|
+
...sentryHeaders, // Add Sentry distributed tracing headers
|
|
452
|
+
},
|
|
453
|
+
responseType: typeof onChunk === "function" ? "stream" : "json",
|
|
454
|
+
timeout: 120000, // 120 second timeout to prevent hanging requests
|
|
455
|
+
data: {
|
|
456
|
+
...data,
|
|
457
|
+
session: sessionInstance.get(),
|
|
458
|
+
stream: typeof onChunk === "function",
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
let response;
|
|
464
|
+
|
|
465
|
+
// Use retry logic for non-streaming requests
|
|
466
|
+
// Streaming requests are not retried as they involve ongoing data transfer
|
|
467
|
+
if (typeof onChunk !== "function") {
|
|
468
|
+
response = await withRetry(
|
|
469
|
+
() => axios(url, c),
|
|
470
|
+
{
|
|
471
|
+
onRetry: (attempt, error, delayMs) => {
|
|
472
|
+
emitter.emit(events.sdk.retry, {
|
|
473
|
+
path,
|
|
474
|
+
attempt,
|
|
475
|
+
error: error.message || error.code,
|
|
476
|
+
delayMs,
|
|
477
|
+
});
|
|
478
|
+
},
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
} else {
|
|
482
|
+
response = await axios(url, c);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
emitter.emit(events.sdk.response, {
|
|
486
|
+
path,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const contentType = response.headers["content-type"]?.toLowerCase();
|
|
490
|
+
const isJsonl = contentType === "application/jsonl";
|
|
491
|
+
let result;
|
|
492
|
+
|
|
493
|
+
if (onChunk) {
|
|
494
|
+
result = "";
|
|
495
|
+
let lastLineIndex = -1;
|
|
496
|
+
|
|
497
|
+
await new Promise((resolve, reject) => {
|
|
498
|
+
// theres some kind of race condition here that makes things resolve
|
|
499
|
+
// before the stream is done
|
|
500
|
+
|
|
501
|
+
response.data.on("data", (chunk) => {
|
|
502
|
+
result += chunk.toString();
|
|
503
|
+
const lines = result.split("\n");
|
|
504
|
+
|
|
505
|
+
const events = lines
|
|
506
|
+
.slice(lastLineIndex + 1, lines.length - 1)
|
|
507
|
+
.filter((line) => line.length)
|
|
508
|
+
.map((line) => JSON.parse(line));
|
|
509
|
+
|
|
510
|
+
for (const event of events) {
|
|
511
|
+
onChunk(event);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
lastLineIndex = lines.length - 2;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
response.data.on("end", () => {
|
|
518
|
+
if (isJsonl) {
|
|
519
|
+
const events = result
|
|
520
|
+
.split("\n")
|
|
521
|
+
.slice(lastLineIndex + 2)
|
|
522
|
+
.filter((line) => line.length)
|
|
523
|
+
.map((line) => JSON.parse(line));
|
|
524
|
+
|
|
525
|
+
for (const event of events) {
|
|
526
|
+
onChunk(event);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
resolve();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
response.data.on("error", (error) => {
|
|
534
|
+
reject(error);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const value = await parseBody(response, result);
|
|
540
|
+
|
|
541
|
+
return value;
|
|
542
|
+
} catch (error) {
|
|
543
|
+
// Check for network-level errors (no response received)
|
|
544
|
+
if (!error.response) {
|
|
545
|
+
const networkError = new Error(
|
|
546
|
+
`Unable to reach TestDriver API at ${config["TD_API_ROOT"]}. ` +
|
|
547
|
+
getNetworkErrorHint(error.code)
|
|
548
|
+
);
|
|
549
|
+
networkError.code = "NETWORK_ERROR";
|
|
550
|
+
networkError.isNetworkError = true;
|
|
551
|
+
networkError.originalError = error;
|
|
552
|
+
networkError.path = path;
|
|
553
|
+
|
|
554
|
+
emitter.emit(events.error.sdk, {
|
|
555
|
+
message: networkError.message,
|
|
556
|
+
code: networkError.code,
|
|
557
|
+
fullError: error,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
throw networkError;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Check if this is an API validation error with detailed problems
|
|
564
|
+
if (error.response?.data?.problems) {
|
|
565
|
+
const problems = error.response.data.problems;
|
|
566
|
+
const errorMessage = error.response.data.message || 'API validation error';
|
|
567
|
+
const detailedError = new Error(
|
|
568
|
+
`${errorMessage}\n\nDetails:\n${problems.map(p => ` - ${p}`).join('\n')}`
|
|
569
|
+
);
|
|
570
|
+
detailedError.originalError = error;
|
|
571
|
+
detailedError.problems = problems;
|
|
572
|
+
|
|
573
|
+
// Emit the formatted error
|
|
574
|
+
emitter.emit(events.error.sdk, {
|
|
575
|
+
message: detailedError.message,
|
|
576
|
+
code: error.response?.data?.code || error.code,
|
|
577
|
+
problems: problems,
|
|
578
|
+
fullError: error,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
throw detailedError;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Server errors (5xx) - API is down or having issues
|
|
585
|
+
const status = error.response?.status;
|
|
586
|
+
if (status >= 500) {
|
|
587
|
+
const serverError = new Error(
|
|
588
|
+
error.response?.data?.message ||
|
|
589
|
+
`TestDriver API is currently unavailable (HTTP ${status}). Please try again later.`
|
|
590
|
+
);
|
|
591
|
+
serverError.code = error.response?.data?.error || "API_UNAVAILABLE";
|
|
592
|
+
serverError.isServerError = true;
|
|
593
|
+
serverError.originalError = error;
|
|
594
|
+
serverError.path = path;
|
|
595
|
+
|
|
596
|
+
emitter.emit(events.error.sdk, {
|
|
597
|
+
message: serverError.message,
|
|
598
|
+
code: serverError.code,
|
|
599
|
+
fullError: error,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
throw serverError;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Rate limiting (429)
|
|
606
|
+
if (status === 429) {
|
|
607
|
+
const rateLimitError = new Error(
|
|
608
|
+
"Too many requests to TestDriver API. Please wait a moment and try again."
|
|
609
|
+
);
|
|
610
|
+
rateLimitError.code = "RATE_LIMITED";
|
|
611
|
+
rateLimitError.isRateLimitError = true;
|
|
612
|
+
rateLimitError.originalError = error;
|
|
613
|
+
rateLimitError.path = path;
|
|
614
|
+
|
|
615
|
+
emitter.emit(events.error.sdk, {
|
|
616
|
+
message: rateLimitError.message,
|
|
617
|
+
code: rateLimitError.code,
|
|
618
|
+
fullError: error,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
throw rateLimitError;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
outputError(error);
|
|
625
|
+
throw error; // Re-throw the error so calling code can handle it properly
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
return { req, auth };
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// Export the factory function and shared utilities
|
|
633
|
+
module.exports = { createSDK, withRetry, getSentryTraceHeaders, sleep };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Factory function to create session instance
|
|
2
|
+
function createSession() {
|
|
3
|
+
let session = null;
|
|
4
|
+
|
|
5
|
+
return {
|
|
6
|
+
get: () => {
|
|
7
|
+
return session;
|
|
8
|
+
},
|
|
9
|
+
set: (s) => {
|
|
10
|
+
if (s && !session) {
|
|
11
|
+
session = s;
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Export both factory function and legacy static instance for backward compatibility
|
|
18
|
+
const staticSession = createSession();
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
createSession,
|
|
22
|
+
// Legacy static exports for backward compatibility
|
|
23
|
+
get: staticSession.get,
|
|
24
|
+
set: staticSession.set,
|
|
25
|
+
};
|