testdriverai 6.2.2 → 7.1.0
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/.github/workflows/acceptance-linux.yml +75 -0
- package/.github/workflows/acceptance-sdk-tests.yml +133 -0
- package/.vscode/settings.json +5 -1
- package/AGENTS.md +550 -0
- package/CODEOWNERS +0 -1
- package/README.md +126 -0
- package/{testdriver → _testdriver}/acceptance/drag-and-drop.yaml +2 -2
- package/{testdriver → _testdriver}/acceptance/snippets/login.yaml +1 -1
- package/_testdriver/examples/desktop/lifecycle/prerun.yaml +0 -0
- package/{testdriver → _testdriver}/examples/web/lifecycle/prerun.yaml +6 -1
- package/{testdriver → _testdriver}/lifecycle/postrun.yaml +3 -2
- package/_testdriver/lifecycle/prerun.yaml +15 -0
- package/{testdriver → _testdriver}/lifecycle/provision.yaml +7 -2
- package/agent/index.js +300 -85
- package/agent/interface.js +15 -0
- package/agent/lib/cache.js +142 -0
- package/agent/lib/commander.js +1 -39
- package/agent/lib/commands.js +910 -296
- package/agent/lib/redraw.js +129 -41
- package/agent/lib/sandbox.js +29 -6
- package/agent/lib/sdk.js +22 -0
- package/agent/lib/system.js +0 -3
- package/agent/lib/validation.js +1 -7
- package/debug-locate-response.js +82 -0
- package/debugger/index.html +15 -4
- package/docs/ARCHITECTURE.md +424 -0
- package/docs/AWESOME_LOGS_QUICK_REF.md +100 -0
- package/docs/MIGRATION.md +425 -0
- package/docs/PRESETS.md +210 -0
- package/docs/QUICK_START_TEST_RECORDING.md +215 -0
- package/docs/SDK_AWESOME_LOGS.md +468 -0
- package/docs/TEST_RECORDING.md +388 -0
- package/docs/docs.json +286 -152
- package/docs/guide/best-practices-polling.mdx +154 -0
- package/docs/sdk-browser-rendering.md +167 -0
- package/docs/v6/getting-started/self-hosting.mdx +407 -0
- package/docs/{guide → v6/guide}/dashcam.mdx +1 -1
- package/docs/{guide → v6/guide}/environment-variables.mdx +4 -5
- package/docs/{guide → v6/guide}/lifecycle.mdx +1 -1
- package/docs/v6/overview/comparison.mdx +101 -0
- package/docs/v7/README.md +135 -0
- package/docs/v7/api/ai.mdx +205 -0
- package/docs/v7/api/assert.mdx +285 -0
- package/docs/v7/api/assertions.mdx +403 -0
- package/docs/v7/api/click.mdx +287 -0
- package/docs/v7/api/client.mdx +322 -0
- package/docs/v7/api/dashcam.mdx +497 -0
- package/docs/v7/api/doubleClick.mdx +102 -0
- package/docs/v7/api/elements.mdx +479 -0
- package/docs/v7/api/exec.mdx +346 -0
- package/docs/v7/api/find.mdx +316 -0
- package/docs/v7/api/focusApplication.mdx +294 -0
- package/docs/v7/api/hover.mdx +279 -0
- package/docs/v7/api/mouseDown.mdx +161 -0
- package/docs/v7/api/mouseUp.mdx +164 -0
- package/docs/v7/api/pressKeys.mdx +349 -0
- package/docs/v7/api/rightClick.mdx +123 -0
- package/docs/v7/api/sandbox.mdx +404 -0
- package/docs/v7/api/scroll.mdx +300 -0
- package/docs/v7/api/type.mdx +314 -0
- package/docs/v7/commands/assert.mdx +45 -0
- package/docs/v7/commands/exec.mdx +282 -0
- package/docs/v7/commands/focus-application.mdx +44 -0
- package/docs/v7/commands/hover-image.mdx +69 -0
- package/docs/v7/commands/hover-text.mdx +47 -0
- package/docs/v7/commands/if.mdx +53 -0
- package/docs/v7/commands/match-image.mdx +67 -0
- package/docs/v7/commands/press-keys.mdx +87 -0
- package/docs/v7/commands/remember.mdx +49 -0
- package/docs/v7/commands/run.mdx +44 -0
- package/docs/v7/commands/scroll-until-image.mdx +66 -0
- package/docs/v7/commands/scroll-until-text.mdx +60 -0
- package/docs/v7/commands/scroll.mdx +69 -0
- package/docs/v7/commands/type.mdx +45 -0
- package/docs/v7/commands/wait-for-image.mdx +54 -0
- package/docs/v7/commands/wait-for-text.mdx +48 -0
- package/docs/v7/commands/wait.mdx +45 -0
- package/docs/v7/getting-started/configuration.mdx +380 -0
- package/docs/v7/getting-started/quickstart.mdx +332 -0
- package/docs/v7/guides/best-practices.mdx +486 -0
- package/docs/v7/guides/caching-ai.mdx +215 -0
- package/docs/v7/guides/caching-selectors.mdx +292 -0
- package/docs/v7/guides/caching.mdx +366 -0
- package/docs/v7/guides/ci-cd/azure.mdx +587 -0
- package/docs/v7/guides/ci-cd/circleci.mdx +523 -0
- package/docs/v7/guides/ci-cd/github-actions.mdx +457 -0
- package/docs/v7/guides/ci-cd/gitlab.mdx +498 -0
- package/docs/v7/guides/ci-cd/jenkins.mdx +664 -0
- package/docs/v7/guides/ci-cd/travis.mdx +438 -0
- package/docs/v7/guides/debugging.mdx +349 -0
- package/docs/v7/guides/faq.mdx +393 -0
- package/docs/v7/guides/migration.mdx +562 -0
- package/docs/v7/guides/performance.mdx +517 -0
- package/docs/{getting-started → v7/guides}/self-hosting.mdx +11 -12
- package/docs/v7/guides/troubleshooting.mdx +526 -0
- package/docs/v7/guides/vitest-plugin.mdx +477 -0
- package/docs/v7/guides/vitest.mdx +535 -0
- package/docs/v7/platforms/linux.mdx +308 -0
- package/docs/v7/platforms/macos.mdx +433 -0
- package/docs/v7/platforms/windows.mdx +430 -0
- package/docs/v7/playwright.mdx +342 -0
- package/docs/v7/presets/chrome-extension.mdx +223 -0
- package/docs/v7/presets/chrome.mdx +287 -0
- package/docs/v7/presets/electron.mdx +435 -0
- package/docs/v7/presets/vscode.mdx +398 -0
- package/docs/v7/presets/webapp.mdx +396 -0
- package/docs/v7/progressive-apis/CORE.md +459 -0
- package/docs/v7/progressive-apis/HOOKS.md +360 -0
- package/docs/v7/progressive-apis/PROGRESSIVE_DISCLOSURE.md +230 -0
- package/docs/v7/progressive-apis/PROVISION.md +266 -0
- package/eslint.config.js +19 -1
- package/interfaces/cli/lib/base.js +10 -4
- package/interfaces/logger.js +2 -1
- package/interfaces/shared-test-state.mjs +69 -0
- package/interfaces/vitest-plugin.mjs +830 -0
- package/package.json +29 -5
- package/schema.json +8 -29
- package/scripts/view-test-results.mjs +96 -0
- package/sdk-log-formatter.js +714 -0
- package/sdk.d.ts +1028 -0
- package/sdk.js +2567 -0
- package/{.github/workflows/self-hosted.yml → self-hosted.yml} +13 -4
- package/setup/aws/cloudformation.yaml +9 -2
- package/src/core/Dashcam.js +469 -0
- package/src/core/index.d.ts +150 -0
- package/src/core/index.js +12 -0
- package/src/presets/index.mjs +331 -0
- package/src/vitest/extended.mjs +108 -0
- package/src/vitest/hooks.d.ts +119 -0
- package/src/vitest/hooks.mjs +298 -0
- package/src/vitest/index.mjs +64 -0
- package/src/vitest/lifecycle.mjs +277 -0
- package/src/vitest/utils.mjs +150 -0
- package/test/dashcam.test.js +137 -0
- package/test/mcp-example-test.yaml +27 -0
- package/testdriver/acceptance-sdk/QUICK_REFERENCE.md +61 -0
- package/testdriver/acceptance-sdk/README.md +128 -0
- package/testdriver/acceptance-sdk/TEST_REPORTING.md +245 -0
- package/testdriver/acceptance-sdk/assert.test.mjs +26 -0
- package/testdriver/acceptance-sdk/auto-cache-key-demo.test.mjs +56 -0
- package/testdriver/acceptance-sdk/chrome-extension.test.mjs +89 -0
- package/testdriver/acceptance-sdk/drag-and-drop.test.mjs +58 -0
- package/testdriver/acceptance-sdk/element-not-found.test.mjs +25 -0
- package/testdriver/acceptance-sdk/exec-js.test.mjs +43 -0
- package/testdriver/acceptance-sdk/exec-output.test.mjs +59 -0
- package/testdriver/acceptance-sdk/exec-pwsh.test.mjs +57 -0
- package/testdriver/acceptance-sdk/focus-window.test.mjs +36 -0
- package/testdriver/acceptance-sdk/formatted-logging.test.mjs +26 -0
- package/testdriver/acceptance-sdk/hooks-example.test.mjs +38 -0
- package/testdriver/acceptance-sdk/hover-image.test.mjs +34 -0
- package/testdriver/acceptance-sdk/hover-text-with-description.test.mjs +38 -0
- package/testdriver/acceptance-sdk/hover-text.test.mjs +27 -0
- package/testdriver/acceptance-sdk/match-image.test.mjs +36 -0
- package/testdriver/acceptance-sdk/presets-example.test.mjs +87 -0
- package/testdriver/acceptance-sdk/press-keys.test.mjs +50 -0
- package/testdriver/acceptance-sdk/prompt.test.mjs +33 -0
- package/testdriver/acceptance-sdk/scroll-keyboard.test.mjs +38 -0
- package/testdriver/acceptance-sdk/scroll-until-image.test.mjs +39 -0
- package/testdriver/acceptance-sdk/scroll-until-text.test.mjs +28 -0
- package/testdriver/acceptance-sdk/scroll.test.mjs +41 -0
- package/testdriver/acceptance-sdk/setup/globalTeardown.mjs +11 -0
- package/testdriver/acceptance-sdk/setup/testHelpers.mjs +420 -0
- package/testdriver/acceptance-sdk/setup/vitestSetup.mjs +40 -0
- package/testdriver/acceptance-sdk/sully-ai.test.mjs +234 -0
- package/testdriver/acceptance-sdk/test-console-logs.test.mjs +42 -0
- package/testdriver/acceptance-sdk/type-checking-demo.js +49 -0
- package/testdriver/acceptance-sdk/type.test.mjs +45 -0
- package/verify-element-api.js +89 -0
- package/verify-types.js +0 -0
- package/vitest.config.example.js +19 -0
- package/vitest.config.mjs +66 -0
- package/vitest.config.mjs.bak +44 -0
- package/.github/workflows/acceptance-v6.yml +0 -169
- package/.vscode/mcp.json +0 -9
- package/docs/overview/comparison.mdx +0 -82
- package/testdriver/lifecycle/prerun.yaml +0 -17
- /package/{testdriver/examples/desktop/lifecycle/prerun.yaml → .env.example} +0 -0
- /package/{testdriver → _testdriver}/acceptance/assert.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/dashcam.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/embed.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/exec-js.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/exec-output.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/exec-shell.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/focus-window.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/hover-image.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/hover-text-with-description.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/hover-text.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/if-else.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/match-image.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/press-keys.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/prompt.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/remember.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/screenshots/cart.png +0 -0
- /package/{testdriver → _testdriver}/acceptance/scroll-keyboard.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/scroll-until-image.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/scroll-until-text.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/scroll.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/snippets/match-cart.yaml +0 -0
- /package/{testdriver → _testdriver}/acceptance/type.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/failure.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/hover-text.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/lifecycle/postrun.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/lifecycle/prerun.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/lifecycle/provision.yaml +0 -0
- /package/{testdriver → _testdriver}/behavior/secrets.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/dashcam-chrome.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/exec-pwsh-multiline.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/js-exception.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/js-promise.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/lifecycle/postrun.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/prompt-in-middle.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/prompt-nested.yaml +0 -0
- /package/{testdriver → _testdriver}/edge-cases/success-test.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/android/example.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/android/lifecycle/postrun.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/android/lifecycle/provision.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/android/readme.md +0 -0
- /package/{testdriver → _testdriver}/examples/chrome-extension/lifecycle/provision.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/desktop/lifecycle/provision.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/vscode-extension/lifecycle/provision.yaml +0 -0
- /package/{testdriver → _testdriver}/examples/web/lifecycle/postrun.yaml +0 -0
- /package/docs/{account → v6/account}/dashboard.mdx +0 -0
- /package/docs/{account → v6/account}/enterprise.mdx +0 -0
- /package/docs/{account → v6/account}/pricing.mdx +0 -0
- /package/docs/{account → v6/account}/projects.mdx +0 -0
- /package/docs/{account → v6/account}/team.mdx +0 -0
- /package/docs/{action → v6/action}/ami.mdx +0 -0
- /package/docs/{action → v6/action}/performance.mdx +0 -0
- /package/docs/{action → v6/action}/secrets.mdx +0 -0
- /package/docs/{apps → v6/apps}/chrome-extensions.mdx +0 -0
- /package/docs/{apps → v6/apps}/desktop-apps.mdx +0 -0
- /package/docs/{apps → v6/apps}/mobile-apps.mdx +0 -0
- /package/docs/{apps → v6/apps}/static-websites.mdx +0 -0
- /package/docs/{apps → v6/apps}/tauri-apps.mdx +0 -0
- /package/docs/{bugs → v6/bugs}/jira.mdx +0 -0
- /package/docs/{cli → v6/cli}/overview.mdx +0 -0
- /package/docs/{commands → v6/commands}/assert.mdx +0 -0
- /package/docs/{commands → v6/commands}/exec.mdx +0 -0
- /package/docs/{commands → v6/commands}/focus-application.mdx +0 -0
- /package/docs/{commands → v6/commands}/hover-image.mdx +0 -0
- /package/docs/{commands → v6/commands}/hover-text.mdx +0 -0
- /package/docs/{commands → v6/commands}/if.mdx +0 -0
- /package/docs/{commands → v6/commands}/match-image.mdx +0 -0
- /package/docs/{commands → v6/commands}/press-keys.mdx +0 -0
- /package/docs/{commands → v6/commands}/remember.mdx +0 -0
- /package/docs/{commands → v6/commands}/run.mdx +0 -0
- /package/docs/{commands → v6/commands}/scroll-until-image.mdx +0 -0
- /package/docs/{commands → v6/commands}/scroll-until-text.mdx +0 -0
- /package/docs/{commands → v6/commands}/scroll.mdx +0 -0
- /package/docs/{commands → v6/commands}/type.mdx +0 -0
- /package/docs/{commands → v6/commands}/wait-for-image.mdx +0 -0
- /package/docs/{commands → v6/commands}/wait-for-text.mdx +0 -0
- /package/docs/{commands → v6/commands}/wait.mdx +0 -0
- /package/docs/{exporting → v6/exporting}/junit.mdx +0 -0
- /package/docs/{exporting → v6/exporting}/playwright.mdx +0 -0
- /package/docs/{features → v6/features}/auto-healing.mdx +0 -0
- /package/docs/{features → v6/features}/generation.mdx +0 -0
- /package/docs/{features → v6/features}/parallel-testing.mdx +0 -0
- /package/docs/{features → v6/features}/reusable-snippets.mdx +0 -0
- /package/docs/{features → v6/features}/selectorless.mdx +0 -0
- /package/docs/{features → v6/features}/visual-assertions.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/ci.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/cli.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/editing.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/playwright.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/running.mdx +0 -0
- /package/docs/{getting-started → v6/getting-started}/vscode.mdx +0 -0
- /package/docs/{guide → v6/guide}/assertions.mdx +0 -0
- /package/docs/{guide → v6/guide}/authentication.mdx +0 -0
- /package/docs/{guide → v6/guide}/code.mdx +0 -0
- /package/docs/{guide → v6/guide}/locating.mdx +0 -0
- /package/docs/{guide → v6/guide}/protips.mdx +0 -0
- /package/docs/{guide → v6/guide}/variables.mdx +0 -0
- /package/docs/{guide → v6/guide}/waiting.mdx +0 -0
- /package/docs/{importing → v6/importing}/csv.mdx +0 -0
- /package/docs/{importing → v6/importing}/gherkin.mdx +0 -0
- /package/docs/{importing → v6/importing}/jira.mdx +0 -0
- /package/docs/{importing → v6/importing}/testrail.mdx +0 -0
- /package/docs/{integrations → v6/integrations}/electron.mdx +0 -0
- /package/docs/{integrations → v6/integrations}/netlify.mdx +0 -0
- /package/docs/{integrations → v6/integrations}/vercel.mdx +0 -0
- /package/docs/{interactive → v6/interactive}/explore.mdx +0 -0
- /package/docs/{interactive → v6/interactive}/run.mdx +0 -0
- /package/docs/{interactive → v6/interactive}/save.mdx +0 -0
- /package/docs/{overview → v6/overview}/faq.mdx +0 -0
- /package/docs/{overview → v6/overview}/performance.mdx +0 -0
- /package/docs/{overview → v6/overview}/quickstart.mdx +0 -0
- /package/docs/{overview → v6/overview}/what-is-testdriver.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/ai-chatbot.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/cookie-banner.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/file-upload.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/form-filling.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/log-in.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/pdf-generation.mdx +0 -0
- /package/docs/{scenarios → v6/scenarios}/spell-check.mdx +0 -0
- /package/docs/{security → v6/security}/action.mdx +0 -0
- /package/docs/{security → v6/security}/agent.mdx +0 -0
- /package/docs/{security → v6/security}/platform.mdx +0 -0
- /package/docs/{tutorials → v6/tutorials}/advanced-test.mdx +0 -0
- /package/docs/{tutorials → v6/tutorials}/basic-test.mdx +0 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { setTestRunInfo } from "./shared-test-state.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Simple logger for the vitest plugin
|
|
9
|
+
* Supports log levels: debug, info, warn, error
|
|
10
|
+
* Control via TD_LOG_LEVEL environment variable (default: "info")
|
|
11
|
+
* Set TD_LOG_LEVEL=debug for verbose output
|
|
12
|
+
*/
|
|
13
|
+
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
14
|
+
const currentLogLevel = LOG_LEVELS[process.env.TD_LOG_LEVEL?.toLowerCase()] ?? LOG_LEVELS.info;
|
|
15
|
+
|
|
16
|
+
const logger = {
|
|
17
|
+
debug: (...args) => {
|
|
18
|
+
if (currentLogLevel <= LOG_LEVELS.debug) {
|
|
19
|
+
console.log("[TestDriver]", ...args);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
info: (...args) => {
|
|
23
|
+
if (currentLogLevel <= LOG_LEVELS.info) {
|
|
24
|
+
console.log("[TestDriver]", ...args);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
warn: (...args) => {
|
|
28
|
+
if (currentLogLevel <= LOG_LEVELS.warn) {
|
|
29
|
+
console.warn("[TestDriver]", ...args);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
error: (...args) => {
|
|
33
|
+
if (currentLogLevel <= LOG_LEVELS.error) {
|
|
34
|
+
console.error("[TestDriver]", ...args);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Timeout wrapper for promises
|
|
41
|
+
* @param {Promise} promise - Promise to wrap
|
|
42
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
43
|
+
* @param {string} operationName - Name of operation for error message
|
|
44
|
+
* @returns {Promise} Promise that rejects if timeout is reached
|
|
45
|
+
*/
|
|
46
|
+
function withTimeout(promise, timeoutMs, operationName) {
|
|
47
|
+
return Promise.race([
|
|
48
|
+
promise,
|
|
49
|
+
new Promise((_, reject) =>
|
|
50
|
+
setTimeout(
|
|
51
|
+
() =>
|
|
52
|
+
reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)),
|
|
53
|
+
timeoutMs,
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Vitest Plugin for TestDriver
|
|
61
|
+
*
|
|
62
|
+
* Records test runs, test cases, and associates them with dashcam recordings.
|
|
63
|
+
* Uses plugin architecture for better global state management.
|
|
64
|
+
*
|
|
65
|
+
* ## How it works:
|
|
66
|
+
*
|
|
67
|
+
* 1. **Plugin State**: All state is managed in a single `pluginState` object
|
|
68
|
+
* - No class instances or complex scoping
|
|
69
|
+
* - Easy to access from anywhere in the plugin
|
|
70
|
+
* - Dashcam URLs tracked in memory (no temp files!)
|
|
71
|
+
*
|
|
72
|
+
* 2. **Dashcam URL Registration**: Tests register dashcam URLs via simple API
|
|
73
|
+
* - `globalThis.__testdriverPlugin.registerDashcamUrl(testId, url, platform)`
|
|
74
|
+
* - No file system operations
|
|
75
|
+
* - No complex matching logic
|
|
76
|
+
* - Direct association via test ID
|
|
77
|
+
*
|
|
78
|
+
* 3. **Test Recording Flow**:
|
|
79
|
+
* - `onTestRunStart`: Create test run record
|
|
80
|
+
* - `onTestCaseReady`: Track test start time
|
|
81
|
+
* - `onTestCaseResult`: Record individual test result (immediate)
|
|
82
|
+
* - `onTestRunEnd`: Complete test run with final stats
|
|
83
|
+
*
|
|
84
|
+
* 4. **Platform Detection**: Automatically detects platform from SDK client
|
|
85
|
+
* - No manual configuration needed
|
|
86
|
+
* - Stored when dashcam URL is registered
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
// Shared state that can be imported by both the reporter and setup files
|
|
90
|
+
export const pluginState = {
|
|
91
|
+
testRun: null,
|
|
92
|
+
testRunId: null,
|
|
93
|
+
testRunCompleted: false,
|
|
94
|
+
client: null,
|
|
95
|
+
startTime: null,
|
|
96
|
+
testCases: new Map(),
|
|
97
|
+
token: null,
|
|
98
|
+
detectedPlatform: null,
|
|
99
|
+
pendingTestCaseRecords: new Set(),
|
|
100
|
+
ciProvider: null,
|
|
101
|
+
gitInfo: {},
|
|
102
|
+
apiKey: null,
|
|
103
|
+
apiRoot: null,
|
|
104
|
+
// TestDriver options to pass to all instances
|
|
105
|
+
testDriverOptions: {},
|
|
106
|
+
// Dashcam URL tracking (in-memory, no files needed!)
|
|
107
|
+
dashcamUrls: new Map(), // testId -> dashcamUrl
|
|
108
|
+
lastDashcamUrl: null, // Fallback for when test ID isn't available
|
|
109
|
+
// Suite-level test run tracking
|
|
110
|
+
suiteTestRuns: new Map(), // suiteId -> { runId, testRunDbId, token }
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Export functions that can be used by the reporter or tests
|
|
114
|
+
export function registerDashcamUrl(testId, url, platform) {
|
|
115
|
+
logger.debug(`Registering dashcam URL for test ${testId}:`, url);
|
|
116
|
+
pluginState.dashcamUrls.set(testId, { url, platform });
|
|
117
|
+
pluginState.lastDashcamUrl = url;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getDashcamUrl(testId) {
|
|
121
|
+
return pluginState.dashcamUrls.get(testId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function clearDashcamUrls() {
|
|
125
|
+
pluginState.dashcamUrls.clear();
|
|
126
|
+
pluginState.lastDashcamUrl = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getSuiteTestRun(suiteId) {
|
|
130
|
+
return pluginState.suiteTestRuns.get(suiteId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function setSuiteTestRun(suiteId, runData) {
|
|
134
|
+
logger.debug(`Setting test run for suite ${suiteId}:`, runData);
|
|
135
|
+
pluginState.suiteTestRuns.set(suiteId, runData);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function clearSuiteTestRun(suiteId) {
|
|
139
|
+
pluginState.suiteTestRuns.delete(suiteId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getPluginState() {
|
|
143
|
+
return pluginState;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Export API helper functions for direct use from tests
|
|
147
|
+
export async function authenticateWithApiKey(apiKey, apiRoot) {
|
|
148
|
+
const url = `${apiRoot}/auth/exchange-api-key`;
|
|
149
|
+
const response = await withTimeout(
|
|
150
|
+
fetch(url, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: {
|
|
153
|
+
"Content-Type": "application/json",
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({ apiKey }),
|
|
156
|
+
}),
|
|
157
|
+
10000,
|
|
158
|
+
"Authentication",
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Authentication failed: ${response.status} ${response.statusText}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const data = await response.json();
|
|
168
|
+
return data.token;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function createTestRunDirect(token, apiRoot, testRunData) {
|
|
172
|
+
const url = `${apiRoot}/api/v1/testdriver/test-run-create`;
|
|
173
|
+
const response = await withTimeout(
|
|
174
|
+
fetch(url, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: {
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
Authorization: `Bearer ${token}`,
|
|
179
|
+
},
|
|
180
|
+
body: JSON.stringify(testRunData),
|
|
181
|
+
}),
|
|
182
|
+
10000,
|
|
183
|
+
"Create Test Run",
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
const errorText = await response.text();
|
|
188
|
+
throw new Error(
|
|
189
|
+
`API error: ${response.status} ${response.statusText} - ${errorText}`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return await response.json();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function recordTestCaseDirect(token, apiRoot, testCaseData) {
|
|
197
|
+
const url = `${apiRoot}/api/v1/testdriver/test-case-create`;
|
|
198
|
+
const response = await withTimeout(
|
|
199
|
+
fetch(url, {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: {
|
|
202
|
+
"Content-Type": "application/json",
|
|
203
|
+
Authorization: `Bearer ${token}`,
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify(testCaseData),
|
|
206
|
+
}),
|
|
207
|
+
10000,
|
|
208
|
+
"Record Test Case",
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const errorText = await response.text();
|
|
213
|
+
throw new Error(
|
|
214
|
+
`API error: ${response.status} ${response.statusText} - ${errorText}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return await response.json();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Handle process termination and mark test run as cancelled
|
|
223
|
+
*/
|
|
224
|
+
async function handleProcessExit() {
|
|
225
|
+
if (!pluginState.testRun || !pluginState.testRunId) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
logger.info("Process interrupted, marking test run as cancelled...");
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const stats = {
|
|
233
|
+
totalTests: pluginState.testCases.size,
|
|
234
|
+
passedTests: 0,
|
|
235
|
+
failedTests: 0,
|
|
236
|
+
skippedTests: 0,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const completeData = {
|
|
240
|
+
runId: pluginState.testRunId,
|
|
241
|
+
status: "cancelled",
|
|
242
|
+
totalTests: stats.totalTests,
|
|
243
|
+
passedTests: stats.passedTests,
|
|
244
|
+
failedTests: stats.failedTests,
|
|
245
|
+
skippedTests: stats.skippedTests,
|
|
246
|
+
duration: Date.now() - pluginState.startTime,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Update platform if detected
|
|
250
|
+
const platform = getPlatform();
|
|
251
|
+
if (platform) {
|
|
252
|
+
completeData.platform = platform;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await completeTestRun(completeData);
|
|
256
|
+
logger.info("✅ Test run marked as cancelled");
|
|
257
|
+
} catch (error) {
|
|
258
|
+
logger.error("Failed to mark test run as cancelled:", error.message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Set up process exit handlers
|
|
263
|
+
let exitHandlersRegistered = false;
|
|
264
|
+
|
|
265
|
+
function registerExitHandlers() {
|
|
266
|
+
if (exitHandlersRegistered) return;
|
|
267
|
+
exitHandlersRegistered = true;
|
|
268
|
+
|
|
269
|
+
// Handle Ctrl+C
|
|
270
|
+
process.on("SIGINT", async () => {
|
|
271
|
+
await handleProcessExit();
|
|
272
|
+
process.exit(130); // Standard exit code for SIGINT
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Handle kill command
|
|
276
|
+
process.on("SIGTERM", async () => {
|
|
277
|
+
await handleProcessExit();
|
|
278
|
+
process.exit(143); // Standard exit code for SIGTERM
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Handle unexpected exits
|
|
282
|
+
process.on("beforeExit", async () => {
|
|
283
|
+
// Only handle if test run is still running (hasn't been completed normally)
|
|
284
|
+
if (pluginState.testRun && !pluginState.testRunCompleted) {
|
|
285
|
+
await handleProcessExit();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create the TestDriver Vitest plugin
|
|
292
|
+
* This sets up global state and provides the registration API
|
|
293
|
+
*/
|
|
294
|
+
export default function testDriverPlugin(options = {}) {
|
|
295
|
+
// Initialize plugin state with options
|
|
296
|
+
pluginState.apiKey = options.apiKey;
|
|
297
|
+
pluginState.apiRoot =
|
|
298
|
+
options.apiRoot || process.env.TD_API_ROOT || "http://localhost:1337";
|
|
299
|
+
pluginState.ciProvider = detectCI();
|
|
300
|
+
pluginState.gitInfo = getGitInfo();
|
|
301
|
+
|
|
302
|
+
// Store TestDriver-specific options (excluding plugin-specific ones)
|
|
303
|
+
const { apiKey, apiRoot, ...testDriverOptions } = options;
|
|
304
|
+
pluginState.testDriverOptions = testDriverOptions;
|
|
305
|
+
|
|
306
|
+
// Register process exit handlers to handle cancellation
|
|
307
|
+
registerExitHandlers();
|
|
308
|
+
|
|
309
|
+
// Note: globalThis setup happens in vitestSetup.mjs for worker processes
|
|
310
|
+
logger.debug("Initialized with API root:", pluginState.apiRoot);
|
|
311
|
+
if (Object.keys(testDriverOptions).length > 0) {
|
|
312
|
+
logger.debug("Global TestDriver options:", testDriverOptions);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return new TestDriverReporter(options);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* TestDriver Reporter Class
|
|
320
|
+
* Handles Vitest test lifecycle events
|
|
321
|
+
*/
|
|
322
|
+
class TestDriverReporter {
|
|
323
|
+
constructor(options = {}) {
|
|
324
|
+
this.options = options;
|
|
325
|
+
logger.debug("Reporter created");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async onInit(ctx) {
|
|
329
|
+
this.ctx = ctx;
|
|
330
|
+
logger.debug("onInit called");
|
|
331
|
+
|
|
332
|
+
// Initialize test run
|
|
333
|
+
await this.initializeTestRun();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async initializeTestRun() {
|
|
337
|
+
logger.debug("Initializing test run...");
|
|
338
|
+
|
|
339
|
+
// Check if we should enable the reporter
|
|
340
|
+
if (!pluginState.apiKey) {
|
|
341
|
+
logger.debug("No API key provided, skipping test recording");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
// Exchange API key for JWT token
|
|
347
|
+
await authenticate();
|
|
348
|
+
|
|
349
|
+
// Generate unique run ID
|
|
350
|
+
pluginState.testRunId = generateRunId();
|
|
351
|
+
pluginState.startTime = Date.now();
|
|
352
|
+
pluginState.testRunCompleted = false; // Reset completion flag
|
|
353
|
+
|
|
354
|
+
// Create test run via direct API call
|
|
355
|
+
const testRunData = {
|
|
356
|
+
runId: pluginState.testRunId,
|
|
357
|
+
suiteName: getSuiteName(),
|
|
358
|
+
...pluginState.gitInfo,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Session ID will be added from the first test result file that includes it
|
|
362
|
+
|
|
363
|
+
// Only add ciProvider if it's not null
|
|
364
|
+
if (pluginState.ciProvider) {
|
|
365
|
+
testRunData.ciProvider = pluginState.ciProvider;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Platform will be set from the first test result file
|
|
369
|
+
// Default to linux if no tests write platform info
|
|
370
|
+
testRunData.platform = "linux";
|
|
371
|
+
|
|
372
|
+
pluginState.testRun = await createTestRun(testRunData);
|
|
373
|
+
|
|
374
|
+
// Store in environment variables for worker processes to access
|
|
375
|
+
process.env.TD_TEST_RUN_ID = pluginState.testRunId;
|
|
376
|
+
process.env.TD_TEST_RUN_DB_ID = pluginState.testRun.data?.id || "";
|
|
377
|
+
process.env.TD_TEST_RUN_TOKEN = pluginState.token;
|
|
378
|
+
|
|
379
|
+
// Also store in shared state module (won't work across processes but good for main)
|
|
380
|
+
setTestRunInfo({
|
|
381
|
+
testRun: pluginState.testRun,
|
|
382
|
+
testRunId: pluginState.testRunId,
|
|
383
|
+
token: pluginState.token,
|
|
384
|
+
apiKey: pluginState.apiKey,
|
|
385
|
+
apiRoot: pluginState.apiRoot,
|
|
386
|
+
startTime: pluginState.startTime,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
logger.info(`Test run created: ${pluginState.testRunId}`);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
logger.error("Failed to initialize:", error.message);
|
|
392
|
+
pluginState.apiKey = null;
|
|
393
|
+
pluginState.token = null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async onTestRunEnd(testModules, unhandledErrors, reason) {
|
|
398
|
+
logger.debug("Test run ending with reason:", reason);
|
|
399
|
+
|
|
400
|
+
if (!pluginState.apiKey) {
|
|
401
|
+
logger.debug("Skipping completion - no API key");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!pluginState.testRun) {
|
|
406
|
+
logger.debug("Skipping completion - no test run created");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
// Calculate statistics from testModules
|
|
412
|
+
const stats = calculateStatsFromModules(testModules);
|
|
413
|
+
|
|
414
|
+
logger.debug("Stats:", stats);
|
|
415
|
+
|
|
416
|
+
// Determine overall status based on reason and stats
|
|
417
|
+
let status = "passed";
|
|
418
|
+
if (reason === "failed" || stats.failedTests > 0) {
|
|
419
|
+
status = "failed";
|
|
420
|
+
} else if (reason === "interrupted") {
|
|
421
|
+
status = "cancelled";
|
|
422
|
+
} else if (stats.totalTests === 0) {
|
|
423
|
+
status = "cancelled";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Complete test run via API
|
|
427
|
+
logger.debug(`Completing test run ${pluginState.testRunId} with status: ${status}`);
|
|
428
|
+
|
|
429
|
+
const completeData = {
|
|
430
|
+
runId: pluginState.testRunId,
|
|
431
|
+
status,
|
|
432
|
+
totalTests: stats.totalTests,
|
|
433
|
+
passedTests: stats.passedTests,
|
|
434
|
+
failedTests: stats.failedTests,
|
|
435
|
+
skippedTests: stats.skippedTests,
|
|
436
|
+
duration: Date.now() - pluginState.startTime,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// Update platform if detected from test results
|
|
440
|
+
const platform = getPlatform();
|
|
441
|
+
if (platform) {
|
|
442
|
+
completeData.platform = platform;
|
|
443
|
+
logger.debug(`Updating test run with platform: ${platform}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Wait for any pending operations (shouldn't be any, but just in case)
|
|
447
|
+
if (pluginState.pendingTestCaseRecords.size > 0) {
|
|
448
|
+
logger.debug(`Waiting for ${pluginState.pendingTestCaseRecords.size} pending operations...`);
|
|
449
|
+
await Promise.all(Array.from(pluginState.pendingTestCaseRecords));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Test cases are reported directly from teardownTest
|
|
453
|
+
logger.debug("All test cases reported from teardown");
|
|
454
|
+
|
|
455
|
+
const completeResponse = await completeTestRun(completeData);
|
|
456
|
+
logger.debug("Test run completion API response:", completeResponse);
|
|
457
|
+
|
|
458
|
+
// Mark test run as completed to prevent duplicate completion
|
|
459
|
+
pluginState.testRunCompleted = true;
|
|
460
|
+
|
|
461
|
+
logger.info(`✅ Test run completed: ${stats.passedTests}/${stats.totalTests} passed`);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
logger.error("Failed to complete test run:", error.message);
|
|
464
|
+
logger.debug("Error stack:", error.stack);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
onTestCaseReady(test) {
|
|
469
|
+
if (!pluginState.apiKey || !pluginState.testRun) return;
|
|
470
|
+
|
|
471
|
+
pluginState.testCases.set(test.id, {
|
|
472
|
+
test,
|
|
473
|
+
startTime: Date.now(),
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Try to detect platform from test context
|
|
477
|
+
detectPlatformFromTest(test);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async onTestCaseResult(test) {
|
|
481
|
+
if (!pluginState.apiKey || !pluginState.testRun) return;
|
|
482
|
+
|
|
483
|
+
const result = test.result();
|
|
484
|
+
const status =
|
|
485
|
+
result.state === "passed"
|
|
486
|
+
? "passed"
|
|
487
|
+
: result.state === "skipped"
|
|
488
|
+
? "skipped"
|
|
489
|
+
: "failed";
|
|
490
|
+
|
|
491
|
+
logger.info(`Test case completed: ${test.name} (${status})`);
|
|
492
|
+
|
|
493
|
+
// Calculate duration from tracked start time
|
|
494
|
+
const testCase = pluginState.testCases.get(test.id);
|
|
495
|
+
const duration = testCase ? Date.now() - testCase.startTime : 0;
|
|
496
|
+
|
|
497
|
+
logger.debug(`Calculated duration: ${duration}ms (startTime: ${testCase?.startTime}, now: ${Date.now()})`);
|
|
498
|
+
|
|
499
|
+
// Read test metadata from file (cross-process communication)
|
|
500
|
+
let dashcamUrl = null;
|
|
501
|
+
let sessionId = null;
|
|
502
|
+
let testFile = "unknown";
|
|
503
|
+
let testOrder = 0;
|
|
504
|
+
|
|
505
|
+
const testResultFile = path.join(
|
|
506
|
+
os.tmpdir(),
|
|
507
|
+
"testdriver-results",
|
|
508
|
+
`${test.id}.json`,
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
logger.debug(`Looking for test result file with test.id: ${test.id}`);
|
|
512
|
+
logger.debug(`Test result file path: ${testResultFile}`);
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
if (fs.existsSync(testResultFile)) {
|
|
516
|
+
const testResult = JSON.parse(fs.readFileSync(testResultFile, "utf-8"));
|
|
517
|
+
dashcamUrl = testResult.dashcamUrl || null;
|
|
518
|
+
const platform = testResult.platform || null;
|
|
519
|
+
sessionId = testResult.sessionId || null;
|
|
520
|
+
testFile =
|
|
521
|
+
testResult.testFile ||
|
|
522
|
+
test.file?.filepath ||
|
|
523
|
+
test.file?.name ||
|
|
524
|
+
"unknown";
|
|
525
|
+
testOrder =
|
|
526
|
+
testResult.testOrder !== undefined ? testResult.testOrder : 0;
|
|
527
|
+
// Don't override duration from file - use Vitest's result.duration
|
|
528
|
+
// duration is already set above from result.duration
|
|
529
|
+
|
|
530
|
+
logger.debug(`Read from file - dashcam: ${dashcamUrl}, platform: ${platform}, sessionId: ${sessionId}, testFile: ${testFile}, testOrder: ${testOrder}, duration: ${duration}ms`);
|
|
531
|
+
|
|
532
|
+
// Update test run platform from first test that reports it
|
|
533
|
+
if (platform && !pluginState.detectedPlatform) {
|
|
534
|
+
pluginState.detectedPlatform = platform;
|
|
535
|
+
logger.debug(`Detected platform from test: ${platform}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Clean up the file after reading
|
|
539
|
+
try {
|
|
540
|
+
fs.unlinkSync(testResultFile);
|
|
541
|
+
} catch {
|
|
542
|
+
// Ignore cleanup errors
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
logger.debug(`No result file found for test: ${test.id}`);
|
|
546
|
+
// Fallback to test object properties - try multiple sources
|
|
547
|
+
// In Vitest, the file path is on test.module.task.filepath
|
|
548
|
+
testFile =
|
|
549
|
+
test.module?.task?.filepath ||
|
|
550
|
+
test.module?.file?.filepath ||
|
|
551
|
+
test.module?.file?.name ||
|
|
552
|
+
test.file?.filepath ||
|
|
553
|
+
test.file?.name ||
|
|
554
|
+
test.suite?.file?.filepath ||
|
|
555
|
+
test.suite?.file?.name ||
|
|
556
|
+
test.location?.file ||
|
|
557
|
+
"unknown";
|
|
558
|
+
logger.debug(`Resolved testFile: ${testFile}`);
|
|
559
|
+
}
|
|
560
|
+
} catch (error) {
|
|
561
|
+
logger.error("Failed to read test result file:", error.message);
|
|
562
|
+
// Fallback to test object properties - try multiple sources
|
|
563
|
+
// In Vitest, the file path is on test.module.task.filepath
|
|
564
|
+
testFile =
|
|
565
|
+
test.module?.task?.filepath ||
|
|
566
|
+
test.module?.file?.filepath ||
|
|
567
|
+
test.module?.file?.name ||
|
|
568
|
+
test.file?.filepath ||
|
|
569
|
+
test.file?.name ||
|
|
570
|
+
test.suite?.file?.filepath ||
|
|
571
|
+
test.suite?.file?.name ||
|
|
572
|
+
test.location?.file ||
|
|
573
|
+
"unknown";
|
|
574
|
+
logger.debug(`Resolved testFile from fallback: ${testFile}`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Get test run info from environment variables
|
|
578
|
+
const testRunId = process.env.TD_TEST_RUN_ID;
|
|
579
|
+
const token = process.env.TD_TEST_RUN_TOKEN;
|
|
580
|
+
|
|
581
|
+
if (!testRunId || !token) {
|
|
582
|
+
logger.warn(`Test run not initialized, skipping test case recording for: ${test.name}`);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
let errorMessage = null;
|
|
588
|
+
let errorStack = null;
|
|
589
|
+
|
|
590
|
+
if (
|
|
591
|
+
result.state === "failed" &&
|
|
592
|
+
result.errors &&
|
|
593
|
+
result.errors.length > 0
|
|
594
|
+
) {
|
|
595
|
+
const error = result.errors[0];
|
|
596
|
+
errorMessage = error.message;
|
|
597
|
+
errorStack = error.stack;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const suiteName = test.suite?.name;
|
|
601
|
+
const startTime = Date.now() - duration; // Calculate start time from duration
|
|
602
|
+
|
|
603
|
+
// Record test case with all metadata
|
|
604
|
+
const testCaseData = {
|
|
605
|
+
runId: testRunId,
|
|
606
|
+
testName: test.name,
|
|
607
|
+
testFile: testFile,
|
|
608
|
+
testOrder: testOrder,
|
|
609
|
+
status,
|
|
610
|
+
startTime: startTime,
|
|
611
|
+
endTime: Date.now(),
|
|
612
|
+
duration: duration,
|
|
613
|
+
retries: result.retryCount || 0,
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// Add sessionId if available
|
|
617
|
+
if (sessionId) {
|
|
618
|
+
testCaseData.sessionId = sessionId;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Only include replayUrl if we have a valid dashcam URL
|
|
622
|
+
if (dashcamUrl) {
|
|
623
|
+
testCaseData.replayUrl = dashcamUrl;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (suiteName) testCaseData.suiteName = suiteName;
|
|
627
|
+
if (errorMessage) testCaseData.errorMessage = errorMessage;
|
|
628
|
+
if (errorStack) testCaseData.errorStack = errorStack;
|
|
629
|
+
|
|
630
|
+
logger.debug(`Recording test case: ${test.name} (${status}) with testFile: ${testFile}, testOrder: ${testOrder}, duration: ${duration}ms, replay: ${dashcamUrl ? "yes" : "no"}`);
|
|
631
|
+
|
|
632
|
+
const testCaseResponse = await recordTestCaseDirect(
|
|
633
|
+
token,
|
|
634
|
+
pluginState.apiRoot,
|
|
635
|
+
testCaseData,
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
const testCaseDbId = testCaseResponse.data?.id;
|
|
639
|
+
const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
|
|
640
|
+
|
|
641
|
+
logger.debug(`Reported test case to API${dashcamUrl ? " with dashcam URL" : ""}`);
|
|
642
|
+
logger.info(`🔗 View test: ${pluginState.apiRoot.replace("testdriver-api.onrender.com", "app.testdriver.ai")}/runs/${testRunDbId}/${testCaseDbId}`);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
logger.error("Failed to report test case:", error.message);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ============================================================================
|
|
650
|
+
// Helper Functions
|
|
651
|
+
// ============================================================================
|
|
652
|
+
|
|
653
|
+
function generateRunId() {
|
|
654
|
+
return `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function getSuiteName() {
|
|
658
|
+
return process.env.npm_package_name || path.basename(process.cwd());
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function getPlatform() {
|
|
662
|
+
// First try to get platform from SDK client detected during test execution
|
|
663
|
+
if (pluginState.detectedPlatform) {
|
|
664
|
+
logger.debug(`Using platform from SDK client: ${pluginState.detectedPlatform}`);
|
|
665
|
+
return pluginState.detectedPlatform;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
logger.debug("Platform not yet detected from client");
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function detectPlatformFromTest(test) {
|
|
673
|
+
// Check if testdriver client is accessible via test context
|
|
674
|
+
const client = test.context?.testdriver || test.meta?.testdriver;
|
|
675
|
+
|
|
676
|
+
if (client && client.os) {
|
|
677
|
+
// Normalize platform value
|
|
678
|
+
let platform = client.os.toLowerCase();
|
|
679
|
+
if (platform === "darwin" || platform === "mac") platform = "mac";
|
|
680
|
+
else if (platform === "win32" || platform === "windows")
|
|
681
|
+
platform = "windows";
|
|
682
|
+
else if (platform === "linux") platform = "linux";
|
|
683
|
+
|
|
684
|
+
pluginState.detectedPlatform = platform;
|
|
685
|
+
logger.debug(`Detected platform from test context: ${platform}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function calculateStatsFromModules(testModules) {
|
|
690
|
+
let totalTests = 0;
|
|
691
|
+
let passedTests = 0;
|
|
692
|
+
let failedTests = 0;
|
|
693
|
+
let skippedTests = 0;
|
|
694
|
+
|
|
695
|
+
for (const testModule of testModules) {
|
|
696
|
+
for (const testCase of testModule.children.allTests()) {
|
|
697
|
+
totalTests++;
|
|
698
|
+
const result = testCase.result();
|
|
699
|
+
if (result.state === "passed") passedTests++;
|
|
700
|
+
else if (result.state === "failed") failedTests++;
|
|
701
|
+
else if (result.state === "skipped") skippedTests++;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return { totalTests, passedTests, failedTests, skippedTests };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function detectCI() {
|
|
709
|
+
if (process.env.GITHUB_ACTIONS) return "github";
|
|
710
|
+
if (process.env.GITLAB_CI) return "gitlab";
|
|
711
|
+
if (process.env.CIRCLECI) return "circle";
|
|
712
|
+
if (process.env.TRAVIS) return "travis";
|
|
713
|
+
if (process.env.JENKINS_URL) return "jenkins";
|
|
714
|
+
if (process.env.BUILDKITE) return "buildkite";
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function getGitInfo() {
|
|
719
|
+
const info = {};
|
|
720
|
+
|
|
721
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
722
|
+
if (process.env.GITHUB_REPOSITORY)
|
|
723
|
+
info.repo = process.env.GITHUB_REPOSITORY;
|
|
724
|
+
if (process.env.GITHUB_REF_NAME) info.branch = process.env.GITHUB_REF_NAME;
|
|
725
|
+
if (process.env.GITHUB_SHA) info.commit = process.env.GITHUB_SHA;
|
|
726
|
+
if (process.env.GITHUB_ACTOR) info.author = process.env.GITHUB_ACTOR;
|
|
727
|
+
} else if (process.env.GITLAB_CI) {
|
|
728
|
+
if (process.env.CI_PROJECT_PATH) info.repo = process.env.CI_PROJECT_PATH;
|
|
729
|
+
if (process.env.CI_COMMIT_BRANCH)
|
|
730
|
+
info.branch = process.env.CI_COMMIT_BRANCH;
|
|
731
|
+
if (process.env.CI_COMMIT_SHA) info.commit = process.env.CI_COMMIT_SHA;
|
|
732
|
+
if (process.env.GITLAB_USER_LOGIN)
|
|
733
|
+
info.author = process.env.GITLAB_USER_LOGIN;
|
|
734
|
+
} else if (process.env.CIRCLECI) {
|
|
735
|
+
if (
|
|
736
|
+
process.env.CIRCLE_PROJECT_USERNAME &&
|
|
737
|
+
process.env.CIRCLE_PROJECT_REPONAME
|
|
738
|
+
) {
|
|
739
|
+
info.repo = `${process.env.CIRCLE_PROJECT_USERNAME}/${process.env.CIRCLE_PROJECT_REPONAME}`;
|
|
740
|
+
}
|
|
741
|
+
if (process.env.CIRCLE_BRANCH) info.branch = process.env.CIRCLE_BRANCH;
|
|
742
|
+
if (process.env.CIRCLE_SHA1) info.commit = process.env.CIRCLE_SHA1;
|
|
743
|
+
if (process.env.CIRCLE_USERNAME) info.author = process.env.CIRCLE_USERNAME;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return info;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// ============================================================================
|
|
750
|
+
// API Methods
|
|
751
|
+
// ============================================================================
|
|
752
|
+
|
|
753
|
+
async function authenticate() {
|
|
754
|
+
const url = `${pluginState.apiRoot}/auth/exchange-api-key`;
|
|
755
|
+
const response = await withTimeout(
|
|
756
|
+
fetch(url, {
|
|
757
|
+
method: "POST",
|
|
758
|
+
headers: {
|
|
759
|
+
"Content-Type": "application/json",
|
|
760
|
+
},
|
|
761
|
+
body: JSON.stringify({
|
|
762
|
+
apiKey: pluginState.apiKey,
|
|
763
|
+
}),
|
|
764
|
+
}),
|
|
765
|
+
10000,
|
|
766
|
+
"Internal Authentication",
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
if (!response.ok) {
|
|
770
|
+
throw new Error(
|
|
771
|
+
`Authentication failed: ${response.status} ${response.statusText}`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const data = await response.json();
|
|
776
|
+
pluginState.token = data.token;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function createTestRun(data) {
|
|
780
|
+
const url = `${pluginState.apiRoot}/api/v1/testdriver/test-run-create`;
|
|
781
|
+
const response = await withTimeout(
|
|
782
|
+
fetch(url, {
|
|
783
|
+
method: "POST",
|
|
784
|
+
headers: {
|
|
785
|
+
"Content-Type": "application/json",
|
|
786
|
+
Authorization: `Bearer ${pluginState.token}`,
|
|
787
|
+
},
|
|
788
|
+
body: JSON.stringify(data),
|
|
789
|
+
}),
|
|
790
|
+
10000,
|
|
791
|
+
"Internal Create Test Run",
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
if (!response.ok) {
|
|
795
|
+
const errorText = await response.text();
|
|
796
|
+
throw new Error(
|
|
797
|
+
`API error: ${response.status} ${response.statusText} - ${errorText}`,
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return await response.json();
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function completeTestRun(data) {
|
|
805
|
+
const url = `${pluginState.apiRoot}/api/v1/testdriver/test-run-complete`;
|
|
806
|
+
const response = await withTimeout(
|
|
807
|
+
fetch(url, {
|
|
808
|
+
method: "POST",
|
|
809
|
+
headers: {
|
|
810
|
+
"Content-Type": "application/json",
|
|
811
|
+
Authorization: `Bearer ${pluginState.token}`,
|
|
812
|
+
},
|
|
813
|
+
body: JSON.stringify(data),
|
|
814
|
+
}),
|
|
815
|
+
10000,
|
|
816
|
+
"Internal Complete Test Run",
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
if (!response.ok) {
|
|
820
|
+
const errorText = await response.text();
|
|
821
|
+
throw new Error(
|
|
822
|
+
`API error: ${response.status} ${response.statusText} - ${errorText}`,
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return await response.json();
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Global state setup moved to setup file (vitestSetup.mjs)
|
|
830
|
+
// The setup file imports the exported functions and makes them available globally in worker processes
|