@@ -2,15 +2,24 @@ import {
22 getConnectionDoneHandler ,
33 getOnMessageHandler ,
44 getPayloadUpdateHandler ,
5+ handleLogEvent ,
6+ parseLogMessage ,
57 websocketUpgradeHandler ,
68} from './handlers.js'
79import { SetupWebSocketConnectionOptions } from './models.js'
810import { ExtensionsEndpointPayload } from '../payload/models.js'
9- import { vi , describe , test , expect } from 'vitest'
11+ import { vi , describe , test , expect , Mock } from 'vitest'
12+ import { useConcurrentOutputContext } from '@shopify/cli-kit/node/ui/components'
1013import WebSocket , { RawData , WebSocketServer } from 'ws'
1114import { IncomingMessage } from 'h3'
15+ import colors from '@shopify/cli-kit/node/colors'
16+ import { outputContent , outputToken } from '@shopify/cli-kit/node/output'
1217import { Duplex } from 'stream'
1318
19+ vi . mock ( '@shopify/cli-kit/node/ui/components' , ( ) => ( {
20+ useConcurrentOutputContext : vi . fn ( ) ,
21+ } ) )
22+
1423function getMockRequest ( ) {
1524 const request = {
1625 url : '/extensions' ,
@@ -57,6 +66,9 @@ function getMockSetupWebSocketConnectionOptions() {
5766 updateExtensions : vi . fn ( ) ,
5867 } ,
5968 manifestVersion : '3' ,
69+ stdout : {
70+ write : vi . fn ( ) ,
71+ } ,
6072 } as unknown as SetupWebSocketConnectionOptions
6173}
6274
@@ -183,4 +195,134 @@ describe('getOnMessageHandler()', () => {
183195 } ) as unknown as RawData
184196 wss . clients . forEach ( ( ws ) => expect ( ws . send ) . toHaveBeenCalledWith ( outgoingMessage ) )
185197 } )
198+
199+ test ( 'on an incoming log event calls handleLogMessage and does not notify clients' , ( ) => {
200+ const wss = getMockWebsocketServer ( )
201+ const options = getMockSetupWebSocketConnectionOptions ( )
202+ const data = JSON . stringify ( {
203+ event : 'log' ,
204+ data : {
205+ type : 'info' ,
206+ message : 'Test log message' ,
207+ extensionName : 'test-extension' ,
208+ } ,
209+ } ) as unknown as RawData
210+
211+ getOnMessageHandler ( wss , options ) ( data )
212+
213+ // Verify useConcurrentOutputContext (any therefore handleLogMessage) was called with correct parameters
214+ expect ( useConcurrentOutputContext ) . toHaveBeenCalledWith (
215+ { outputPrefix : 'test-extension' , stripAnsi : false } ,
216+ expect . any ( Function ) ,
217+ )
218+
219+ // Verify no client messages were sent since this was a log event
220+ wss . clients . forEach ( ( ws ) => expect ( ws . send ) . not . toHaveBeenCalled ( ) )
221+ } )
222+ } )
223+
224+ describe ( 'parseLogMessage()' , ( ) => {
225+ test ( 'parses and formats JSON array of strings' , ( ) => {
226+ const message = JSON . stringify ( [ 'Hello' , 'world' , 'test' ] , null , 2 )
227+ const result = parseLogMessage ( message )
228+ expect ( result ) . toBe ( 'Hello world test' )
229+ } )
230+
231+ test ( 'parses and formats JSON array with mixed types' , ( ) => {
232+ const message = JSON . stringify ( [ 'String' , 42 , true , null ] , null , 2 )
233+ const result = parseLogMessage ( message )
234+ expect ( result ) . toBe ( 'String 42 true null' )
235+ } )
236+
237+ test ( 'parses and formats JSON array with objects' , ( ) => {
238+ const object = { user : 'john' , age : 30 }
239+ const message = JSON . stringify ( [ 'Message:' , object ] , null , 2 )
240+ const result = parseLogMessage ( message )
241+ expect ( result ) . toBe ( outputContent `Message: ${ outputToken . json ( object ) } ` . value )
242+ } )
243+
244+ test ( 'returns original message when JSON parsing fails' , ( ) => {
245+ const invalidJson = 'This is not JSON'
246+ const result = parseLogMessage ( invalidJson )
247+ expect ( result ) . toBe ( 'This is not JSON' )
248+ } )
249+
250+ test ( 'returns original message for JSON that is not an array' , ( ) => {
251+ const malformedJson = '{"invalid": json}'
252+ const result = parseLogMessage ( malformedJson )
253+ expect ( result ) . toBe ( '{"invalid": json}' )
254+ } )
255+ } )
256+
257+ describe ( 'handleLogEvent()' , ( ) => {
258+ // Helper function to abstract the common expect pattern
259+ function expectLogMessageOutput (
260+ extensionName : string ,
261+ expectedOutput : string ,
262+ options : SetupWebSocketConnectionOptions ,
263+ ) {
264+ expect ( useConcurrentOutputContext ) . toHaveBeenCalledWith (
265+ { outputPrefix : extensionName , stripAnsi : false } ,
266+ expect . any ( Function ) ,
267+ )
268+ const contextCallback = ( useConcurrentOutputContext as Mock ) . mock . calls [ 0 ] ?. [ 1 ]
269+ contextCallback ( )
270+
271+ expect ( options . stdout . write ) . toHaveBeenCalledWith ( expectedOutput )
272+ }
273+
274+ test ( 'outputs info level log message with correct formatting' , ( ) => {
275+ const options = getMockSetupWebSocketConnectionOptions ( )
276+ const eventData = {
277+ type : 'info' ,
278+ message : 'Test info message' ,
279+ extensionName : 'test-extension' ,
280+ }
281+ handleLogEvent ( eventData , options )
282+
283+ expectLogMessageOutput ( 'test-extension' , `Test info message` , options )
284+ } )
285+
286+ test ( 'outputs log message with parsed JSON array' , ( ) => {
287+ const options = getMockSetupWebSocketConnectionOptions ( )
288+ const message = JSON . stringify ( [ 'Hello' , 'world' , { user : 'test' } ] , null , 2 )
289+ const eventData = {
290+ type : 'info' ,
291+ message,
292+ extensionName : 'test-extension' ,
293+ }
294+
295+ handleLogEvent ( eventData , options )
296+
297+ expectLogMessageOutput (
298+ 'test-extension' ,
299+ outputContent `Hello world ${ outputToken . json ( { user : 'test' } ) } ` . value ,
300+ options ,
301+ )
302+ } )
303+
304+ test ( 'outputs error level log message with error formatting' , ( ) => {
305+ const options = getMockSetupWebSocketConnectionOptions ( )
306+ const eventData = {
307+ type : 'error' ,
308+ message : 'Test error message' ,
309+ extensionName : 'error-extension' ,
310+ }
311+
312+ handleLogEvent ( eventData , options )
313+
314+ expectLogMessageOutput ( 'error-extension' , `${ colors . bold . redBright ( 'ERROR' ) } : Test error message` , options )
315+ } )
316+
317+ test ( 'handles unknown log type without erroring' , ( ) => {
318+ const options = getMockSetupWebSocketConnectionOptions ( )
319+ const eventData = {
320+ type : 'unknown-type' ,
321+ message : 'Message with unknown log type' ,
322+ extensionName : 'test-extension' ,
323+ }
324+ handleLogEvent ( eventData , options )
325+
326+ expectLogMessageOutput ( 'test-extension' , 'UNKNOWN-TYPE: Message with unknown log type' , options )
327+ } )
186328} )
0 commit comments