@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
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest Hooks for TestDriver
|
|
3
|
+
*
|
|
4
|
+
* Provides lifecycle management for TestDriver in Vitest tests.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { TestDriver } from 'testdriverai/vitest/hooks';
|
|
8
|
+
*
|
|
9
|
+
* test('my test', async (context) => {
|
|
10
|
+
* const testdriver = TestDriver(context, { headless: true });
|
|
11
|
+
*
|
|
12
|
+
* await testdriver.provision.chrome({ url: 'https://example.com' });
|
|
13
|
+
* await testdriver.find('button').click();
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import { createRequire } from "module";
|
|
19
|
+
import path from "path";
|
|
20
|
+
import { vi } from "vitest";
|
|
21
|
+
import TestDriverSDK from "../../sdk.js";
|
|
22
|
+
|
|
23
|
+
// Use createRequire to import CommonJS modules
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const channelConfig = require("../../lib/resolve-channel.js");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Minimum required Vitest major version
|
|
29
|
+
*/
|
|
30
|
+
const MINIMUM_VITEST_VERSION = 4;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check that Vitest version meets minimum requirements
|
|
34
|
+
* @throws {Error} if Vitest version is below minimum or not installed
|
|
35
|
+
*/
|
|
36
|
+
function checkVitestVersion() {
|
|
37
|
+
try {
|
|
38
|
+
const vitestPkg = require("vitest/package.json");
|
|
39
|
+
const version = vitestPkg.version;
|
|
40
|
+
const major = parseInt(version.split(".")[0], 10);
|
|
41
|
+
|
|
42
|
+
if (major < MINIMUM_VITEST_VERSION) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`TestDriver requires Vitest >= ${MINIMUM_VITEST_VERSION}.0.0, but found ${version}. ` +
|
|
45
|
+
`Please upgrade Vitest: npm install vitest@latest`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err.code === "MODULE_NOT_FOUND") {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"TestDriver requires Vitest to be installed. " +
|
|
52
|
+
"Please install it: npm install vitest@latest",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check Vitest version at module load time
|
|
60
|
+
checkVitestVersion();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Singleton console spy that forwards logs to all active sandbox connections.
|
|
64
|
+
*
|
|
65
|
+
* When --sequence.concurrent is used, multiple tests run at the same time in
|
|
66
|
+
* the same worker process. The previous implementation called vi.spyOn on
|
|
67
|
+
* console.log once per test, stacking N mock layers deep. Every console.log
|
|
68
|
+
* then cascaded through all N layers — each one calling JSON.stringify inside
|
|
69
|
+
* forwardToSandbox — easily exceeding the call-stack limit for ≥ ~30 tests.
|
|
70
|
+
*
|
|
71
|
+
* This singleton intercepts the console methods exactly **once** and keeps a
|
|
72
|
+
* Set of active sandbox clients. Each log call is forwarded to every active
|
|
73
|
+
* client's sandbox in O(N) *flat* iterations instead of O(N) nested frames.
|
|
74
|
+
*/
|
|
75
|
+
const _consoleSpy = {
|
|
76
|
+
/** @type {Set<import('../../sdk.js').default>} */
|
|
77
|
+
activeClients: new Set(),
|
|
78
|
+
installed: false,
|
|
79
|
+
/** Original (un-spied) console references, captured once. */
|
|
80
|
+
originals: /** @type {{ log: Function, error: Function, warn: Function, info: Function } | null} */ (null),
|
|
81
|
+
spies: /** @type {{ log: any, error: any, warn: any, info: any } | null} */ (null),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Serialise console args to a single string for sandbox forwarding.
|
|
88
|
+
* Falls back to toString on circular/huge objects to avoid blowing out the
|
|
89
|
+
* stack inside JSON.stringify.
|
|
90
|
+
*/
|
|
91
|
+
function serialiseConsoleArgs(args) {
|
|
92
|
+
return args
|
|
93
|
+
.map((arg) => {
|
|
94
|
+
if (typeof arg === "object" && arg !== null) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.stringify(arg, null, 2);
|
|
97
|
+
} catch {
|
|
98
|
+
// Circular reference or too deep — fall back safely
|
|
99
|
+
return String(arg);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return String(arg);
|
|
103
|
+
})
|
|
104
|
+
.join(" ");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Buffer a console message into every active client's local log buffer.
|
|
109
|
+
* Replaces the old forwardToAllSandboxes which sent data over websocket
|
|
110
|
+
* to the sandbox for dashcam file-log capture. Logs are now uploaded
|
|
111
|
+
* directly to S3 from the vitest client at cleanup time.
|
|
112
|
+
*/
|
|
113
|
+
function bufferConsoleToClients(args, level) {
|
|
114
|
+
if (_consoleSpy.activeClients.size === 0) return;
|
|
115
|
+
|
|
116
|
+
const message = serialiseConsoleArgs(args);
|
|
117
|
+
|
|
118
|
+
for (const client of _consoleSpy.activeClients) {
|
|
119
|
+
if (client._logBuffer) {
|
|
120
|
+
client._logBuffer.push({
|
|
121
|
+
time: Date.now(),
|
|
122
|
+
line: message,
|
|
123
|
+
level: level || "log",
|
|
124
|
+
source: "console",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Install the singleton console spy (idempotent).
|
|
132
|
+
* Must be called *after* Vitest has set up its own console interception so
|
|
133
|
+
* that the originals we capture are Vitest's wrappers (which feed the test
|
|
134
|
+
* reporter output).
|
|
135
|
+
*/
|
|
136
|
+
function installConsoleSpy() {
|
|
137
|
+
// Check both installed flag AND that spies are still valid.
|
|
138
|
+
// Guards against a race where cleanupConsoleSpy restores mocks (setting
|
|
139
|
+
// installed=false) while a new test is starting up concurrently.
|
|
140
|
+
if (_consoleSpy.installed && _consoleSpy.spies) return;
|
|
141
|
+
_consoleSpy.installed = true;
|
|
142
|
+
|
|
143
|
+
// Capture originals once — these are whatever console methods look like
|
|
144
|
+
// right now (possibly already wrapped by Vitest's own reporter).
|
|
145
|
+
_consoleSpy.originals = {
|
|
146
|
+
log: console.log.bind(console),
|
|
147
|
+
error: console.error.bind(console),
|
|
148
|
+
warn: console.warn.bind(console),
|
|
149
|
+
info: console.info.bind(console),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const makeHandler = (originalFn, level) => (...args) => {
|
|
153
|
+
originalFn(...args); // Let Vitest's reporter capture the output
|
|
154
|
+
bufferConsoleToClients(args, level); // Buffer into local log store for S3 upload
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
_consoleSpy.spies = {
|
|
158
|
+
log: vi.spyOn(console, "log").mockImplementation(makeHandler(_consoleSpy.originals.log, "log")),
|
|
159
|
+
error: vi.spyOn(console, "error").mockImplementation(makeHandler(_consoleSpy.originals.error, "error")),
|
|
160
|
+
warn: vi.spyOn(console, "warn").mockImplementation(makeHandler(_consoleSpy.originals.warn, "warn")),
|
|
161
|
+
info: vi.spyOn(console, "info").mockImplementation(makeHandler(_consoleSpy.originals.info, "info")),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if (debugConsoleSpy) {
|
|
165
|
+
process.stdout.write("[DEBUG consoleSpy] Singleton console spy installed\n");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Register a TestDriver client so its sandbox receives forwarded logs.
|
|
171
|
+
* @param {import('../../sdk.js').default} client - TestDriver client instance
|
|
172
|
+
* @param {string} taskId - Unique task identifier (for debug logging)
|
|
173
|
+
*/
|
|
174
|
+
function setupConsoleSpy(client, taskId) {
|
|
175
|
+
if (debugConsoleSpy) {
|
|
176
|
+
process.stdout.write(`[DEBUG setupConsoleSpy] registering taskId: ${taskId}\n`);
|
|
177
|
+
}
|
|
178
|
+
installConsoleSpy();
|
|
179
|
+
_consoleSpy.activeClients.add(client);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Unregister a client so its sandbox no longer receives forwarded logs.
|
|
184
|
+
* When the last client is removed we restore the original console methods so
|
|
185
|
+
* the Vitest worker fork can exit cleanly (unreleased vi.spyOn mocks prevent
|
|
186
|
+
* the worker from shutting down, producing "Worker exited unexpectedly").
|
|
187
|
+
* If another test starts later (e.g. a retry), installConsoleSpy() will
|
|
188
|
+
* re-install the spy on demand.
|
|
189
|
+
* @param {import('../../sdk.js').default} client - TestDriver client instance
|
|
190
|
+
*/
|
|
191
|
+
function cleanupConsoleSpy(client) {
|
|
192
|
+
_consoleSpy.activeClients.delete(client);
|
|
193
|
+
|
|
194
|
+
// Restore spies when no tests need them — allows clean worker exit
|
|
195
|
+
if (_consoleSpy.activeClients.size === 0 && _consoleSpy.spies) {
|
|
196
|
+
_consoleSpy.spies.log.mockRestore();
|
|
197
|
+
_consoleSpy.spies.error.mockRestore();
|
|
198
|
+
_consoleSpy.spies.warn.mockRestore();
|
|
199
|
+
_consoleSpy.spies.info.mockRestore();
|
|
200
|
+
_consoleSpy.spies = null;
|
|
201
|
+
_consoleSpy.originals = null;
|
|
202
|
+
_consoleSpy.installed = false;
|
|
203
|
+
|
|
204
|
+
if (debugConsoleSpy) {
|
|
205
|
+
process.stdout.write("[DEBUG cleanupConsoleSpy] All spies restored\n");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (debugConsoleSpy) {
|
|
210
|
+
process.stdout.write(
|
|
211
|
+
`[DEBUG cleanupConsoleSpy] clients remaining: ${_consoleSpy.activeClients.size}\n`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Weak maps to store instances per test context
|
|
217
|
+
const testDriverInstances = new WeakMap();
|
|
218
|
+
const lifecycleHandlers = new WeakMap();
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Upload buffered SDK + console logs directly to S3 via the existing Log system.
|
|
222
|
+
* Extracts the replayId from the dashcam URL, calls POST /api/v1/logs to create
|
|
223
|
+
* a Log record and get a presigned PUT URL, then uploads the JSONL payload.
|
|
224
|
+
*
|
|
225
|
+
* @param {import('../../sdk.js').default} client - TestDriver SDK instance
|
|
226
|
+
* @param {string|null} dashcamUrl - Dashcam replay URL from dashcam.stop()
|
|
227
|
+
*/
|
|
228
|
+
async function uploadLogsToReplay(client, dashcamUrl) {
|
|
229
|
+
if (!dashcamUrl) return;
|
|
230
|
+
|
|
231
|
+
// Extract replayId from the dashcam URL (e.g. https://app.dashcam.io/replay/6789abcdef012345...)
|
|
232
|
+
const replayMatch = dashcamUrl.match(/replay\/([a-f0-9]{24})/);
|
|
233
|
+
if (!replayMatch) {
|
|
234
|
+
if (debugConsoleSpy) {
|
|
235
|
+
console.log("[uploadLogsToReplay] Could not extract replayId from:", dashcamUrl);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const replayId = replayMatch[1];
|
|
241
|
+
const logData = client.getLogs();
|
|
242
|
+
|
|
243
|
+
if (!logData || logData.trim().length === 0) {
|
|
244
|
+
if (debugConsoleSpy) {
|
|
245
|
+
console.log("[uploadLogsToReplay] No logs to upload");
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
// Get TD_API_KEY for auth (prefer SDK config which is always correctly resolved)
|
|
252
|
+
const apiKey = client.config?.TD_API_KEY || process.env.TD_API_KEY;
|
|
253
|
+
if (!apiKey) {
|
|
254
|
+
console.warn("[TestDriver] TD_API_KEY not set, skipping log upload");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Use the SDK's configured API root (matches what the SDK uses for all other API calls)
|
|
259
|
+
const apiRoot = client.config?.TD_API_ROOT || process.env.TD_API_ROOT || channelConfig.channels[channelConfig.active];
|
|
260
|
+
|
|
261
|
+
console.log(`[TestDriver] Uploading logs for replay ${replayId} to ${apiRoot}...`);
|
|
262
|
+
|
|
263
|
+
const authRes = await fetch(`${apiRoot}/auth/exchange-api-key`, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers: { "Content-Type": "application/json" },
|
|
266
|
+
body: JSON.stringify({ apiKey }),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (!authRes.ok) {
|
|
270
|
+
console.warn("[TestDriver] Failed to exchange API key for log upload:", authRes.status);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const authData = await authRes.json();
|
|
275
|
+
const token = authData.token;
|
|
276
|
+
|
|
277
|
+
// Create a Log record and get presigned upload URL
|
|
278
|
+
const logCreateRes = await fetch(`${apiRoot}/api/v1/logs`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: {
|
|
281
|
+
"Content-Type": "application/json",
|
|
282
|
+
Authorization: `Bearer ${token}`,
|
|
283
|
+
},
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
replayId,
|
|
286
|
+
appId: "testdriver-sdk",
|
|
287
|
+
name: "TestDriver Log",
|
|
288
|
+
type: "cli",
|
|
289
|
+
}),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (!logCreateRes.ok) {
|
|
293
|
+
console.warn("[TestDriver] Failed to create log record:", logCreateRes.status);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const logCreateData = await logCreateRes.json();
|
|
298
|
+
const uploadUrl = logCreateData.presignedUploadUrl;
|
|
299
|
+
|
|
300
|
+
if (!uploadUrl) {
|
|
301
|
+
console.warn("[TestDriver] No presigned upload URL returned from log-create");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Upload the JSONL log data directly to S3
|
|
306
|
+
const uploadRes = await fetch(uploadUrl, {
|
|
307
|
+
method: "PUT",
|
|
308
|
+
headers: { "Content-Type": "application/x-ndjson" },
|
|
309
|
+
body: logData,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (!uploadRes.ok) {
|
|
313
|
+
console.warn("[TestDriver] Failed to upload logs to S3:", uploadRes.status);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log(`[TestDriver] ✅ Logs uploaded successfully for replay: ${replayId}`);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
// Fire-and-forget — don't let log upload failure break the test
|
|
320
|
+
console.warn("[TestDriver] Log upload failed:", err.message);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Create a TestDriver client in a Vitest test with automatic lifecycle management
|
|
326
|
+
*
|
|
327
|
+
* @param {import('vitest').TestContext} context - Vitest test context (from async (context) => {})
|
|
328
|
+
* @param {import('../../sdk.js').TestDriverOptions} [options] - TestDriver options (passed directly to TestDriver constructor)
|
|
329
|
+
* @returns {import('../../sdk.js').default} TestDriver client instance
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* test('my test', async (context) => {
|
|
333
|
+
* const testdriver = TestDriver(context, { headless: true });
|
|
334
|
+
*
|
|
335
|
+
* // provision.chrome() automatically calls ready() and starts dashcam
|
|
336
|
+
* await testdriver.provision.chrome({ url: 'https://example.com' });
|
|
337
|
+
*
|
|
338
|
+
* await testdriver.find('Login button').click();
|
|
339
|
+
* });
|
|
340
|
+
*/
|
|
341
|
+
export function TestDriver(context, options = {}) {
|
|
342
|
+
if (!context || !context.task) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
'TestDriver() requires Vitest context. Pass the context parameter from your test function: test("name", async (context) => { ... })',
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Return existing instance if already created for this test AND it's still connected
|
|
349
|
+
// On retry, the previous instance will be disconnected, so we need to create a new one
|
|
350
|
+
if (testDriverInstances.has(context.task)) {
|
|
351
|
+
const existingInstance = testDriverInstances.get(context.task);
|
|
352
|
+
if (existingInstance.connected) {
|
|
353
|
+
return existingInstance;
|
|
354
|
+
}
|
|
355
|
+
// Instance exists but is disconnected (likely a retry) - remove it and create fresh
|
|
356
|
+
testDriverInstances.delete(context.task);
|
|
357
|
+
lifecycleHandlers.delete(context.task);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Get global plugin options if available
|
|
361
|
+
const pluginOptions =
|
|
362
|
+
globalThis.__testdriverPlugin?.state?.testDriverOptions || {};
|
|
363
|
+
|
|
364
|
+
// Merge options: plugin global options < test-specific options
|
|
365
|
+
const mergedOptions = { ...pluginOptions, ...options };
|
|
366
|
+
|
|
367
|
+
// Support TD_OS environment variable for specifying target OS (linux, mac, windows)
|
|
368
|
+
// Priority: test options > plugin options > environment variable > default (linux)
|
|
369
|
+
if (!mergedOptions.os && process.env.TD_OS) {
|
|
370
|
+
mergedOptions.os = process.env.TD_OS;
|
|
371
|
+
console.log(
|
|
372
|
+
`[testdriver] Set mergedOptions.os = ${mergedOptions.os} from TD_OS environment variable`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Use IP from context if set by setup-aws.mjs (or other setup files)
|
|
377
|
+
// Priority: test options > context.ip (from setup hooks)
|
|
378
|
+
if (!mergedOptions.ip && context.ip) {
|
|
379
|
+
mergedOptions.ip = context.ip;
|
|
380
|
+
console.log(
|
|
381
|
+
`[testdriver] Set mergedOptions.ip = ${mergedOptions.ip} from context.ip`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Use instanceId from context if set by setup-aws.mjs
|
|
386
|
+
// This allows the API to provision Ably credentials via SSM for direct connections
|
|
387
|
+
if (!mergedOptions.instanceId && context.instanceId) {
|
|
388
|
+
mergedOptions.instanceId = context.instanceId;
|
|
389
|
+
console.log(
|
|
390
|
+
`[testdriver] Set mergedOptions.instanceId = ${mergedOptions.instanceId} from context.instanceId`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Extract TestDriver-specific options
|
|
395
|
+
const apiKey = mergedOptions.apiKey || process.env.TD_API_KEY;
|
|
396
|
+
|
|
397
|
+
// Build config for TestDriverSDK constructor
|
|
398
|
+
const config = { ...mergedOptions };
|
|
399
|
+
delete config.apiKey;
|
|
400
|
+
|
|
401
|
+
// Use TD_API_ROOT from environment if not provided in config
|
|
402
|
+
if (!config.apiRoot && process.env.TD_API_ROOT) {
|
|
403
|
+
config.apiRoot = process.env.TD_API_ROOT;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const testdriver = new TestDriverSDK(apiKey, config);
|
|
407
|
+
testdriver.__vitestContext = context.task;
|
|
408
|
+
testdriver._debugOnFailure = mergedOptions.debugOnFailure || false;
|
|
409
|
+
testDriverInstances.set(context.task, testdriver);
|
|
410
|
+
|
|
411
|
+
// Set platform metadata early so the reporter can show the correct OS from the start
|
|
412
|
+
if (!context.task.meta) {
|
|
413
|
+
context.task.meta = {};
|
|
414
|
+
}
|
|
415
|
+
const platform = mergedOptions.os || "linux";
|
|
416
|
+
const absolutePath =
|
|
417
|
+
context.task.file?.filepath || context.task.file?.name || "unknown";
|
|
418
|
+
const projectRoot = process.cwd();
|
|
419
|
+
const testFile =
|
|
420
|
+
absolutePath !== "unknown"
|
|
421
|
+
? path.relative(projectRoot, absolutePath)
|
|
422
|
+
: absolutePath;
|
|
423
|
+
|
|
424
|
+
context.task.meta.platform = platform;
|
|
425
|
+
context.task.meta.testFile = testFile;
|
|
426
|
+
context.task.meta.testOrder = 0;
|
|
427
|
+
|
|
428
|
+
// Pass test file name to SDK for debugger display
|
|
429
|
+
testdriver.testFile = testFile;
|
|
430
|
+
|
|
431
|
+
const debugConsoleSpy = process.env.TD_DEBUG_CONSOLE_SPY === "true";
|
|
432
|
+
|
|
433
|
+
testdriver.__connectionPromise = (async () => {
|
|
434
|
+
if (debugConsoleSpy) {
|
|
435
|
+
console.log(
|
|
436
|
+
"[DEBUG] Before auth - sandbox.instanceSocketConnected:",
|
|
437
|
+
testdriver.sandbox?.instanceSocketConnected,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
await testdriver.auth();
|
|
442
|
+
await testdriver.connect();
|
|
443
|
+
|
|
444
|
+
// Clear the connection promise now that we're connected
|
|
445
|
+
// This prevents deadlock when exec() is called below (exec() lazy-awaits __connectionPromise)
|
|
446
|
+
testdriver.__connectionPromise = null;
|
|
447
|
+
|
|
448
|
+
if (debugConsoleSpy) {
|
|
449
|
+
console.log(
|
|
450
|
+
"[DEBUG] After connect - sandbox.instanceSocketConnected:",
|
|
451
|
+
testdriver.sandbox?.instanceSocketConnected,
|
|
452
|
+
);
|
|
453
|
+
console.log(
|
|
454
|
+
"[DEBUG] After connect - sandbox.send:",
|
|
455
|
+
typeof testdriver.sandbox?.send,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Set up console spy for local log buffering (always, regardless of dashcam)
|
|
460
|
+
setupConsoleSpy(testdriver, context.task.id);
|
|
461
|
+
|
|
462
|
+
// Note: We no longer create a log file on the sandbox or call dashcam.addFileLog().
|
|
463
|
+
// TestDriver logs are buffered locally in _logBuffer and uploaded directly to S3
|
|
464
|
+
// from the vitest client at cleanup time. This avoids the expensive path of
|
|
465
|
+
// forwarding logs over websocket → sandbox file → dashcam upload.
|
|
466
|
+
//
|
|
467
|
+
// Web log tracking and dashcam.start() are still handled by provision.chrome()
|
|
468
|
+
// This ensures addWebLog is called with the domain pattern BEFORE dashcam.start()
|
|
469
|
+
})();
|
|
470
|
+
|
|
471
|
+
// Register cleanup handler with dashcam.stop()
|
|
472
|
+
// We always register a new cleanup handler because on retry we need to clean up the new instance
|
|
473
|
+
const cleanup = async () => {
|
|
474
|
+
// Get the current instance from the WeakMap (not from closure)
|
|
475
|
+
// This ensures we clean up the correct instance on retries
|
|
476
|
+
const currentInstance = testDriverInstances.get(context.task);
|
|
477
|
+
if (!currentInstance) {
|
|
478
|
+
return; // Already cleaned up
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Check if debug-on-failure mode is enabled and test failed
|
|
482
|
+
const debugOnFailure = currentInstance._debugOnFailure;
|
|
483
|
+
const testFailed = context.task.result?.state === "fail";
|
|
484
|
+
|
|
485
|
+
if (debugOnFailure && testFailed) {
|
|
486
|
+
// Get sandbox ID before we skip cleanup
|
|
487
|
+
const instance = currentInstance.getInstance?.();
|
|
488
|
+
const sandboxId = instance?.sandboxId || instance?.instanceId;
|
|
489
|
+
|
|
490
|
+
console.log("");
|
|
491
|
+
console.log(
|
|
492
|
+
chalk.yellow(
|
|
493
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
494
|
+
),
|
|
495
|
+
);
|
|
496
|
+
console.log(
|
|
497
|
+
chalk.yellow(" DEBUG MODE: Sandbox kept alive for debugging"),
|
|
498
|
+
);
|
|
499
|
+
console.log(
|
|
500
|
+
chalk.yellow(
|
|
501
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
502
|
+
),
|
|
503
|
+
);
|
|
504
|
+
console.log("");
|
|
505
|
+
console.log(" To connect via MCP:");
|
|
506
|
+
console.log(
|
|
507
|
+
chalk.cyan(` session_start({ sandboxId: "${sandboxId}" })`),
|
|
508
|
+
);
|
|
509
|
+
console.log("");
|
|
510
|
+
console.log(chalk.dim(" Sandbox will expire in 5 minutes."));
|
|
511
|
+
console.log(
|
|
512
|
+
chalk.yellow(
|
|
513
|
+
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
514
|
+
),
|
|
515
|
+
);
|
|
516
|
+
console.log("");
|
|
517
|
+
|
|
518
|
+
// Still stop dashcam to save the recording
|
|
519
|
+
if (currentInstance._dashcam && currentInstance._dashcam.recording) {
|
|
520
|
+
try {
|
|
521
|
+
await currentInstance.dashcam.stop();
|
|
522
|
+
} catch {
|
|
523
|
+
// Ignore dashcam stop errors in debug mode
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Clean up console spies
|
|
528
|
+
cleanupConsoleSpy(currentInstance);
|
|
529
|
+
|
|
530
|
+
// DO NOT disconnect or terminate - keep sandbox alive for debugging
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
// Ensure meta object exists
|
|
536
|
+
if (!context.task.meta) {
|
|
537
|
+
context.task.meta = {};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Always set test metadata, even if dashcam never started or fails to stop
|
|
541
|
+
// This ensures the reporter can record test results even for early failures
|
|
542
|
+
const platform = currentInstance.os || "linux";
|
|
543
|
+
const absolutePath =
|
|
544
|
+
context.task.file?.filepath || context.task.file?.name || "unknown";
|
|
545
|
+
const projectRoot = process.cwd();
|
|
546
|
+
const testFile =
|
|
547
|
+
absolutePath !== "unknown"
|
|
548
|
+
? path.relative(projectRoot, absolutePath)
|
|
549
|
+
: absolutePath;
|
|
550
|
+
|
|
551
|
+
// Set basic metadata that's always available
|
|
552
|
+
context.task.meta.platform = platform;
|
|
553
|
+
context.task.meta.testFile = testFile;
|
|
554
|
+
context.task.meta.testOrder = 0;
|
|
555
|
+
context.task.meta.sessionId = currentInstance.getSessionId?.() || null;
|
|
556
|
+
|
|
557
|
+
// Initialize dashcamUrls array for tracking per-attempt URLs (persists across retries)
|
|
558
|
+
if (!context.task.meta.dashcamUrls) {
|
|
559
|
+
context.task.meta.dashcamUrls = [];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Determine the current attempt number (1-based)
|
|
563
|
+
const attemptNumber = context.task.meta.dashcamUrls.length + 1;
|
|
564
|
+
const isRetry = attemptNumber > 1;
|
|
565
|
+
const attemptLabel = isRetry ? ` (attempt ${attemptNumber})` : "";
|
|
566
|
+
|
|
567
|
+
// Stop dashcam if it was started - with timeout to prevent hanging
|
|
568
|
+
if (currentInstance._dashcam && currentInstance._dashcam.recording) {
|
|
569
|
+
try {
|
|
570
|
+
const dashcamUrl = await currentInstance.dashcam.stop();
|
|
571
|
+
|
|
572
|
+
// Track this attempt's URL in the per-attempt array
|
|
573
|
+
context.task.meta.dashcamUrls.push({
|
|
574
|
+
attempt: attemptNumber,
|
|
575
|
+
url: dashcamUrl || null,
|
|
576
|
+
sessionId: currentInstance.getSessionId?.() || null,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Keep backward compatibility - last attempt's URL
|
|
580
|
+
context.task.meta.dashcamUrl = dashcamUrl || null;
|
|
581
|
+
|
|
582
|
+
// Also register in memory if plugin is available (for cross-process scenarios)
|
|
583
|
+
if (globalThis.__testdriverPlugin?.registerDashcamUrl) {
|
|
584
|
+
globalThis.__testdriverPlugin.registerDashcamUrl(
|
|
585
|
+
context.task.id,
|
|
586
|
+
dashcamUrl,
|
|
587
|
+
platform,
|
|
588
|
+
attemptNumber,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Always print the dashcam URL for each attempt so it's visible in logs
|
|
593
|
+
if (dashcamUrl) {
|
|
594
|
+
console.log("");
|
|
595
|
+
console.log(
|
|
596
|
+
"🎥" + chalk.yellow(` Dashcam URL${attemptLabel}`) + `: ${dashcamUrl}`,
|
|
597
|
+
);
|
|
598
|
+
console.log("");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Upload buffered logs directly to S3 via the existing Log system.
|
|
602
|
+
// This replaces the old path of forwarding logs to the sandbox for dashcam capture.
|
|
603
|
+
await uploadLogsToReplay(currentInstance, dashcamUrl);
|
|
604
|
+
} catch (error) {
|
|
605
|
+
// Log more detailed error information for debugging
|
|
606
|
+
console.error(
|
|
607
|
+
"❌ Failed to stop dashcam:",
|
|
608
|
+
error.name || error.constructor?.name || "Error",
|
|
609
|
+
);
|
|
610
|
+
if (error.message) console.error(" Message:", error.message);
|
|
611
|
+
// NotFoundError during cleanup is expected if sandbox already terminated
|
|
612
|
+
if (
|
|
613
|
+
error.name === "NotFoundError" ||
|
|
614
|
+
error.responseData?.error === "NotFoundError"
|
|
615
|
+
) {
|
|
616
|
+
console.log(
|
|
617
|
+
" ℹ️ Sandbox session already terminated - dashcam stop skipped",
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
// Mark as not recording to prevent retries
|
|
621
|
+
if (currentInstance._dashcam) {
|
|
622
|
+
currentInstance._dashcam.recording = false;
|
|
623
|
+
}
|
|
624
|
+
// Track failed attempt
|
|
625
|
+
context.task.meta.dashcamUrls.push({
|
|
626
|
+
attempt: attemptNumber,
|
|
627
|
+
url: null,
|
|
628
|
+
sessionId: currentInstance.getSessionId?.() || null,
|
|
629
|
+
error: error.message,
|
|
630
|
+
});
|
|
631
|
+
// Ensure dashcamUrl is set to null if stop failed
|
|
632
|
+
context.task.meta.dashcamUrl = null;
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
// No dashcam recording - still track the attempt
|
|
636
|
+
context.task.meta.dashcamUrls.push({
|
|
637
|
+
attempt: attemptNumber,
|
|
638
|
+
url: null,
|
|
639
|
+
sessionId: currentInstance.getSessionId?.() || null,
|
|
640
|
+
});
|
|
641
|
+
context.task.meta.dashcamUrl = null;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Clean up console spies
|
|
645
|
+
cleanupConsoleSpy(currentInstance);
|
|
646
|
+
|
|
647
|
+
// Wait for connection to finish if it was initiated
|
|
648
|
+
if (currentInstance.__connectionPromise) {
|
|
649
|
+
await currentInstance.__connectionPromise.catch(() => {}); // Ignore connection errors during cleanup
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Disconnect with timeout
|
|
653
|
+
await Promise.race([
|
|
654
|
+
currentInstance.disconnect(),
|
|
655
|
+
new Promise((resolve) => setTimeout(resolve, 5000)), // 5s timeout for disconnect
|
|
656
|
+
]);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.error("Error disconnecting client:", error);
|
|
659
|
+
} finally {
|
|
660
|
+
// Terminate AWS instance if one was spawned for this test
|
|
661
|
+
// This must happen AFTER dashcam.stop() to ensure recording is saved
|
|
662
|
+
// AND it must happen even if disconnect() fails
|
|
663
|
+
if (globalThis.__testdriverAWS?.terminateInstance) {
|
|
664
|
+
await globalThis.__testdriverAWS.terminateInstance(context.task.id);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
lifecycleHandlers.set(context.task, cleanup);
|
|
669
|
+
|
|
670
|
+
// Vitest will call this automatically after the test (each retry attempt)
|
|
671
|
+
context.onTestFinished?.(cleanup);
|
|
672
|
+
|
|
673
|
+
return testdriver;
|
|
674
|
+
}
|