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.
Files changed (72) hide show
  1. package/.github/workflows/acceptance.yaml +81 -0
  2. package/.github/workflows/publish.yaml +44 -0
  3. package/agent/index.js +18 -19
  4. package/agent/interface.js +4 -0
  5. package/agent/lib/commands.js +321 -121
  6. package/agent/lib/redraw.js +99 -39
  7. package/agent/lib/sandbox.js +98 -6
  8. package/agent/lib/sdk.js +25 -0
  9. package/agent/lib/system.js +2 -1
  10. package/agent/lib/validation.js +6 -6
  11. package/docs/docs.json +211 -101
  12. package/docs/snippets/tests/type-repeated-replay.mdx +1 -1
  13. package/docs/v7/_drafts/caching-selectors.mdx +24 -0
  14. package/docs/v7/api/act.mdx +1 -1
  15. package/docs/v7/api/assert.mdx +1 -1
  16. package/docs/v7/api/assertions.mdx +7 -7
  17. package/docs/v7/api/elements.mdx +78 -0
  18. package/docs/v7/api/find.mdx +38 -0
  19. package/docs/v7/api/focusApplication.mdx +2 -2
  20. package/docs/v7/api/hover.mdx +2 -2
  21. package/docs/v7/features/ai-native.mdx +57 -71
  22. package/docs/v7/features/application-logs.mdx +353 -0
  23. package/docs/v7/features/browser-logs.mdx +414 -0
  24. package/docs/v7/features/cache-management.mdx +402 -0
  25. package/docs/v7/features/continuous-testing.mdx +346 -0
  26. package/docs/v7/features/coverage.mdx +508 -0
  27. package/docs/v7/features/data-driven-testing.mdx +441 -0
  28. package/docs/v7/features/easy-to-write.mdx +2 -73
  29. package/docs/v7/features/enterprise.mdx +155 -39
  30. package/docs/v7/features/fast.mdx +63 -81
  31. package/docs/v7/features/managed-sandboxes.mdx +384 -0
  32. package/docs/v7/features/network-monitoring.mdx +568 -0
  33. package/docs/v7/features/observable.mdx +3 -22
  34. package/docs/v7/features/parallel-execution.mdx +381 -0
  35. package/docs/v7/features/powerful.mdx +1 -1
  36. package/docs/v7/features/reports.mdx +414 -0
  37. package/docs/v7/features/sandbox-customization.mdx +229 -0
  38. package/docs/v7/features/scalable.mdx +217 -2
  39. package/docs/v7/features/stable.mdx +106 -147
  40. package/docs/v7/features/system-performance.mdx +616 -0
  41. package/docs/v7/features/test-analytics.mdx +373 -0
  42. package/docs/v7/features/test-cases.mdx +393 -0
  43. package/docs/v7/features/test-replays.mdx +408 -0
  44. package/docs/v7/features/test-reports.mdx +308 -0
  45. package/docs/v7/getting-started/{running-and-debugging.mdx โ†’ debugging-tests.mdx} +12 -142
  46. package/docs/v7/getting-started/quickstart.mdx +22 -305
  47. package/docs/v7/getting-started/running-tests.mdx +173 -0
  48. package/docs/v7/overview/what-is-testdriver.mdx +2 -14
  49. package/docs/v7/presets/chrome-extension.mdx +147 -122
  50. package/interfaces/cli/commands/init.js +3 -3
  51. package/interfaces/cli/lib/base.js +3 -2
  52. package/interfaces/logger.js +0 -2
  53. package/interfaces/shared-test-state.mjs +0 -5
  54. package/interfaces/vitest-plugin.mjs +70 -50
  55. package/lib/core/Dashcam.js +60 -85
  56. package/lib/vitest/hooks.mjs +42 -50
  57. package/package.json +1 -1
  58. package/sdk-log-formatter.js +350 -175
  59. package/sdk.d.ts +36 -3
  60. package/sdk.js +431 -116
  61. package/setup/aws/cloudformation.yaml +2 -2
  62. package/setup/aws/self-hosted.yml +1 -1
  63. package/test/testdriver/chrome-extension.test.mjs +55 -72
  64. package/test/testdriver/element-not-found.test.mjs +2 -1
  65. package/test/testdriver/hover-image.test.mjs +1 -1
  66. package/test/testdriver/scroll-until-text.test.mjs +10 -6
  67. package/test/testdriver/setup/lifecycleHelpers.mjs +19 -24
  68. package/test/testdriver/setup/testHelpers.mjs +18 -23
  69. package/vitest.config.mjs +3 -3
  70. package/.github/workflows/linux-tests.yml +0 -28
  71. package/docs/v7/getting-started/generating-tests.mdx +0 -525
  72. package/test/testdriver/auto-cache-key-demo.test.mjs +0 -56
@@ -8,6 +8,15 @@ const chalk = require("chalk");
8
8
  * Now with full UTF-8 and emoji support! ๐Ÿš€
9
9
  */
10
10
 
11
+ // Duration threshold configurations for different contexts
12
+ const DURATION_THRESHOLDS = {
13
+ default: { fast: 3000, medium: 10000 },
14
+ action: { fast: 100, medium: 500 },
15
+ redraw: { fast: 5000, medium: 10000 },
16
+ quickAction: { fast: 50, medium: 200 },
17
+ test: { fast: 1000, medium: 5000 },
18
+ };
19
+
11
20
  class SDKLogFormatter {
12
21
  constructor(options = {}) {
13
22
  this.testContext = {
@@ -41,6 +50,156 @@ class SDKLogFormatter {
41
50
  return `[${seconds}s]`;
42
51
  }
43
52
 
53
+ /**
54
+ * Add elapsed time to parts array if available
55
+ * @param {Array} parts - Array to push time string to
56
+ * @param {boolean} dim - Whether to dim the time string
57
+ */
58
+ addTimestamp(parts, dim = true) {
59
+ const timeStr = this.getElapsedTime();
60
+ if (timeStr) {
61
+ parts.push(dim ? chalk.dim(timeStr) : timeStr);
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Get color function based on duration and thresholds
67
+ * @param {number} durationMs - Duration in milliseconds
68
+ * @param {string} thresholdKey - Key from DURATION_THRESHOLDS
69
+ * @returns {Function} Chalk color function
70
+ */
71
+ getDurationColor(durationMs, thresholdKey = "default") {
72
+ const thresholds = DURATION_THRESHOLDS[thresholdKey] || DURATION_THRESHOLDS.default;
73
+ if (durationMs < thresholds.fast) return chalk.green;
74
+ if (durationMs < thresholds.medium) return chalk.yellow;
75
+ return chalk.red;
76
+ }
77
+
78
+ /**
79
+ * Format duration with appropriate color
80
+ * @param {number|string} duration - Duration in ms
81
+ * @param {string} thresholdKey - Key from DURATION_THRESHOLDS
82
+ * @param {boolean} showSeconds - Show as seconds (true) or raw (false)
83
+ * @returns {string} Formatted duration string
84
+ */
85
+ formatDurationColored(duration, thresholdKey = "default", showSeconds = true) {
86
+ const durationMs = parseInt(duration);
87
+ const color = this.getDurationColor(durationMs, thresholdKey);
88
+ const display = showSeconds ? `(${(durationMs / 1000).toFixed(1)}s)` : `(${duration})`;
89
+ return color(display);
90
+ }
91
+
92
+ /**
93
+ * Join metadata parts with separator
94
+ * @param {Array} metaParts - Array of metadata strings
95
+ * @returns {string} Joined metadata string with separators
96
+ */
97
+ joinMetaParts(metaParts) {
98
+ if (metaParts.length === 0) return "";
99
+ return chalk.dim("ยท") + " " + metaParts.join(chalk.dim(" ยท "));
100
+ }
101
+
102
+ /**
103
+ * Create an indented result line prefix (for child results)
104
+ * @returns {string} Indented arrow prefix
105
+ */
106
+ getResultPrefix() {
107
+ return " " + chalk.dim("โ†’");
108
+ }
109
+
110
+ /**
111
+ * Format a nested action result line (scrolled, clicked, typed, pressed keys, etc.)
112
+ * @param {string} message - The action message (e.g., "scrolled down 300px", "pressed keys: tab")
113
+ * @param {number} durationMs - Duration in milliseconds
114
+ * @returns {string} Formatted nested result line
115
+ */
116
+ formatNestedAction(message, durationMs) {
117
+ return this.getResultPrefix() + " " + chalk.dim(message) + " " + this.formatDurationColored(durationMs);
118
+ }
119
+
120
+ /**
121
+ * Format a redraw/idle wait completion line
122
+ * @param {number} durationMs - Duration in milliseconds
123
+ * @returns {string} Formatted redraw complete line
124
+ */
125
+ formatRedrawComplete(durationMs) {
126
+ return this.formatNestedAction("flake protection", durationMs);
127
+ }
128
+
129
+ /**
130
+ * Format a scroll action result
131
+ * @param {string} direction - Scroll direction (up, down, left, right)
132
+ * @param {number} amount - Scroll amount in pixels
133
+ * @param {number} durationMs - Duration in milliseconds
134
+ * @returns {string} Formatted scroll result line
135
+ */
136
+ formatScrollResult(direction, amount, durationMs) {
137
+ return this.formatNestedAction(`scrolled ${direction} ${amount}px`, durationMs);
138
+ }
139
+
140
+ /**
141
+ * Format a click action result
142
+ * @param {string} button - Button type (left, right, middle)
143
+ * @param {number} x - X coordinate
144
+ * @param {number} y - Y coordinate
145
+ * @param {number} durationMs - Duration in milliseconds
146
+ * @returns {string} Formatted click result line
147
+ */
148
+ formatClickResult(button, x, y, durationMs) {
149
+ return this.formatNestedAction(`click ${button} clicking at ${x}, ${y}`, durationMs);
150
+ }
151
+
152
+ /**
153
+ * Format a type action result
154
+ * @param {string} text - Text that was typed (or "****" for secrets)
155
+ * @param {boolean} isSecret - Whether the text is a secret
156
+ * @param {number} durationMs - Duration in milliseconds
157
+ * @returns {string} Formatted type result line
158
+ */
159
+ formatTypeResult(text, isSecret, durationMs) {
160
+ const displayText = isSecret ? "secret ****" : `"${text}"`;
161
+ return this.formatNestedAction(`typed ${displayText}`, durationMs);
162
+ }
163
+
164
+ /**
165
+ * Format a press keys action result
166
+ * @param {string} keysDisplay - Keys that were pressed (comma-separated)
167
+ * @param {number} durationMs - Duration in milliseconds
168
+ * @returns {string} Formatted press keys result line
169
+ */
170
+ formatPressKeysResult(keysDisplay, durationMs) {
171
+ return this.formatNestedAction(`pressed keys: ${keysDisplay}`, durationMs);
172
+ }
173
+
174
+ /**
175
+ * Format a nested code display line (for exec commands)
176
+ * @param {string} codeDisplay - The code to display
177
+ * @returns {string} Formatted code line
178
+ */
179
+ formatCodeLine(codeDisplay) {
180
+ return this.getResultPrefix() + " " + chalk.dim(codeDisplay);
181
+ }
182
+
183
+ /**
184
+ * Format an exec complete result
185
+ * @param {number} exitCode - The exit code
186
+ * @param {number} durationMs - Duration in milliseconds
187
+ * @returns {string} Formatted exec result line
188
+ */
189
+ formatExecComplete(exitCode, durationMs) {
190
+ const statusText = exitCode !== 0
191
+ ? `failed (exit code ${exitCode})`
192
+ : `complete (exit code 0)`;
193
+ const statusColor = exitCode !== 0 ? chalk.red : chalk.green;
194
+
195
+ return this.formatResultLine(
196
+ statusText,
197
+ statusColor,
198
+ { duration: durationMs },
199
+ "action"
200
+ );
201
+ }
202
+
44
203
  /**
45
204
  * Format a log message in Vitest style
46
205
  * @param {string} type - Log type (info, success, error, action, debug)
@@ -54,8 +213,7 @@ class SDKLogFormatter {
54
213
  const parts = [];
55
214
 
56
215
  // Add timestamp/elapsed time
57
- const timeStr = this.getElapsedTime();
58
- if (timeStr) parts.push(timeStr);
216
+ this.addTimestamp(parts, false);
59
217
 
60
218
  // Add type prefix with color
61
219
  const prefix = this.getPrefix(type);
@@ -118,7 +276,7 @@ class SDKLogFormatter {
118
276
  drag: chalk.cyan("โœŠ"),
119
277
 
120
278
  // Keyboard actions
121
- type: chalk.yellow("โŒจ๏ธ"),
279
+ type: chalk.yellow("โŒจ๏ธ "),
122
280
  pressKeys: chalk.yellow("๐ŸŽน"),
123
281
 
124
282
  // Navigation
@@ -149,7 +307,7 @@ class SDKLogFormatter {
149
307
  debug: chalk.gray("๐Ÿ”ง"),
150
308
 
151
309
  // Default
152
- action: chalk.cyan("โ–ถ๏ธ"),
310
+ action: chalk.cyan("โ–ถ๏ธ "),
153
311
  };
154
312
  return prefixes[type] || chalk.gray("โ€ข");
155
313
  }
@@ -173,94 +331,109 @@ class SDKLogFormatter {
173
331
  }
174
332
 
175
333
  /**
176
- * Format an element finding message (when search starts) ๐Ÿ”
177
- * @param {string} description - Element description
334
+ * Format a "finding" style message (when search starts) ๐Ÿ”
335
+ * @param {string} prefixType - Prefix type for getPrefix
336
+ * @param {string} label - Action label (e.g., "Finding", "Finding All", "Asserting")
337
+ * @param {string} description - Element/assertion description
178
338
  * @returns {string} Formatted message
179
339
  */
180
- formatElementFinding(description) {
340
+ formatFindingStyle(prefixType, label, description) {
181
341
  const parts = [];
182
-
183
- // Time and icon on same line
184
- const timeStr = this.getElapsedTime();
185
- if (timeStr) {
186
- parts.push(chalk.dim(timeStr));
187
- }
188
- parts.push(this.getPrefix("find"));
189
-
190
- // Main message with emphasis
191
- parts.push(chalk.bold.cyan("Finding"));
342
+ this.addTimestamp(parts);
343
+ parts.push(this.getPrefix(prefixType));
344
+ parts.push(chalk.bold.cyan(label));
192
345
  parts.push(chalk.cyan(`"${description}"`));
193
-
194
346
  return parts.join(" ");
195
347
  }
196
348
 
197
349
  /**
198
- * Format an element found message with AWESOME styling ๐ŸŽฏ
350
+ * Format an element finding message (when search starts) ๐Ÿ”
199
351
  * @param {string} description - Element description
200
- * @param {Object} meta - Element metadata (coordinates, duration, cache hit)
201
352
  * @returns {string} Formatted message
202
353
  */
203
- formatElementFound(description, meta = {}) {
204
- const parts = [];
205
-
206
- // Time and icon on same line
207
- const timeStr = this.getElapsedTime();
208
- if (timeStr) {
209
- parts.push(chalk.dim(timeStr));
210
- }
211
- parts.push(this.getPrefix("find"));
212
-
213
- // Main message with emphasis
214
- parts.push(chalk.bold.green("Found"));
215
- parts.push(chalk.cyan(`"${description}"`));
354
+ formatElementFinding(description) {
355
+ return this.formatFindingStyle("find", "Finding", description);
356
+ }
216
357
 
217
- // Metadata on same line with subtle styling
358
+ /**
359
+ * Build common metadata parts for result messages
360
+ * @param {Object} meta - Metadata object
361
+ * @param {string} thresholdKey - Duration threshold key
362
+ * @returns {Array} Array of formatted metadata strings
363
+ */
364
+ buildResultMetaParts(meta, thresholdKey = "default") {
218
365
  const metaParts = [];
366
+
219
367
  if (meta.x !== undefined && meta.y !== undefined) {
220
368
  metaParts.push(chalk.dim.gray(`๐Ÿ“ (${meta.x}, ${meta.y})`));
221
369
  }
222
- if (meta.duration) {
223
- const durationMs = parseInt(meta.duration);
224
- const durationColor =
225
- durationMs < 100
226
- ? chalk.green
227
- : durationMs < 500
228
- ? chalk.yellow
229
- : chalk.red;
230
- metaParts.push(chalk.dim(`โฑ๏ธ ${durationColor(meta.duration)}`));
370
+ if (meta.selectorId && meta.consoleUrl) {
371
+ const cacheUrl = `${meta.consoleUrl}/cache/${meta.selectorId}`;
372
+ metaParts.push(chalk.blue.underline(cacheUrl));
373
+ }
374
+ if (meta.error) {
375
+ metaParts.push(chalk.dim.red(meta.error));
231
376
  }
232
377
  if (meta.cacheHit) {
233
378
  metaParts.push(chalk.bold.yellow("โšก cached"));
234
379
  }
380
+ // Duration always last
381
+ if (meta.duration) {
382
+ metaParts.push(this.formatDurationColored(meta.duration, thresholdKey));
383
+ }
384
+
385
+ return metaParts;
386
+ }
235
387
 
388
+ /**
389
+ * Format a result line (indented child result)
390
+ * @param {string} statusText - Status text (e.g., "found", "not found")
391
+ * @param {Function} statusColor - Chalk color function for status
392
+ * @param {Object} meta - Metadata object
393
+ * @param {string} thresholdKey - Duration threshold key
394
+ * @returns {string} Formatted result line
395
+ */
396
+ formatResultLine(statusText, statusColor, meta = {}, thresholdKey = "default") {
397
+ const parts = [];
398
+ this.addTimestamp(parts);
399
+ parts.push(this.getResultPrefix());
400
+ parts.push(statusColor(statusText));
401
+
402
+ const metaParts = this.buildResultMetaParts(meta, thresholdKey);
236
403
  if (metaParts.length > 0) {
237
- parts.push(chalk.dim("ยท"));
238
- parts.push(metaParts.join(chalk.dim(" ยท ")));
404
+ parts.push(this.joinMetaParts(metaParts));
239
405
  }
240
-
406
+
241
407
  return parts.join(" ");
242
408
  }
243
409
 
244
410
  /**
245
- * Format a finding all message (when search starts) ๐Ÿ”Ž
411
+ * Format an element found message with AWESOME styling ๐ŸŽฏ
246
412
  * @param {string} description - Element description
413
+ * @param {Object} meta - Element metadata (coordinates, duration, cache hit)
247
414
  * @returns {string} Formatted message
248
415
  */
249
- formatElementsFinding(description) {
250
- const parts = [];
251
-
252
- // Time and icon on same line
253
- const timeStr = this.getElapsedTime();
254
- if (timeStr) {
255
- parts.push(chalk.dim(timeStr));
256
- }
257
- parts.push(this.getPrefix("findAll"));
416
+ formatElementFound(description, meta = {}) {
417
+ return this.formatResultLine("found", chalk.green, meta);
418
+ }
258
419
 
259
- // Main message with emphasis
260
- parts.push(chalk.bold.cyan("Finding All"));
261
- parts.push(chalk.cyan(`"${description}"`));
420
+ /**
421
+ * Format an element not found message with styling โŒ
422
+ * @param {string} description - Element description
423
+ * @param {Object} meta - Metadata (duration, error)
424
+ * @returns {string} Formatted message
425
+ */
426
+ formatElementNotFound(description, meta = {}) {
427
+ return this.formatResultLine("not found", chalk.red, meta);
428
+ }
262
429
 
263
- return parts.join(" ");
430
+ /**
431
+ * Format a finding all message (when search starts) ๐Ÿ”Ž
432
+ * @param {string} description - Element description
433
+ * @returns {string} Formatted message
434
+ */
435
+ formatElementsFinding(description) {
436
+ return this.formatFindingStyle("findAll", "Finding All", description);
264
437
  }
265
438
 
266
439
  /**
@@ -272,38 +445,20 @@ class SDKLogFormatter {
272
445
  */
273
446
  formatElementsFound(description, count, meta = {}) {
274
447
  const parts = [];
448
+ this.addTimestamp(parts);
449
+ parts.push(this.getResultPrefix());
450
+ parts.push(chalk.green(`found ${count} elements`));
275
451
 
276
- // Time and icon on same line
277
- const timeStr = this.getElapsedTime();
278
- if (timeStr) {
279
- parts.push(chalk.dim(timeStr));
280
- }
281
- parts.push(this.getPrefix("findAll"));
282
-
283
- // Main message with emphasis
284
- parts.push(chalk.bold.green("Found"));
285
- parts.push(chalk.cyan(`${count}`));
286
- parts.push(chalk.cyan(`"${description}"`));
287
-
288
- // Metadata on same line with subtle styling
289
452
  const metaParts = [];
290
- if (meta.duration) {
291
- const durationMs = parseInt(meta.duration);
292
- const durationColor =
293
- durationMs < 100
294
- ? chalk.green
295
- : durationMs < 500
296
- ? chalk.yellow
297
- : chalk.red;
298
- metaParts.push(chalk.dim(`โฑ๏ธ ${durationColor(meta.duration)}`));
299
- }
300
453
  if (meta.cacheHit) {
301
454
  metaParts.push(chalk.bold.yellow("โšก cached"));
302
455
  }
456
+ if (meta.duration) {
457
+ metaParts.push(this.formatDurationColored(meta.duration));
458
+ }
303
459
 
304
460
  if (metaParts.length > 0) {
305
- parts.push(chalk.dim("ยท"));
306
- parts.push(metaParts.join(chalk.dim(" ยท ")));
461
+ parts.push(this.joinMetaParts(metaParts));
307
462
  }
308
463
 
309
464
  return parts.join(" ");
@@ -315,79 +470,134 @@ class SDKLogFormatter {
315
470
  * @returns {string} Formatted message
316
471
  */
317
472
  formatAsserting(assertion) {
473
+ return this.formatFindingStyle("assert", "Asserting", assertion);
474
+ }
475
+ /**
476
+ * Format the assertion result as a subtask line
477
+ * @param {boolean} passed - Whether assertion passed
478
+ * @param {string} response - The AI response message
479
+ * @param {number} durationMs - Duration in milliseconds
480
+ * @returns {string} Formatted result line
481
+ */
482
+ formatAssertResult(passed, response, durationMs) {
318
483
  const parts = [];
319
-
320
- // Time and icon
321
- const timeStr = this.getElapsedTime();
322
- if (timeStr) {
323
- parts.push(chalk.dim(timeStr));
484
+ this.addTimestamp(parts);
485
+ parts.push(this.getResultPrefix());
486
+
487
+ if (passed) {
488
+ parts.push(chalk.green("passed"));
489
+ } else {
490
+ parts.push(chalk.red("failed"));
324
491
  }
325
-
326
- parts.push(this.getPrefix("assert"));
327
- parts.push(chalk.bold.cyan("Asserting"));
328
- parts.push(chalk.cyan(`"${assertion}"`));
329
-
492
+
493
+ // Add the response message (trimmed)
494
+ if (response) {
495
+ const trimmedResponse = response.trim().split('\n')[0]; // First line only
496
+ parts.push(chalk.dim(trimmedResponse));
497
+ }
498
+
499
+ // Add duration
500
+ if (durationMs) {
501
+ parts.push(this.formatDurationColored(durationMs, "action"));
502
+ }
503
+
330
504
  return parts.join(" ");
331
505
  }
332
506
 
507
+ // Action color mapping (shared between formatAction and formatActionComplete)
508
+ static ACTION_COLORS = {
509
+ click: chalk.bold.cyan,
510
+ hover: chalk.bold.blue,
511
+ type: chalk.bold.yellow,
512
+ scroll: chalk.bold.magenta,
513
+ assert: chalk.bold.green,
514
+ wait: chalk.bold.yellow,
515
+ };
516
+
333
517
  /**
334
- * Format an action message with AWESOME emojis! ๐ŸŽฌ
518
+ * Build action message parts (shared logic for formatAction and formatActionComplete)
335
519
  * @param {string} action - Action type
336
520
  * @param {string} description - Description or target
337
- * @param {Object} meta - Action metadata
338
- * @returns {string} Formatted message
521
+ * @returns {Array} Array of formatted parts
339
522
  */
340
- formatAction(action, description, meta = {}) {
523
+ buildActionParts(action, description) {
341
524
  const parts = [];
525
+ this.addTimestamp(parts);
342
526
 
343
- // Time and icon
344
- const timeStr = this.getElapsedTime();
345
- if (timeStr) {
346
- parts.push(chalk.dim(timeStr));
347
- }
348
-
349
- // Use action-specific prefix
350
527
  const actionKey = action.toLowerCase().replace(/\s+/g, "");
351
528
  parts.push(this.getPrefix(actionKey));
352
529
 
353
- // Action text with emphasis and color coding
354
- const actionText =
355
- action.charAt(0).toUpperCase() + action.slice(1).toLowerCase();
356
- const actionColors = {
357
- click: chalk.bold.cyan,
358
- hover: chalk.bold.blue,
359
- type: chalk.bold.yellow,
360
- scroll: chalk.bold.magenta,
361
- assert: chalk.bold.green,
362
- wait: chalk.bold.yellow,
363
- };
364
- const colorFn = actionColors[actionKey] || chalk.bold.white;
530
+ const actionText = action.charAt(0).toUpperCase() + action.slice(1).toLowerCase();
531
+ const colorFn = SDKLogFormatter.ACTION_COLORS[actionKey] || chalk.bold.white;
365
532
  parts.push(colorFn(actionText));
366
533
 
367
- // Target with color
368
534
  if (description) {
369
535
  parts.push(chalk.cyan(`"${description}"`));
370
536
  }
371
537
 
372
- // Additional metadata
538
+ return { parts, actionKey };
539
+ }
540
+
541
+ /**
542
+ * Format an action message with AWESOME emojis! ๐ŸŽฌ
543
+ * @param {string} action - Action type
544
+ * @param {string} description - Description or target
545
+ * @param {Object} meta - Action metadata
546
+ * @returns {string} Formatted message
547
+ */
548
+ formatAction(action, description, meta = {}) {
549
+ const { parts } = this.buildActionParts(action, description);
550
+
373
551
  const metaParts = [];
374
552
  if (meta.text) {
375
553
  metaParts.push(chalk.gray(`โ†’ ${chalk.white(meta.text)}`));
376
554
  }
377
555
  if (meta.duration) {
378
- const durationMs = parseInt(meta.duration);
379
- const durationColor =
380
- durationMs < 50
381
- ? chalk.green
382
- : durationMs < 200
383
- ? chalk.yellow
384
- : chalk.red;
385
- metaParts.push(chalk.dim(`โฑ๏ธ ${durationColor(meta.duration)}`));
556
+ metaParts.push(chalk.dim(`โฑ๏ธ ${this.formatDurationColored(meta.duration, "quickAction", false)}`));
386
557
  }
387
558
 
388
559
  if (metaParts.length > 0) {
389
- parts.push(chalk.dim("ยท"));
390
- parts.push(metaParts.join(chalk.dim(" ยท ")));
560
+ parts.push(this.joinMetaParts(metaParts));
561
+ }
562
+
563
+ return parts.join(" ");
564
+ }
565
+
566
+ /**
567
+ * Format an action complete message with separate action and redraw durations ๐ŸŽฌ
568
+ * @param {string} action - Action type
569
+ * @param {string} description - Description or target
570
+ * @param {Object} meta - Action metadata
571
+ * @param {number} meta.actionDuration - Duration of the action itself in ms
572
+ * @param {number} meta.redrawDuration - Duration of the redraw wait in ms
573
+ * @param {boolean} meta.cacheHit - Whether cache was hit
574
+ * @returns {string} Formatted message
575
+ */
576
+ formatActionComplete(action, description, meta = {}) {
577
+ const { parts } = this.buildActionParts(action, description);
578
+
579
+ const metaParts = [];
580
+
581
+ if (meta.actionDuration !== undefined) {
582
+ const durationMs = parseInt(meta.actionDuration);
583
+ const durationSec = (durationMs / 1000).toFixed(1) + 's';
584
+ const color = this.getDurationColor(durationMs, "action");
585
+ metaParts.push(chalk.dim(`โšก ${color(durationSec)}`));
586
+ }
587
+
588
+ if (meta.redrawDuration !== undefined) {
589
+ const durationMs = parseInt(meta.redrawDuration);
590
+ const durationSec = (durationMs / 1000).toFixed(1) + 's';
591
+ const color = this.getDurationColor(durationMs, "redraw");
592
+ metaParts.push(chalk.dim(`๐Ÿ”„ ${color(durationSec)}`));
593
+ }
594
+
595
+ if (meta.cacheHit) {
596
+ metaParts.push(chalk.bold.yellow("โšก cached"));
597
+ }
598
+
599
+ if (metaParts.length > 0) {
600
+ parts.push(this.joinMetaParts(metaParts));
391
601
  }
392
602
 
393
603
  return parts.join(" ");
@@ -402,12 +612,7 @@ class SDKLogFormatter {
402
612
  */
403
613
  formatAssertion(assertion, passed, meta = {}) {
404
614
  const parts = [];
405
-
406
- // Time and icon
407
- const timeStr = this.getElapsedTime();
408
- if (timeStr) {
409
- parts.push(chalk.dim(timeStr));
410
- }
615
+ this.addTimestamp(parts);
411
616
 
412
617
  if (passed) {
413
618
  parts.push(this.getPrefix("success"));
@@ -424,15 +629,8 @@ class SDKLogFormatter {
424
629
  }
425
630
 
426
631
  if (meta.duration) {
427
- const durationMs = parseInt(meta.duration);
428
- const durationColor =
429
- durationMs < 100
430
- ? chalk.green
431
- : durationMs < 500
432
- ? chalk.yellow
433
- : chalk.red;
434
632
  parts.push(chalk.dim("ยท"));
435
- parts.push(chalk.dim(`โฑ๏ธ ${durationColor(meta.duration)}`));
633
+ parts.push(chalk.dim(`โฑ๏ธ ${this.formatDurationColored(meta.duration, "action", false)}`));
436
634
  }
437
635
 
438
636
  return parts.join(" ");
@@ -446,12 +644,7 @@ class SDKLogFormatter {
446
644
  */
447
645
  formatError(message, error) {
448
646
  const parts = [];
449
-
450
- const timeStr = this.getElapsedTime();
451
- if (timeStr) {
452
- parts.push(chalk.dim(timeStr));
453
- }
454
-
647
+ this.addTimestamp(parts);
455
648
  parts.push(this.getPrefix("error"));
456
649
  parts.push(chalk.red.bold(message));
457
650
 
@@ -471,12 +664,7 @@ class SDKLogFormatter {
471
664
  */
472
665
  formatConnection(type, meta = {}) {
473
666
  const parts = [];
474
-
475
- const timeStr = this.getElapsedTime();
476
- if (timeStr) {
477
- parts.push(chalk.dim(timeStr));
478
- }
479
-
667
+ this.addTimestamp(parts);
480
668
  parts.push(this.getPrefix(type));
481
669
 
482
670
  if (type === "connect") {
@@ -503,12 +691,7 @@ class SDKLogFormatter {
503
691
  */
504
692
  formatScreenshot(meta = {}) {
505
693
  const parts = [];
506
-
507
- const timeStr = this.getElapsedTime();
508
- if (timeStr) {
509
- parts.push(chalk.dim(timeStr));
510
- }
511
-
694
+ this.addTimestamp(parts);
512
695
  parts.push(this.getPrefix("screenshot"));
513
696
  parts.push(chalk.bold.blue("Screenshot"));
514
697
 
@@ -533,7 +716,6 @@ class SDKLogFormatter {
533
716
  */
534
717
  formatCacheStatus(hit, meta = {}) {
535
718
  const parts = [];
536
-
537
719
  parts.push(this.getPrefix(hit ? "cacheHit" : "cacheMiss"));
538
720
 
539
721
  if (hit) {
@@ -565,9 +747,7 @@ class SDKLogFormatter {
565
747
  const width = Math.min(60, Math.max(title.length + 4, 40));
566
748
  const topLine = chalk.dim("โ•ญ" + "โ”€".repeat(width - 2) + "โ•ฎ");
567
749
  const titleLine =
568
- `${chalk.dim("โ”‚")} ${emoji} ${chalk.bold.white(title)}`.padEnd(
569
- width + 20,
570
- ) + chalk.dim("โ”‚");
750
+ `${chalk.dim("โ”‚")} ${emoji} ${chalk.bold.white(title)}`.padEnd(width + 20) + chalk.dim("โ”‚");
571
751
  const bottomLine = chalk.dim("โ•ฐ" + "โ”€".repeat(width - 2) + "โ•ฏ");
572
752
  return `\n${topLine}\n${titleLine}\n${bottomLine}\n`;
573
753
  }
@@ -605,8 +785,9 @@ class SDKLogFormatter {
605
785
  parts.push(chalk.dim(`โฑ๏ธ ${stats.duration}`));
606
786
  }
607
787
 
788
+ const divider = this.formatDivider();
608
789
  const separator = chalk.dim(" โ”‚ ");
609
- return `\n${chalk.dim("โ”€".repeat(60))}\n${parts.join(separator)}\n${chalk.dim("โ”€".repeat(60))}\n`;
790
+ return `\n${divider}\n${parts.join(separator)}\n${divider}\n`;
610
791
  }
611
792
 
612
793
  /**
@@ -648,7 +829,6 @@ class SDKLogFormatter {
648
829
  */
649
830
  formatWaiting(message, elapsed) {
650
831
  const parts = [];
651
-
652
832
  parts.push(this.getPrefix("wait"));
653
833
  parts.push(chalk.bold.yellow("Waiting"));
654
834
  parts.push(chalk.cyan(message));
@@ -691,14 +871,9 @@ class SDKLogFormatter {
691
871
 
692
872
  if (duration) {
693
873
  const seconds = (duration / 1000).toFixed(2);
694
- const durationColor =
695
- duration < 1000
696
- ? chalk.green
697
- : duration < 5000
698
- ? chalk.yellow
699
- : chalk.red;
874
+ const color = this.getDurationColor(duration, "test");
700
875
  parts.push(chalk.dim("ยท"));
701
- parts.push(durationColor(`${seconds}s`));
876
+ parts.push(color(`${seconds}s`));
702
877
  }
703
878
 
704
879
  return `\n${parts.join(" ")}\n`;