testdriverai 7.1.4 → 7.2.2
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/interface.js +4 -0
- 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 +70 -50
- package/lib/core/Dashcam.js +60 -85
- package/lib/vitest/hooks.mjs +42 -50
- package/package.json +1 -1
- package/sdk-log-formatter.js +350 -175
- package/sdk.d.ts +36 -3
- 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
|
}
|
|
@@ -402,14 +403,7 @@ function registerExitHandlers() {
|
|
|
402
403
|
await handleProcessExit();
|
|
403
404
|
process.exit(143); // Standard exit code for SIGTERM
|
|
404
405
|
});
|
|
405
|
-
|
|
406
|
-
// Handle unexpected exits
|
|
407
|
-
process.on("beforeExit", async () => {
|
|
408
|
-
// Only handle if test run is still running (hasn't been completed normally)
|
|
409
|
-
if (pluginState.testRun && !pluginState.testRunCompleted) {
|
|
410
|
-
await handleProcessExit();
|
|
411
|
-
}
|
|
412
|
-
});
|
|
406
|
+
|
|
413
407
|
}
|
|
414
408
|
|
|
415
409
|
/**
|
|
@@ -494,8 +488,6 @@ class TestDriverReporter {
|
|
|
494
488
|
return;
|
|
495
489
|
}
|
|
496
490
|
|
|
497
|
-
logger.info("Starting test run initialization with API key...");
|
|
498
|
-
|
|
499
491
|
try {
|
|
500
492
|
// Exchange API key for JWT token
|
|
501
493
|
logger.debug("Authenticating with API...");
|
|
@@ -544,7 +536,7 @@ class TestDriverReporter {
|
|
|
544
536
|
startTime: pluginState.startTime,
|
|
545
537
|
});
|
|
546
538
|
|
|
547
|
-
logger.
|
|
539
|
+
logger.debug(`Test run created: ${pluginState.testRunId}`);
|
|
548
540
|
} catch (error) {
|
|
549
541
|
logger.error("Failed to initialize:", error.message);
|
|
550
542
|
pluginState.apiKey = null;
|
|
@@ -566,7 +558,7 @@ class TestDriverReporter {
|
|
|
566
558
|
return;
|
|
567
559
|
}
|
|
568
560
|
|
|
569
|
-
logger.
|
|
561
|
+
logger.debug("Completing test run...");
|
|
570
562
|
|
|
571
563
|
try {
|
|
572
564
|
// Calculate statistics from testModules
|
|
@@ -574,14 +566,17 @@ class TestDriverReporter {
|
|
|
574
566
|
|
|
575
567
|
logger.debug("Stats:", stats);
|
|
576
568
|
|
|
577
|
-
// Determine overall status based on reason
|
|
569
|
+
// Determine overall status based on stats (not reason, which is unreliable in parallel runs)
|
|
578
570
|
let status = "passed";
|
|
579
|
-
if (
|
|
571
|
+
if (stats.failedTests > 0) {
|
|
580
572
|
status = "failed";
|
|
581
573
|
} else if (reason === "interrupted") {
|
|
582
574
|
status = "cancelled";
|
|
583
575
|
} else if (stats.totalTests === 0) {
|
|
584
576
|
status = "cancelled";
|
|
577
|
+
} else if (stats.passedTests === 0 && stats.skippedTests === 0) {
|
|
578
|
+
// No tests actually ran (all were filtered/excluded)
|
|
579
|
+
status = "cancelled";
|
|
585
580
|
}
|
|
586
581
|
|
|
587
582
|
// Complete test run via API
|
|
@@ -599,9 +594,12 @@ class TestDriverReporter {
|
|
|
599
594
|
|
|
600
595
|
// Update platform if detected from test results
|
|
601
596
|
const platform = getPlatform();
|
|
597
|
+
logger.debug(`Platform detection result: ${platform}, detectedPlatform in state: ${pluginState.detectedPlatform}`);
|
|
602
598
|
if (platform) {
|
|
603
599
|
completeData.platform = platform;
|
|
604
600
|
logger.debug(`Updating test run with platform: ${platform}`);
|
|
601
|
+
} else {
|
|
602
|
+
logger.warn(`No platform detected, test run will keep default platform`);
|
|
605
603
|
}
|
|
606
604
|
|
|
607
605
|
// Wait for any pending operations (shouldn't be any, but just in case)
|
|
@@ -619,7 +617,17 @@ class TestDriverReporter {
|
|
|
619
617
|
// Mark test run as completed to prevent duplicate completion
|
|
620
618
|
pluginState.testRunCompleted = true;
|
|
621
619
|
|
|
622
|
-
|
|
620
|
+
// Output the test run URL for CI to capture
|
|
621
|
+
const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
|
|
622
|
+
const consoleUrl = getConsoleUrl(pluginState.apiRoot);
|
|
623
|
+
if (testRunDbId) {
|
|
624
|
+
const testRunUrl = `${consoleUrl}/runs/${testRunDbId}`;
|
|
625
|
+
logger.debug(`🔗 View test run: ${testRunUrl}`);
|
|
626
|
+
// Output in a parseable format for CI
|
|
627
|
+
console.log(`TESTDRIVER_RUN_URL=${testRunUrl}`);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
logger.debug(`✅ Test run completed: ${stats.passedTests}/${stats.totalTests} passed`);
|
|
623
631
|
} catch (error) {
|
|
624
632
|
logger.error("Failed to complete test run:", error.message);
|
|
625
633
|
logger.debug("Error stack:", error.stack);
|
|
@@ -633,9 +641,6 @@ class TestDriverReporter {
|
|
|
633
641
|
test,
|
|
634
642
|
startTime: Date.now(),
|
|
635
643
|
});
|
|
636
|
-
|
|
637
|
-
// Try to detect platform from test context
|
|
638
|
-
detectPlatformFromTest(test);
|
|
639
644
|
}
|
|
640
645
|
|
|
641
646
|
async onTestCaseResult(test) {
|
|
@@ -649,7 +654,7 @@ class TestDriverReporter {
|
|
|
649
654
|
? "skipped"
|
|
650
655
|
: "failed";
|
|
651
656
|
|
|
652
|
-
logger.
|
|
657
|
+
logger.debug(`Test case completed: ${test.name} (${status})`);
|
|
653
658
|
|
|
654
659
|
// Calculate duration from tracked start time
|
|
655
660
|
const testCase = pluginState.testCases.get(test.id);
|
|
@@ -692,12 +697,9 @@ class TestDriverReporter {
|
|
|
692
697
|
// Don't override duration from file - use Vitest's result.duration
|
|
693
698
|
// duration is already set above from result.duration
|
|
694
699
|
|
|
695
|
-
logger.debug(`Read from file - dashcam: ${dashcamUrl}, platform: ${platform}, sessionId: ${sessionId}, testFile: ${testFile}, testOrder: ${testOrder}, duration: ${duration}ms`);
|
|
696
|
-
|
|
697
700
|
// Update test run platform from first test that reports it
|
|
698
701
|
if (platform && !pluginState.detectedPlatform) {
|
|
699
702
|
pluginState.detectedPlatform = platform;
|
|
700
|
-
logger.debug(`Detected platform from test: ${platform}`);
|
|
701
703
|
}
|
|
702
704
|
|
|
703
705
|
// Clean up the file after reading
|
|
@@ -811,8 +813,8 @@ class TestDriverReporter {
|
|
|
811
813
|
const testCaseDbId = testCaseResponse.data?.id;
|
|
812
814
|
const testRunDbId = process.env.TD_TEST_RUN_DB_ID;
|
|
813
815
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
+
console.log('');
|
|
817
|
+
console.log(`🔗 Test Report: ${getConsoleUrl(pluginState.apiRoot)}/runs/${testRunDbId}/${testCaseDbId}`);
|
|
816
818
|
} catch (error) {
|
|
817
819
|
logger.error("Failed to report test case:", error.message);
|
|
818
820
|
}
|
|
@@ -823,6 +825,33 @@ class TestDriverReporter {
|
|
|
823
825
|
// Helper Functions
|
|
824
826
|
// ============================================================================
|
|
825
827
|
|
|
828
|
+
/**
|
|
829
|
+
* Maps an API root URL to its corresponding web console URL.
|
|
830
|
+
* The API and web console are served from different domains/ports.
|
|
831
|
+
*
|
|
832
|
+
* @param {string} apiRoot - The API root URL (e.g., https://testdriver-api.onrender.com)
|
|
833
|
+
* @returns {string} The corresponding web console URL
|
|
834
|
+
*/
|
|
835
|
+
function getConsoleUrl(apiRoot) {
|
|
836
|
+
|
|
837
|
+
if (!apiRoot) return 'https://console.testdriver.ai';
|
|
838
|
+
|
|
839
|
+
// Production: API on render.com -> Console on testdriver.ai
|
|
840
|
+
if (apiRoot.includes('testdriver-api.onrender.com')) {
|
|
841
|
+
return 'https://console.testdriver.ai';
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Local development: API on localhost:1337 -> Web on localhost:3001
|
|
845
|
+
if (apiRoot.includes('ngrok.io')) {
|
|
846
|
+
return `http://localhost:3001`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Ngrok or other tunnels: assume same host, different path structure
|
|
850
|
+
// For ngrok, the API and web might be on same domain or user needs to configure
|
|
851
|
+
// Return as-is since we can't reliably determine the mapping
|
|
852
|
+
return apiRoot;
|
|
853
|
+
}
|
|
854
|
+
|
|
826
855
|
function generateRunId() {
|
|
827
856
|
return `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
828
857
|
}
|
|
@@ -838,27 +867,18 @@ function getPlatform() {
|
|
|
838
867
|
return pluginState.detectedPlatform;
|
|
839
868
|
}
|
|
840
869
|
|
|
870
|
+
// Try to get platform from dashcam URLs (registered during test cleanup)
|
|
871
|
+
for (const [, data] of pluginState.dashcamUrls) {
|
|
872
|
+
if (data.platform) {
|
|
873
|
+
logger.debug(`Using platform from dashcam URL registration: ${data.platform}`);
|
|
874
|
+
return data.platform;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
841
878
|
logger.debug("Platform not yet detected from client");
|
|
842
879
|
return null;
|
|
843
880
|
}
|
|
844
881
|
|
|
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
882
|
function calculateStatsFromModules(testModules) {
|
|
863
883
|
let totalTests = 0;
|
|
864
884
|
let passedTests = 0;
|
|
@@ -973,7 +993,7 @@ function getGitInfo() {
|
|
|
973
993
|
}
|
|
974
994
|
}
|
|
975
995
|
|
|
976
|
-
logger.
|
|
996
|
+
logger.debug("Collected git info:", info);
|
|
977
997
|
return info;
|
|
978
998
|
}
|
|
979
999
|
|
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,40 +344,18 @@ 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
|
-
|
|
364
|
-
// Update the session with dashcam start time for interaction timestamp synchronization
|
|
365
|
-
if (this.client && this.client.agent && this.client.agent.session) {
|
|
366
|
-
try {
|
|
367
|
-
const apiRoot = this.apiRoot || process.env.TD_API_ROOT || 'https://console.testdriver.ai';
|
|
368
|
-
const response = await fetch(`${apiRoot}/api/v7.0.0/testdriver/session/${this.client.agent.session}/update-dashcam-time`, {
|
|
369
|
-
method: 'POST',
|
|
370
|
-
headers: {
|
|
371
|
-
'Content-Type': 'application/json',
|
|
372
|
-
'Authorization': `Bearer ${this.apiKey}`
|
|
373
|
-
},
|
|
374
|
-
body: JSON.stringify({ dashcamStartTime: this.startTime })
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
if (response.ok) {
|
|
378
|
-
this._log('info', `Updated session ${this.client.agent.session} with dashcam start time: ${this.startTime}`);
|
|
379
|
-
} else {
|
|
380
|
-
this._log('warn', 'Failed to update session with dashcam start time:', response.statusText);
|
|
381
|
-
}
|
|
382
|
-
} catch (err) {
|
|
383
|
-
this._log('warn', 'Error updating session with dashcam start time:', err.message);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
359
|
}
|
|
387
360
|
|
|
388
361
|
/**
|
|
@@ -406,7 +379,7 @@ class Dashcam {
|
|
|
406
379
|
return null;
|
|
407
380
|
}
|
|
408
381
|
|
|
409
|
-
this._log('
|
|
382
|
+
this._log('debug', 'Stopping dashcam and retrieving URL...');
|
|
410
383
|
const shell = this._getShell();
|
|
411
384
|
const apiRoot = this._getApiRoot();
|
|
412
385
|
let output;
|
|
@@ -417,12 +390,12 @@ class Dashcam {
|
|
|
417
390
|
const dashcamPath = await this._getDashcamPath();
|
|
418
391
|
|
|
419
392
|
// Stop and get output with TD_API_ROOT
|
|
420
|
-
output = await this.client.exec(shell, `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" stop`,
|
|
393
|
+
output = await this.client.exec(shell, `$env:TD_API_ROOT="${apiRoot}"; & "${dashcamPath}" stop`, 300000, true);
|
|
421
394
|
this._log('debug', 'Dashcam stop command output:', output);
|
|
422
395
|
} else {
|
|
423
396
|
// Linux/Mac with TD_API_ROOT
|
|
424
397
|
const dashcamPath = await this._getDashcamPath();
|
|
425
|
-
output = await this.client.exec(shell, `TD_API_ROOT="${apiRoot}" "${dashcamPath}" stop`,
|
|
398
|
+
output = await this.client.exec(shell, `TD_API_ROOT="${apiRoot}" "${dashcamPath}" stop`, 300000, true);
|
|
426
399
|
this._log('debug', 'Dashcam command output:', output);
|
|
427
400
|
}
|
|
428
401
|
|
|
@@ -437,7 +410,6 @@ class Dashcam {
|
|
|
437
410
|
let url = replayUrlMatch[0];
|
|
438
411
|
// Remove trailing punctuation but keep query params
|
|
439
412
|
url = url.replace(/[.,;:!\)\]]+$/, '').trim();
|
|
440
|
-
this._log('info', 'Found dashcam URL:', url);
|
|
441
413
|
return url;
|
|
442
414
|
}
|
|
443
415
|
|
|
@@ -446,7 +418,6 @@ class Dashcam {
|
|
|
446
418
|
if (dashcamUrlMatch) {
|
|
447
419
|
let url = dashcamUrlMatch[0];
|
|
448
420
|
url = url.replace(/[.,;:!\?\)\]]+$/, '').trim();
|
|
449
|
-
this._log('info', 'Found dashcam URL:', url);
|
|
450
421
|
return url;
|
|
451
422
|
}
|
|
452
423
|
|
|
@@ -459,7 +430,7 @@ class Dashcam {
|
|
|
459
430
|
}
|
|
460
431
|
|
|
461
432
|
/**
|
|
462
|
-
* Internal logging -
|
|
433
|
+
* Internal logging - uses TestDriver logger
|
|
463
434
|
* @private
|
|
464
435
|
*/
|
|
465
436
|
_log(level, ...args) {
|
|
@@ -467,19 +438,23 @@ class Dashcam {
|
|
|
467
438
|
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
|
468
439
|
).join(' ');
|
|
469
440
|
|
|
470
|
-
const
|
|
471
|
-
const logLine = `[${timestamp}] [DASHCAM:${level.toUpperCase()}] ${message}`;
|
|
441
|
+
const logMessage = `[DASHCAM] ${message}`;
|
|
472
442
|
|
|
473
|
-
//
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
443
|
+
// Use the TestDriver logger based on level
|
|
444
|
+
switch (level) {
|
|
445
|
+
case 'error':
|
|
446
|
+
logger.error(logMessage);
|
|
447
|
+
break;
|
|
448
|
+
case 'warn':
|
|
449
|
+
logger.warn(logMessage);
|
|
450
|
+
break;
|
|
451
|
+
case 'debug':
|
|
452
|
+
logger.debug(logMessage);
|
|
453
|
+
break;
|
|
454
|
+
case 'info':
|
|
455
|
+
default:
|
|
456
|
+
logger.info(logMessage);
|
|
457
|
+
break;
|
|
483
458
|
}
|
|
484
459
|
}
|
|
485
460
|
|