@@ -8,21 +8,54 @@ import { resolve } from 'import-meta-resolve'
88import { SevereServiceError } from 'webdriverio'
99import type { WebDriverCommands } from '@wdio/protocols'
1010
11- import { PAGE_TRANSITION_COMMANDS } from './constants.js'
12- import { type CommandLog } from './types.js'
13- import { type TraceLog } from './types.js'
11+ import { PAGE_TRANSITION_COMMANDS , ANSI_REGEX , CONSOLE_METHODS , LOG_LEVEL_PATTERNS , ERROR_INDICATORS , LOG_SOURCES } from './constants.js'
12+ import { type CommandLog , type TraceLog , type LogLevel } from './types.js'
1413
1514const log = logger ( '@wdio/devtools-service:SessionCapturer' )
1615
16+ /**
17+ * Generic helper to strip ANSI escape codes from text
18+ */
19+ const stripAnsi = ( text : string ) : string => text . replace ( ANSI_REGEX , '' )
20+
21+ /**
22+ * Generic helper to detect log level from text content
23+ */
24+ const detectLogLevel = ( text : string ) : LogLevel => {
25+ const cleanText = stripAnsi ( text ) . toLowerCase ( )
26+
27+ // Check log level patterns in priority order
28+ for ( const { level, pattern } of LOG_LEVEL_PATTERNS ) {
29+ if ( pattern . test ( cleanText ) ) return level
30+ }
31+
32+ // Check for error indicators
33+ if ( ERROR_INDICATORS . some ( indicator => cleanText . includes ( indicator . toLowerCase ( ) ) ) ) {
34+ return 'error'
35+ }
36+
37+ return 'log'
38+ }
39+
40+ /**
41+ * Generic helper to create a console log entry
42+ */
43+ const createLogEntry = ( type : LogLevel , args : any [ ] , source : typeof LOG_SOURCES [ keyof typeof LOG_SOURCES ] ) : ConsoleLogs => ( {
44+ timestamp : Date . now ( ) ,
45+ type,
46+ args,
47+ source
48+ } )
49+
1750export class SessionCapturer {
1851 #ws: WebSocket | undefined
1952 #isInjected = false
20- #originalConsoleMethods: {
21- log : typeof console . log
22- info : typeof console . info
23- warn : typeof console . warn
24- error : typeof console . error
53+ #originalConsoleMethods: Record < typeof CONSOLE_METHODS [ number ] , typeof console . log >
54+ #originalProcessMethods: {
55+ stdoutWrite : typeof process . stdout . write
56+ stderrWrite : typeof process . stderr . write
2557 }
58+ #isCapturingConsole = false
2659 commandsLog : CommandLog [ ] = [ ]
2760 sources = new Map < string , string > ( )
2861 mutations : TraceMutation [ ] = [ ]
@@ -55,77 +88,85 @@ export class SessionCapturer {
5588 )
5689 }
5790
58- // Store original console methods
5991 this . #originalConsoleMethods = {
6092 log : console . log ,
6193 info : console . info ,
6294 warn : console . warn ,
6395 error : console . error
6496 }
6597
66- // Patch console methods to capture test logs
98+ this . #originalProcessMethods = {
99+ stdoutWrite : process . stdout . write . bind ( process . stdout ) ,
100+ stderrWrite : process . stderr . write . bind ( process . stderr )
101+ }
102+
67103 this . #patchConsole( )
104+ this . #patchProcessOutput( )
68105 }
69106
70107 #patchConsole( ) {
71- const consoleMethods = [ 'log' , 'info' , 'warn' , 'error' ] as const
72-
73- consoleMethods . forEach ( ( method ) => {
108+ CONSOLE_METHODS . forEach ( ( method ) => {
74109 const originalMethod = this . #originalConsoleMethods[ method ]
75110 console [ method ] = ( ...args : any [ ] ) => {
76- const logEntry : ConsoleLogs = {
77- timestamp : Date . now ( ) ,
78- type : method ,
79- args : args . map ( ( arg ) =>
80- typeof arg === 'object' && arg !== null
81- ? ( ( ) => {
82- try {
83- return JSON . stringify ( arg )
84- } catch {
85- return String ( arg )
86- }
87- } ) ( )
88- : String ( arg )
89- ) ,
90- source : 'test'
91- }
111+ const serializedArgs = args . map ( arg =>
112+ typeof arg === 'object' && arg !== null
113+ ? ( ( ) => { try { return JSON . stringify ( arg ) } catch { return String ( arg ) } } ) ( )
114+ : String ( arg )
115+ )
116+
117+ const logEntry = createLogEntry ( method , serializedArgs , LOG_SOURCES . TEST )
92118 this . consoleLogs . push ( logEntry )
93119 this . sendUpstream ( 'consoleLogs' , [ logEntry ] )
94- return originalMethod . apply ( console , args )
120+
121+ this . #isCapturingConsole = true
122+ const result = originalMethod . apply ( console , args )
123+ this . #isCapturingConsole = false
124+ return result
95125 }
96126 } )
97127 }
98128
129+ #patchProcessOutput( ) {
130+ const captureOutput = ( data : string | Uint8Array ) => {
131+ const text = typeof data === 'string' ? data : data . toString ( )
132+ if ( ! text ?. trim ( ) ) return
133+
134+ text . split ( '\n' )
135+ . filter ( line => line . trim ( ) )
136+ . forEach ( line => {
137+ const logEntry = createLogEntry ( detectLogLevel ( line ) , [ stripAnsi ( line ) ] , LOG_SOURCES . TERMINAL )
138+ this . consoleLogs . push ( logEntry )
139+ this . sendUpstream ( 'consoleLogs' , [ logEntry ] )
140+ } )
141+ }
142+
143+ const patchStream = ( stream : NodeJS . WriteStream , originalWrite : ( ...args : any [ ] ) => boolean ) => {
144+ const self = this
145+ stream . write = function ( data : any , ...rest : any [ ] ) : boolean {
146+ const result = originalWrite . call ( stream , data , ...rest )
147+ if ( data && ! self . #isCapturingConsole) captureOutput ( data )
148+ return result
149+ } as any
150+ }
151+
152+ patchStream ( process . stdout , this . #originalProcessMethods. stdoutWrite )
153+ patchStream ( process . stderr , this . #originalProcessMethods. stderrWrite )
154+ }
155+
99156 #restoreConsole( ) {
100- console . log = this . #originalConsoleMethods. log
101- console . info = this . #originalConsoleMethods. info
102- console . warn = this . #originalConsoleMethods. warn
103- console . error = this . #originalConsoleMethods. error
157+ CONSOLE_METHODS . forEach ( method => {
158+ console [ method ] = this . #originalConsoleMethods[ method ]
159+ } )
104160 }
105161
106162 cleanup ( ) {
107163 this . #restoreConsole( )
108- if ( this . #ws) {
109- this . #ws. close ( )
110- }
111164 }
112165
113166 get isReportingUpstream ( ) {
114167 return Boolean ( this . #ws) && this . #ws?. readyState === WebSocket . OPEN
115168 }
116169
117- /**
118- * after command hook
119- *
120- * Used to
121- * - capture command logs
122- * - capture trace data from the application under test
123- *
124- * @param {string } command command name
125- * @param {Array } args command arguments
126- * @param {object } result command result
127- * @param {Error } error command error
128- */
129170 async afterCommand (
130171 browser : WebdriverIO . Browser ,
131172 command : keyof WebDriverCommands ,
@@ -245,7 +286,7 @@ export class SessionCapturer {
245286 }
246287 if ( Array . isArray ( consoleLogs ) ) {
247288 const browserLogs = consoleLogs as ConsoleLogs [ ]
248- browserLogs . forEach ( ( log ) => ( log . source = 'browser' ) )
289+ browserLogs . forEach ( ( log ) => ( log . source = LOG_SOURCES . BROWSER ) )
249290 this . consoleLogs . push ( ...browserLogs )
250291 this . sendUpstream ( 'consoleLogs' , browserLogs )
251292 }
0 commit comments