testdriverai 7.1.4 → 7.2.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.yaml +81 -0
- package/.github/workflows/publish.yaml +44 -0
- package/agent/index.js +18 -19
- package/agent/lib/commands.js +321 -121
- package/agent/lib/redraw.js +99 -39
- package/agent/lib/sandbox.js +98 -6
- package/agent/lib/sdk.js +25 -0
- package/agent/lib/system.js +2 -1
- package/agent/lib/validation.js +6 -6
- package/docs/docs.json +211 -101
- package/docs/snippets/tests/type-repeated-replay.mdx +1 -1
- package/docs/v7/_drafts/caching-selectors.mdx +24 -0
- package/docs/v7/api/act.mdx +1 -1
- package/docs/v7/api/assert.mdx +1 -1
- package/docs/v7/api/assertions.mdx +7 -7
- package/docs/v7/api/elements.mdx +78 -0
- package/docs/v7/api/find.mdx +38 -0
- package/docs/v7/api/focusApplication.mdx +2 -2
- package/docs/v7/api/hover.mdx +2 -2
- package/docs/v7/features/ai-native.mdx +57 -71
- package/docs/v7/features/application-logs.mdx +353 -0
- package/docs/v7/features/browser-logs.mdx +414 -0
- package/docs/v7/features/cache-management.mdx +402 -0
- package/docs/v7/features/continuous-testing.mdx +346 -0
- package/docs/v7/features/coverage.mdx +508 -0
- package/docs/v7/features/data-driven-testing.mdx +441 -0
- package/docs/v7/features/easy-to-write.mdx +2 -73
- package/docs/v7/features/enterprise.mdx +155 -39
- package/docs/v7/features/fast.mdx +63 -81
- package/docs/v7/features/managed-sandboxes.mdx +384 -0
- package/docs/v7/features/network-monitoring.mdx +568 -0
- package/docs/v7/features/observable.mdx +3 -22
- package/docs/v7/features/parallel-execution.mdx +381 -0
- package/docs/v7/features/powerful.mdx +1 -1
- package/docs/v7/features/reports.mdx +414 -0
- package/docs/v7/features/sandbox-customization.mdx +229 -0
- package/docs/v7/features/scalable.mdx +217 -2
- package/docs/v7/features/stable.mdx +106 -147
- package/docs/v7/features/system-performance.mdx +616 -0
- package/docs/v7/features/test-analytics.mdx +373 -0
- package/docs/v7/features/test-cases.mdx +393 -0
- package/docs/v7/features/test-replays.mdx +408 -0
- package/docs/v7/features/test-reports.mdx +308 -0
- package/docs/v7/getting-started/{running-and-debugging.mdx → debugging-tests.mdx} +12 -142
- package/docs/v7/getting-started/quickstart.mdx +22 -305
- package/docs/v7/getting-started/running-tests.mdx +173 -0
- package/docs/v7/overview/what-is-testdriver.mdx +2 -14
- package/docs/v7/presets/chrome-extension.mdx +147 -122
- package/interfaces/cli/commands/init.js +3 -3
- package/interfaces/cli/lib/base.js +3 -2
- package/interfaces/logger.js +0 -2
- package/interfaces/shared-test-state.mjs +0 -5
- package/interfaces/vitest-plugin.mjs +69 -42
- package/lib/core/Dashcam.js +65 -66
- package/lib/vitest/hooks.mjs +42 -50
- package/package.json +1 -1
- package/sdk-log-formatter.js +350 -175
- package/sdk.js +431 -116
- package/setup/aws/cloudformation.yaml +2 -2
- package/setup/aws/self-hosted.yml +1 -1
- package/test/testdriver/chrome-extension.test.mjs +55 -72
- package/test/testdriver/element-not-found.test.mjs +2 -1
- package/test/testdriver/hover-image.test.mjs +1 -1
- package/test/testdriver/scroll-until-text.test.mjs +10 -6
- package/test/testdriver/setup/lifecycleHelpers.mjs +19 -24
- package/test/testdriver/setup/testHelpers.mjs +18 -23
- package/vitest.config.mjs +3 -3
- package/.github/workflows/linux-tests.yml +0 -28
- package/docs/v7/getting-started/generating-tests.mdx +0 -525
- package/test/testdriver/auto-cache-key-demo.test.mjs +0 -56
|
@@ -249,6 +249,12 @@ export async function createTestDriver(options = {}) {
|
|
|
249
249
|
// Merge options: plugin global options < test-specific options
|
|
250
250
|
const mergedOptions = { ...pluginOptions, ...options };
|
|
251
251
|
|
|
252
|
+
// Support TD_OS environment variable for specifying target OS (linux, mac, windows)
|
|
253
|
+
// Priority: test options > plugin options > environment variable > default (linux)
|
|
254
|
+
if (!mergedOptions.os && process.env.TD_OS) {
|
|
255
|
+
mergedOptions.os = process.env.TD_OS;
|
|
256
|
+
}
|
|
257
|
+
|
|
252
258
|
// Extract TestDriver-specific options
|
|
253
259
|
const apiKey = mergedOptions.apiKey || process.env.TD_API_KEY;
|
|
254
260
|
|
|
@@ -264,10 +270,8 @@ export async function createTestDriver(options = {}) {
|
|
|
264
270
|
const testdriver = new TestDriverSDK(apiKey, config);
|
|
265
271
|
|
|
266
272
|
// Connect to sandbox
|
|
267
|
-
console.log('[testdriver] Connecting to sandbox...');
|
|
268
273
|
await testdriver.auth();
|
|
269
274
|
await testdriver.connect();
|
|
270
|
-
console.log('[testdriver] ✅ Connected to sandbox');
|
|
271
275
|
|
|
272
276
|
return testdriver;
|
|
273
277
|
}
|
|
@@ -312,9 +316,7 @@ export async function cleanupTestDriver(testdriver) {
|
|
|
312
316
|
if (!testdriver) {
|
|
313
317
|
return;
|
|
314
318
|
}
|
|
315
|
-
|
|
316
|
-
console.log('[testdriver] Cleaning up TestDriver client...');
|
|
317
|
-
|
|
319
|
+
|
|
318
320
|
try {
|
|
319
321
|
// Stop dashcam if it was started
|
|
320
322
|
if (testdriver._dashcam && testdriver._dashcam.recording) {
|
|
@@ -337,7 +339,6 @@ export async function cleanupTestDriver(testdriver) {
|
|
|
337
339
|
}
|
|
338
340
|
|
|
339
341
|
await testdriver.disconnect();
|
|
340
|
-
console.log('✅ Client disconnected');
|
|
341
342
|
} catch (error) {
|
|
342
343
|
console.error('Error disconnecting client:', error);
|
|
343
344
|
}
|
|
@@ -351,7 +352,7 @@ async function handleProcessExit() {
|
|
|
351
352
|
return;
|
|
352
353
|
}
|
|
353
354
|
|
|
354
|
-
logger.
|
|
355
|
+
logger.debug("Process interrupted, marking test run as cancelled...");
|
|
355
356
|
|
|
356
357
|
try {
|
|
357
358
|
const stats = {
|
|
@@ -378,7 +379,7 @@ async function handleProcessExit() {
|
|
|
378
379
|
}
|
|
379
380
|
|
|
380
381
|
await completeTestRun(completeData);
|
|
381
|
-
logger.
|
|
382
|
+
logger.debug("✅ Test run marked as cancelled");
|
|
382
383
|
} catch (error) {
|
|
383
384
|
logger.error("Failed to mark test run as cancelled:", error.message);
|
|
384
385
|
}
|
|
@@ -494,8 +495,6 @@ class TestDriverReporter {
|
|
|
494
495
|
return;
|
|
495
496
|
}
|
|
496
497
|
|
|
497
|
-
logger.info("Starting test run initialization with API key...");
|
|
498
|
-
|
|
499
498
|
try {
|
|
500
499
|
// Exchange API key for JWT token
|
|
501
500
|
logger.debug("Authenticating with API...");
|
|
@@ -544,7 +543,7 @@ class TestDriverReporter {
|
|
|
544
543
|
startTime: pluginState.startTime,
|
|
545
544
|
});
|
|
546
545
|
|
|
547
|
-
logger.
|
|
546
|
+
logger.debug(`Test run created: ${pluginState.testRunId}`);
|
|
548
547
|
} catch (error) {
|
|
549
548
|
logger.error("Failed to initialize:", error.message);
|
|
550
549
|
pluginState.apiKey = null;
|
|
@@ -566,7 +565,7 @@ class TestDriverReporter {
|
|
|
566
565
|
return;
|
|
567
566
|
}
|
|
568
567
|
|
|
569
|
-
logger.
|
|
568
|
+
logger.debug("Completing test run...");
|
|
570
569
|
|
|
571
570
|
try {
|
|
572
571
|
// Calculate statistics from testModules
|
|
@@ -574,14 +573,17 @@ class TestDriverReporter {
|
|
|
574
573
|
|
|
575
574
|
logger.debug("Stats:", stats);
|
|
576
575
|
|
|
577
|
-
// Determine overall status based on reason
|
|
576
|
+
// Determine overall status based on stats (not reason, which is unreliable in parallel runs)
|
|
578
577
|
let status = "passed";
|
|
579
|
-
if (
|
|
578
|
+
if (stats.failedTests > 0) {
|
|
580
579
|
status = "failed";
|
|
581
580
|
} else if (reason === "interrupted") {
|
|
582
581
|
status = "cancelled";
|
|
583
582
|
} else if (stats.totalTests === 0) {
|
|
584
583
|
status = "cancelled";
|
|
584
|
+
} else if (stats.passedTests === 0 && stats.skippedTests === 0) {
|
|
585
|
+
// No tests actually ran (all were filtered/excluded)
|
|
586
|
+
status = "cancelled";
|
|
585
587
|
}
|
|
586
588
|
|
|
587
589
|
// Complete test run via API
|
|
@@ -599,9 +601,12 @@ class TestDriverReporter {
|
|
|
599
601
|
|
|
600
602
|
// Update platform if detected from test results
|
|
601
603
|
const platform = getPlatform();
|
|
604
|
+
logger.debug(`Platform detection result: ${platform}, detectedPlatform in state: ${pluginState.detectedPlatform}`);
|
|
602
605
|
if (platform) {
|
|
603
606
|
completeData.platform = platform;
|
|
604
607
|
logger.debug(`Updating test run with platform: ${platform}`);
|
|
608
|
+
} else {
|
|
609
|
+
logger.warn(`No platform detected, test run will keep default platform`);
|
|
605
610
|
}
|
|
606
611
|
|
|
607
612
|
// Wait for any pending operations (shouldn't be any, but just in case)
|
|
@@ -619,7 +624,17 @@ class TestDriverReporter {
|
|
|
619
624
|
// Mark test run as completed to prevent duplicate completion
|
|
620
625
|
pluginState.testRunCompleted = true;
|
|
621
626
|
|
|
622
|
-
|
|
627
|
+
// Output the test run URL for CI to capture
|
|
628
|
+
const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
|
|
629
|
+
const consoleUrl = getConsoleUrl(pluginState.apiRoot);
|
|
630
|
+
if (testRunDbId) {
|
|
631
|
+
const testRunUrl = `${consoleUrl}/runs/${testRunDbId}`;
|
|
632
|
+
logger.debug(`🔗 View test run: ${testRunUrl}`);
|
|
633
|
+
// Output in a parseable format for CI
|
|
634
|
+
console.log(`TESTDRIVER_RUN_URL=${testRunUrl}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
logger.debug(`✅ Test run completed: ${stats.passedTests}/${stats.totalTests} passed`);
|
|
623
638
|
} catch (error) {
|
|
624
639
|
logger.error("Failed to complete test run:", error.message);
|
|
625
640
|
logger.debug("Error stack:", error.stack);
|
|
@@ -633,9 +648,6 @@ class TestDriverReporter {
|
|
|
633
648
|
test,
|
|
634
649
|
startTime: Date.now(),
|
|
635
650
|
});
|
|
636
|
-
|
|
637
|
-
// Try to detect platform from test context
|
|
638
|
-
detectPlatformFromTest(test);
|
|
639
651
|
}
|
|
640
652
|
|
|
641
653
|
async onTestCaseResult(test) {
|
|
@@ -649,7 +661,7 @@ class TestDriverReporter {
|
|
|
649
661
|
? "skipped"
|
|
650
662
|
: "failed";
|
|
651
663
|
|
|
652
|
-
logger.
|
|
664
|
+
logger.debug(`Test case completed: ${test.name} (${status})`);
|
|
653
665
|
|
|
654
666
|
// Calculate duration from tracked start time
|
|
655
667
|
const testCase = pluginState.testCases.get(test.id);
|
|
@@ -692,12 +704,9 @@ class TestDriverReporter {
|
|
|
692
704
|
// Don't override duration from file - use Vitest's result.duration
|
|
693
705
|
// duration is already set above from result.duration
|
|
694
706
|
|
|
695
|
-
logger.debug(`Read from file - dashcam: ${dashcamUrl}, platform: ${platform}, sessionId: ${sessionId}, testFile: ${testFile}, testOrder: ${testOrder}, duration: ${duration}ms`);
|
|
696
|
-
|
|
697
707
|
// Update test run platform from first test that reports it
|
|
698
708
|
if (platform && !pluginState.detectedPlatform) {
|
|
699
709
|
pluginState.detectedPlatform = platform;
|
|
700
|
-
logger.debug(`Detected platform from test: ${platform}`);
|
|
701
710
|
}
|
|
702
711
|
|
|
703
712
|
// Clean up the file after reading
|
|
@@ -811,8 +820,8 @@ class TestDriverReporter {
|
|
|
811
820
|
const testCaseDbId = testCaseResponse.data?.id;
|
|
812
821
|
const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
|
|
813
822
|
|
|
814
|
-
|
|
815
|
-
|
|
823
|
+
console.log('');
|
|
824
|
+
console.log(`🔗 Test Report: ${getConsoleUrl(pluginState.apiRoot)}/runs/${testRunDbId}/${testCaseDbId}`);
|
|
816
825
|
} catch (error) {
|
|
817
826
|
logger.error("Failed to report test case:", error.message);
|
|
818
827
|
}
|
|
@@ -823,6 +832,33 @@ class TestDriverReporter {
|
|
|
823
832
|
// Helper Functions
|
|
824
833
|
// ============================================================================
|
|
825
834
|
|
|
835
|
+
/**
|
|
836
|
+
* Maps an API root URL to its corresponding web console URL.
|
|
837
|
+
* The API and web console are served from different domains/ports.
|
|
838
|
+
*
|
|
839
|
+
* @param {string} apiRoot - The API root URL (e.g., https://testdriver-api.onrender.com)
|
|
840
|
+
* @returns {string} The corresponding web console URL
|
|
841
|
+
*/
|
|
842
|
+
function getConsoleUrl(apiRoot) {
|
|
843
|
+
|
|
844
|
+
if (!apiRoot) return 'https://console.testdriver.ai';
|
|
845
|
+
|
|
846
|
+
// Production: API on render.com -> Console on testdriver.ai
|
|
847
|
+
if (apiRoot.includes('testdriver-api.onrender.com')) {
|
|
848
|
+
return 'https://console.testdriver.ai';
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Local development: API on localhost:1337 -> Web on localhost:3001
|
|
852
|
+
if (apiRoot.includes('ngrok.io')) {
|
|
853
|
+
return `http://localhost:3001`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Ngrok or other tunnels: assume same host, different path structure
|
|
857
|
+
// For ngrok, the API and web might be on same domain or user needs to configure
|
|
858
|
+
// Return as-is since we can't reliably determine the mapping
|
|
859
|
+
return apiRoot;
|
|
860
|
+
}
|
|
861
|
+
|
|
826
862
|
function generateRunId() {
|
|
827
863
|
return `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
828
864
|
}
|
|
@@ -838,27 +874,18 @@ function getPlatform() {
|
|
|
838
874
|
return pluginState.detectedPlatform;
|
|
839
875
|
}
|
|
840
876
|
|
|
877
|
+
// Try to get platform from dashcam URLs (registered during test cleanup)
|
|
878
|
+
for (const [, data] of pluginState.dashcamUrls) {
|
|
879
|
+
if (data.platform) {
|
|
880
|
+
logger.debug(`Using platform from dashcam URL registration: ${data.platform}`);
|
|
881
|
+
return data.platform;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
841
885
|
logger.debug("Platform not yet detected from client");
|
|
842
886
|
return null;
|
|
843
887
|
}
|
|
844
888
|
|
|
845
|
-
function detectPlatformFromTest(test) {
|
|
846
|
-
// Check if testdriver client is accessible via test context
|
|
847
|
-
const client = test.context?.testdriver || test.meta?.testdriver;
|
|
848
|
-
|
|
849
|
-
if (client && client.os) {
|
|
850
|
-
// Normalize platform value
|
|
851
|
-
let platform = client.os.toLowerCase();
|
|
852
|
-
if (platform === "darwin" || platform === "mac") platform = "mac";
|
|
853
|
-
else if (platform === "win32" || platform === "windows")
|
|
854
|
-
platform = "windows";
|
|
855
|
-
else if (platform === "linux") platform = "linux";
|
|
856
|
-
|
|
857
|
-
pluginState.detectedPlatform = platform;
|
|
858
|
-
logger.debug(`Detected platform from test context: ${platform}`);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
889
|
function calculateStatsFromModules(testModules) {
|
|
863
890
|
let totalTests = 0;
|
|
864
891
|
let passedTests = 0;
|
|
@@ -973,7 +1000,7 @@ function getGitInfo() {
|
|
|
973
1000
|
}
|
|
974
1001
|
}
|
|
975
1002
|
|
|
976
|
-
logger.
|
|
1003
|
+
logger.debug("Collected git info:", info);
|
|
977
1004
|
return info;
|
|
978
1005
|
}
|
|
979
1006
|
|
package/lib/core/Dashcam.js
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* - Retrieving replay URLs
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
const { logger } = require('../../interfaces/logger');
|
|
13
|
+
|
|
12
14
|
class Dashcam {
|
|
13
15
|
/**
|
|
14
16
|
* Create a Dashcam instance
|
|
@@ -74,6 +76,23 @@ class Dashcam {
|
|
|
74
76
|
return this.client.config?.TD_API_ROOT || 'https://testdriver-api.onrender.com';
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Get console URL based on API root
|
|
81
|
+
* Maps API endpoints to their corresponding web console URLs
|
|
82
|
+
* @param {string} apiRoot - The API root URL
|
|
83
|
+
* @returns {string} The corresponding console URL
|
|
84
|
+
*/
|
|
85
|
+
static getConsoleUrl(apiRoot = 'https://testdriver-api.onrender.com') {
|
|
86
|
+
// Map API roots to console URLs
|
|
87
|
+
const apiToConsoleMap = {
|
|
88
|
+
'https://testdriver-api.onrender.com': 'https://console.testdriver.ai',
|
|
89
|
+
'https://v6.testdriver.ai': 'https://console.testdriver.ai',
|
|
90
|
+
'https://replayable-dev-ian-mac-m1-16.ngrok.io': 'http://localhost:3001',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return apiToConsoleMap[apiRoot] || 'https://console.testdriver.ai';
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
/**
|
|
78
97
|
* Get dashcam executable path
|
|
79
98
|
* @private
|
|
@@ -83,7 +102,7 @@ class Dashcam {
|
|
|
83
102
|
const npmPrefix = await this.client.exec(shell, 'npm prefix -g', 40000, true);
|
|
84
103
|
|
|
85
104
|
if (this.client.os === 'windows') {
|
|
86
|
-
return
|
|
105
|
+
return 'dashcam';
|
|
87
106
|
}
|
|
88
107
|
return npmPrefix.trim() + '/bin/dashcam';
|
|
89
108
|
}
|
|
@@ -98,33 +117,16 @@ class Dashcam {
|
|
|
98
117
|
const shell = this._getShell();
|
|
99
118
|
const apiRoot = this._getApiRoot();
|
|
100
119
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
let install = await this.client.exec(
|
|
121
|
+
shell,
|
|
122
|
+
'npm ls dashcam -g || echo "not installed"',
|
|
123
|
+
40000,
|
|
124
|
+
true
|
|
125
|
+
);
|
|
126
|
+
this._log('debug', 'Dashcam install check:', install);
|
|
105
127
|
|
|
106
|
-
|
|
107
|
-
await this.client.exec(shell, 'npm uninstall dashcam -g', 40000, true);
|
|
108
|
-
await this.client.exec(shell, 'npm cache clean --force', 40000, true);
|
|
109
|
-
|
|
110
|
-
// Install dashcam with TD_API_ROOT environment variable
|
|
111
|
-
const installOutput = await this.client.exec(
|
|
112
|
-
shell,
|
|
113
|
-
`$env:TD_API_ROOT="${apiRoot}"; npm install dashcam@beta -g`,
|
|
114
|
-
120000,
|
|
115
|
-
true
|
|
116
|
-
);
|
|
117
|
-
this._log('debug', 'Install dashcam output:', installOutput);
|
|
128
|
+
if (this.client.os === 'windows') {
|
|
118
129
|
|
|
119
|
-
// Verify version
|
|
120
|
-
const latestVersion = await this.client.exec(
|
|
121
|
-
shell,
|
|
122
|
-
'npm view dashcam@beta version',
|
|
123
|
-
40000,
|
|
124
|
-
true
|
|
125
|
-
);
|
|
126
|
-
this._log('debug', 'Latest beta version available:', latestVersion);
|
|
127
|
-
|
|
128
130
|
const dashcamPath = await this._getDashcamPath();
|
|
129
131
|
this._log('debug', 'Dashcam executable path:', dashcamPath);
|
|
130
132
|
|
|
@@ -145,16 +147,6 @@ class Dashcam {
|
|
|
145
147
|
);
|
|
146
148
|
this._log('debug', 'Dashcam version test:', versionTest);
|
|
147
149
|
|
|
148
|
-
// Verify installation
|
|
149
|
-
if (!installedVersion) {
|
|
150
|
-
this._log('error', 'Dashcam version command returned null/empty');
|
|
151
|
-
this._log('debug', 'Install output was:', installOutput);
|
|
152
|
-
} else if (!installedVersion.includes('1.3.')) {
|
|
153
|
-
this._log('warn', 'Dashcam version may be outdated. Expected 1.3.x, got:', installedVersion);
|
|
154
|
-
} else {
|
|
155
|
-
this._log('debug', 'Dashcam version verified:', installedVersion);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
150
|
// Authenticate with TD_API_ROOT
|
|
159
151
|
const authOutput = await this.client.exec(
|
|
160
152
|
shell,
|
|
@@ -292,7 +284,6 @@ class Dashcam {
|
|
|
292
284
|
|
|
293
285
|
// Auto-authenticate if not already done
|
|
294
286
|
if (!this._authenticated) {
|
|
295
|
-
this._log('info', 'Auto-authenticating dashcam...');
|
|
296
287
|
await this.auth();
|
|
297
288
|
}
|
|
298
289
|
|
|
@@ -300,7 +291,6 @@ class Dashcam {
|
|
|
300
291
|
const apiRoot = this._getApiRoot();
|
|
301
292
|
|
|
302
293
|
if (this.client.os === 'windows') {
|
|
303
|
-
this._log('info', 'Starting dashcam recording on Windows...');
|
|
304
294
|
|
|
305
295
|
const dashcamPath = await this._getDashcamPath();
|
|
306
296
|
this._log('debug', 'Dashcam path:', dashcamPath);
|
|
@@ -316,11 +306,12 @@ class Dashcam {
|
|
|
316
306
|
|
|
317
307
|
// Start dashcam record and redirect output with TD_API_ROOT
|
|
318
308
|
const outputFile = 'C:\\Users\\testdriver\\.dashcam-cli\\dashcam-start.log';
|
|
319
|
-
const titleArg = this.title ? ` --title
|
|
309
|
+
// const titleArg = this.title ? ` --title=\`"${this.title.replace(/"/g, '`"')}\`"` : '';
|
|
310
|
+
let titleArg = '';
|
|
320
311
|
const startScript = `
|
|
321
312
|
try {
|
|
322
313
|
$env:TD_API_ROOT="${apiRoot}"
|
|
323
|
-
$process = Start-Process "cmd.exe" -ArgumentList "/c", "${dashcamPath} record${titleArg}
|
|
314
|
+
$process = Start-Process "cmd.exe" -ArgumentList "/c", "\`"${dashcamPath}\`" record${titleArg}"
|
|
324
315
|
Write-Output "Process started with PID: $($process.Id)"
|
|
325
316
|
Start-Sleep -Seconds 2
|
|
326
317
|
if ($process.HasExited) {
|
|
@@ -332,7 +323,11 @@ class Dashcam {
|
|
|
332
323
|
Write-Output "ERROR: $_"
|
|
333
324
|
}
|
|
334
325
|
`;
|
|
326
|
+
|
|
327
|
+
// add 2>&1" -PassThru
|
|
335
328
|
|
|
329
|
+
// Capture startTime right before issuing the dashcam command to sync with actual recording start
|
|
330
|
+
this.startTime = Date.now();
|
|
336
331
|
const startOutput = await this.client.exec(shell, startScript, 10000, true);
|
|
337
332
|
this._log('debug', 'Start-Process output:', startOutput);
|
|
338
333
|
|
|
@@ -349,23 +344,25 @@ class Dashcam {
|
|
|
349
344
|
// Give process time to initialize
|
|
350
345
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
351
346
|
|
|
352
|
-
this._log('
|
|
347
|
+
this._log('debug', 'Dashcam recording started');
|
|
353
348
|
} else {
|
|
354
349
|
// Linux/Mac with TD_API_ROOT
|
|
355
|
-
this._log('
|
|
350
|
+
this._log('debug', 'Starting dashcam recording on Linux/Mac...');
|
|
356
351
|
const titleArg = this.title ? ` --title="${this.title.replace(/"/g, '\"')}"` : '';
|
|
352
|
+
// Capture startTime right before issuing the dashcam command to sync with actual recording start
|
|
353
|
+
this.startTime = Date.now();
|
|
357
354
|
await this.client.exec(shell, `TD_API_ROOT="${apiRoot}" dashcam record${titleArg} >/dev/null 2>&1 &`);
|
|
358
|
-
this._log('
|
|
355
|
+
this._log('debug', 'Dashcam recording started');
|
|
359
356
|
}
|
|
360
357
|
|
|
361
358
|
this.recording = true;
|
|
362
|
-
this.startTime = Date.now(); // Record the timestamp when dashcam started
|
|
363
359
|
|
|
364
360
|
// Update the session with dashcam start time for interaction timestamp synchronization
|
|
365
|
-
|
|
361
|
+
const sessionId = this.client?.agent?.session?.get?.();
|
|
362
|
+
if (sessionId) {
|
|
366
363
|
try {
|
|
367
|
-
const apiRoot = this.apiRoot || process.env.TD_API_ROOT ||
|
|
368
|
-
const response = await fetch(`${apiRoot}/api/v7.0.0/testdriver/session/${
|
|
364
|
+
const apiRoot = this.apiRoot || process.env.TD_API_ROOT || this._getApiRoot();
|
|
365
|
+
const response = await fetch(`${apiRoot}/api/v7.0.0/testdriver/session/${sessionId}/update-dashcam-time`, {
|
|
369
366
|
method: 'POST',
|
|
370
367
|
headers: {
|
|
371
368
|
'Content-Type': 'application/json',
|
|
@@ -375,7 +372,7 @@ class Dashcam {
|
|
|
375
372
|
});
|
|
376
373
|
|
|
377
374
|
if (response.ok) {
|
|
378
|
-
this._log('
|
|
375
|
+
this._log('debug', `Updated session ${sessionId} with dashcam start time: ${this.startTime}`);
|
|
379
376
|
} else {
|
|
380
377
|
this._log('warn', 'Failed to update session with dashcam start time:', response.statusText);
|
|
381
378
|
}
|
|
@@ -406,7 +403,7 @@ class Dashcam {
|
|
|
406
403
|
return null;
|
|
407
404
|
}
|
|
408
405
|
|
|
409
|
-
this._log('
|
|
406
|
+
this._log('debug', 'Stopping dashcam and retrieving URL...');
|
|
410
407
|
const shell = this._getShell();
|
|
411
408
|
const apiRoot = this._getApiRoot();
|
|
412
409
|
let output;
|
|
@@ -417,12 +414,12 @@ class Dashcam {
|
|
|
417
414
|
const dashcamPath = await this._getDashcamPath();
|
|
418
415
|
|
|
419
416
|
// Stop and get output with TD_API_ROOT
|
|
420
|
-
output = await this.client.exec(shell, `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" stop`,
|
|
417
|
+
output = await this.client.exec(shell, `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" stop`, 300000, true);
|
|
421
418
|
this._log('debug', 'Dashcam stop command output:', output);
|
|
422
419
|
} else {
|
|
423
420
|
// Linux/Mac with TD_API_ROOT
|
|
424
421
|
const dashcamPath = await this._getDashcamPath();
|
|
425
|
-
output = await this.client.exec(shell, `TD_API_ROOT="${apiRoot}" "${dashcamPath}" stop`,
|
|
422
|
+
output = await this.client.exec(shell, `TD_API_ROOT="${apiRoot}" "${dashcamPath}" stop`, 300000, true);
|
|
426
423
|
this._log('debug', 'Dashcam command output:', output);
|
|
427
424
|
}
|
|
428
425
|
|
|
@@ -437,7 +434,6 @@ class Dashcam {
|
|
|
437
434
|
let url = replayUrlMatch[0];
|
|
438
435
|
// Remove trailing punctuation but keep query params
|
|
439
436
|
url = url.replace(/[.,;:!\)\]]+$/, '').trim();
|
|
440
|
-
this._log('info', 'Found dashcam URL:', url);
|
|
441
437
|
return url;
|
|
442
438
|
}
|
|
443
439
|
|
|
@@ -446,7 +442,6 @@ class Dashcam {
|
|
|
446
442
|
if (dashcamUrlMatch) {
|
|
447
443
|
let url = dashcamUrlMatch[0];
|
|
448
444
|
url = url.replace(/[.,;:!\?\)\]]+$/, '').trim();
|
|
449
|
-
this._log('info', 'Found dashcam URL:', url);
|
|
450
445
|
return url;
|
|
451
446
|
}
|
|
452
447
|
|
|
@@ -459,7 +454,7 @@ class Dashcam {
|
|
|
459
454
|
}
|
|
460
455
|
|
|
461
456
|
/**
|
|
462
|
-
* Internal logging -
|
|
457
|
+
* Internal logging - uses TestDriver logger
|
|
463
458
|
* @private
|
|
464
459
|
*/
|
|
465
460
|
_log(level, ...args) {
|
|
@@ -467,19 +462,23 @@ class Dashcam {
|
|
|
467
462
|
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
|
468
463
|
).join(' ');
|
|
469
464
|
|
|
470
|
-
const
|
|
471
|
-
const logLine = `[${timestamp}] [DASHCAM:${level.toUpperCase()}] ${message}`;
|
|
465
|
+
const logMessage = `[DASHCAM] ${message}`;
|
|
472
466
|
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
467
|
+
// Use the TestDriver logger based on level
|
|
468
|
+
switch (level) {
|
|
469
|
+
case 'error':
|
|
470
|
+
logger.error(logMessage);
|
|
471
|
+
break;
|
|
472
|
+
case 'warn':
|
|
473
|
+
logger.warn(logMessage);
|
|
474
|
+
break;
|
|
475
|
+
case 'debug':
|
|
476
|
+
logger.debug(logMessage);
|
|
477
|
+
break;
|
|
478
|
+
case 'info':
|
|
479
|
+
default:
|
|
480
|
+
logger.info(logMessage);
|
|
481
|
+
break;
|
|
483
482
|
}
|
|
484
483
|
}
|
|
485
484
|
|
package/lib/vitest/hooks.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import fs from 'fs';
|
|
|
19
19
|
import os from 'os';
|
|
20
20
|
import path from 'path';
|
|
21
21
|
import { vi } from 'vitest';
|
|
22
|
+
import chalk from 'chalk';
|
|
22
23
|
import TestDriverSDK from '../../sdk.js';
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -103,7 +104,6 @@ function setupConsoleSpy(client, taskId) {
|
|
|
103
104
|
// Store spies on client for cleanup
|
|
104
105
|
client._consoleSpies = { logSpy, errorSpy, warnSpy, infoSpy };
|
|
105
106
|
|
|
106
|
-
console.log(`[testdriver] Console spy set up for task: ${taskId}`);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
@@ -162,6 +162,13 @@ export function TestDriver(context, options = {}) {
|
|
|
162
162
|
// Merge options: plugin global options < test-specific options
|
|
163
163
|
const mergedOptions = { ...pluginOptions, ...options };
|
|
164
164
|
|
|
165
|
+
// Support TD_OS environment variable for specifying target OS (linux, mac, windows)
|
|
166
|
+
// Priority: test options > plugin options > environment variable > default (linux)
|
|
167
|
+
if (!mergedOptions.os && process.env.TD_OS) {
|
|
168
|
+
mergedOptions.os = process.env.TD_OS;
|
|
169
|
+
console.log(`[testdriver] Set mergedOptions.os = ${mergedOptions.os} from TD_OS environment variable`);
|
|
170
|
+
}
|
|
171
|
+
|
|
165
172
|
// Extract TestDriver-specific options
|
|
166
173
|
const apiKey = mergedOptions.apiKey || process.env.TD_API_KEY;
|
|
167
174
|
|
|
@@ -191,9 +198,7 @@ export function TestDriver(context, options = {}) {
|
|
|
191
198
|
|
|
192
199
|
await testdriver.auth();
|
|
193
200
|
await testdriver.connect();
|
|
194
|
-
|
|
195
|
-
console.log('[testdriver] ✅ Connected to sandbox');
|
|
196
|
-
|
|
201
|
+
|
|
197
202
|
if (debugConsoleSpy) {
|
|
198
203
|
console.log('[DEBUG] After connect - sandbox.instanceSocketConnected:', testdriver.sandbox?.instanceSocketConnected);
|
|
199
204
|
console.log('[DEBUG] After connect - sandbox.send:', typeof testdriver.sandbox?.send);
|
|
@@ -213,7 +218,6 @@ export function TestDriver(context, options = {}) {
|
|
|
213
218
|
: `touch ${logPath}`;
|
|
214
219
|
|
|
215
220
|
await testdriver.exec(shell, createLogCmd, 10000, true);
|
|
216
|
-
console.log('[testdriver] ✅ Created log file:', logPath);
|
|
217
221
|
|
|
218
222
|
// Add automatic log tracking when dashcam starts
|
|
219
223
|
// Store original start method
|
|
@@ -226,56 +230,45 @@ export function TestDriver(context, options = {}) {
|
|
|
226
230
|
// Register cleanup handler with dashcam.stop()
|
|
227
231
|
if (!lifecycleHandlers.has(context.task)) {
|
|
228
232
|
const cleanup = async () => {
|
|
229
|
-
console.log('[testdriver] Cleaning up TestDriver client...');
|
|
230
233
|
try {
|
|
231
234
|
// Stop dashcam if it was started - with timeout to prevent hanging
|
|
232
235
|
if (testdriver._dashcam && testdriver._dashcam.recording) {
|
|
233
236
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
const dashcamUrl = await testdriver.dashcam.stop();
|
|
238
|
+
console.log('');
|
|
239
|
+
console.log('🎥' + chalk.yellow(` Dashcam URL`) + `: ${dashcamUrl}`);
|
|
240
|
+
console.log('');
|
|
241
|
+
// Write test result to file for the reporter (cross-process communication)
|
|
242
|
+
// This should happen regardless of whether dashcam succeeded, to ensure platform info is available
|
|
243
|
+
const testId = context.task.id;
|
|
244
|
+
const platform = testdriver.os || 'linux';
|
|
245
|
+
const absolutePath = context.task.file?.filepath || context.task.file?.name || 'unknown';
|
|
246
|
+
const projectRoot = process.cwd();
|
|
247
|
+
const testFile = absolutePath !== 'unknown'
|
|
248
|
+
? path.relative(projectRoot, absolutePath)
|
|
249
|
+
: absolutePath;
|
|
250
|
+
|
|
251
|
+
// Create results directory if it doesn't exist
|
|
252
|
+
const resultsDir = path.join(os.tmpdir(), 'testdriver-results');
|
|
253
|
+
if (!fs.existsSync(resultsDir)) {
|
|
254
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Write test result file
|
|
258
|
+
const testResultFile = path.join(resultsDir, `${testId}.json`);
|
|
259
|
+
const testResult = {
|
|
260
|
+
dashcamUrl: dashcamUrl || null,
|
|
261
|
+
platform,
|
|
262
|
+
testFile,
|
|
263
|
+
testOrder: 0,
|
|
264
|
+
sessionId: testdriver.getSessionId(),
|
|
265
|
+
};
|
|
241
266
|
|
|
242
|
-
|
|
243
|
-
console.log('🎥 Dashcam URL:', dashcamUrl);
|
|
267
|
+
fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
|
|
244
268
|
|
|
245
|
-
//
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
const platform = testdriver.os || 'linux';
|
|
249
|
-
const absolutePath = context.task.file?.filepath || context.task.file?.name || 'unknown';
|
|
250
|
-
const projectRoot = process.cwd();
|
|
251
|
-
const testFile = absolutePath !== 'unknown'
|
|
252
|
-
? path.relative(projectRoot, absolutePath)
|
|
253
|
-
: absolutePath;
|
|
254
|
-
|
|
255
|
-
// Create results directory if it doesn't exist
|
|
256
|
-
const resultsDir = path.join(os.tmpdir(), 'testdriver-results');
|
|
257
|
-
if (!fs.existsSync(resultsDir)) {
|
|
258
|
-
fs.mkdirSync(resultsDir, { recursive: true });
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Write test result file
|
|
262
|
-
const testResultFile = path.join(resultsDir, `${testId}.json`);
|
|
263
|
-
const testResult = {
|
|
264
|
-
dashcamUrl,
|
|
265
|
-
platform,
|
|
266
|
-
testFile,
|
|
267
|
-
testOrder: 0,
|
|
268
|
-
sessionId: testdriver.getSessionId(),
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
fs.writeFileSync(testResultFile, JSON.stringify(testResult, null, 2));
|
|
272
|
-
console.log(`[testdriver] ✅ Wrote dashcam URL to ${testResultFile}`);
|
|
273
|
-
|
|
274
|
-
// Also register in memory if plugin is available
|
|
275
|
-
if (globalThis.__testdriverPlugin?.registerDashcamUrl) {
|
|
276
|
-
globalThis.__testdriverPlugin.registerDashcamUrl(testId, dashcamUrl, platform);
|
|
277
|
-
console.log(`[testdriver] ✅ Registered dashcam URL in memory for test ${testId}`);
|
|
278
|
-
}
|
|
269
|
+
// Also register in memory if plugin is available
|
|
270
|
+
if (globalThis.__testdriverPlugin?.registerDashcamUrl) {
|
|
271
|
+
globalThis.__testdriverPlugin.registerDashcamUrl(testId, dashcamUrl, platform);
|
|
279
272
|
}
|
|
280
273
|
} catch (error) {
|
|
281
274
|
// Log more detailed error information for debugging
|
|
@@ -305,7 +298,6 @@ export function TestDriver(context, options = {}) {
|
|
|
305
298
|
testdriver.disconnect(),
|
|
306
299
|
new Promise((resolve) => setTimeout(resolve, 5000)) // 5s timeout for disconnect
|
|
307
300
|
]);
|
|
308
|
-
console.log('✅ Client disconnected');
|
|
309
301
|
} catch (error) {
|
|
310
302
|
console.error('Error disconnecting client:', error);
|
|
311
303
|
}
|