@@ -324,4 +324,89 @@ describe('ComponentViewerWebviewProvider', () => {
324324 expect ( html ) . toContain ( 'padding-left:20px' ) ;
325325 expect ( html ) . toContain ( 'padding-left:36px' ) ;
326326 } ) ;
327+
328+ it ( 're-renders when the data provider fires onDidChangeTreeData' , ( ) => {
329+ // Override onDidChangeTreeData to capture the listener
330+ let changeListener : ( ( ) => void ) | undefined ;
331+ Object . defineProperty ( dataProvider , 'onDidChangeTreeData' , {
332+ value : ( listener : ( ) => void ) => {
333+ changeListener = listener ;
334+ return { dispose : jest . fn ( ) } ;
335+ } ,
336+ configurable : true ,
337+ } ) ;
338+
339+ const { view, getHtml } = makeMockWebviewView ( ) ;
340+ webviewProvider . resolveWebviewView ( view , { } as never , { } as never ) ;
341+ expect ( getHtml ( ) ) . toContain ( 'No component data available' ) ;
342+
343+ // Mutate data and fire change event
344+ dataProvider . setRoots ( [ makeGui ( { getGuiName : ( ) => 'Dynamic' } ) ] ) ;
345+ changeListener ?.( ) ;
346+
347+ expect ( getHtml ( ) ) . toContain ( 'Dynamic' ) ;
348+ } ) ;
349+
350+ it ( 'skips rendering a node whose getGuiId returns undefined' , ( ) => {
351+ const good = makeGui ( { getGuiName : ( ) => 'Good' , getGuiId : ( ) => 'ok' } ) ;
352+ const bad = makeGui ( { getGuiName : ( ) => 'Bad' , getGuiId : ( ) => undefined } ) ;
353+ dataProvider . setRoots ( [ good , bad ] ) ;
354+
355+ const { view, getHtml } = makeMockWebviewView ( ) ;
356+ webviewProvider . resolveWebviewView ( view , { } as never , { } as never ) ;
357+
358+ const html = getHtml ( ) ;
359+ expect ( html ) . toContain ( 'Good' ) ;
360+ expect ( html ) . not . toContain ( 'Bad' ) ;
361+ } ) ;
362+
363+ it ( 'handles undefined name and value gracefully' , ( ) => {
364+ const nodeNameOnly = makeGui ( {
365+ getGuiName : ( ) => 'OnlyName' ,
366+ getGuiValue : ( ) => undefined ,
367+ getGuiId : ( ) => 'n1' ,
368+ } ) ;
369+ const nodeValueOnly = makeGui ( {
370+ getGuiName : ( ) => undefined ,
371+ getGuiValue : ( ) => 'OnlyValue' ,
372+ getGuiId : ( ) => 'v1' ,
373+ } ) ;
374+ const nodeNeither = makeGui ( {
375+ getGuiName : ( ) => undefined ,
376+ getGuiValue : ( ) => undefined ,
377+ getGuiId : ( ) => 'e1' ,
378+ } ) ;
379+ dataProvider . setRoots ( [ nodeNameOnly , nodeValueOnly , nodeNeither ] ) ;
380+
381+ const { view, getHtml } = makeMockWebviewView ( ) ;
382+ webviewProvider . resolveWebviewView ( view , { } as never , { } as never ) ;
383+
384+ const html = getHtml ( ) ;
385+ expect ( html ) . toContain ( 'OnlyName' ) ;
386+ expect ( html ) . toContain ( 'OnlyValue' ) ;
387+ // Node with neither name nor value should still render (with empty tooltip)
388+ expect ( html ) . toContain ( 'data-row-id="e1"' ) ;
389+ } ) ;
390+
391+ it ( 'includes CSP meta tag in rendered HTML' , ( ) => {
392+ const { view, getHtml } = makeMockWebviewView ( ) ;
393+ webviewProvider . resolveWebviewView ( view , { } as never , { } as never ) ;
394+ expect ( getHtml ( ) ) . toContain ( 'Content-Security-Policy' ) ;
395+ } ) ;
396+
397+ it ( 'escapes single quotes in HTML output' , ( ) => {
398+ const node = makeGui ( {
399+ getGuiName : ( ) => 'it\'s' ,
400+ getGuiValue : ( ) => 'it\'s' ,
401+ getGuiId : ( ) => 'sq1' ,
402+ } ) ;
403+ dataProvider . setRoots ( [ node ] ) ;
404+
405+ const { view, getHtml } = makeMockWebviewView ( ) ;
406+ webviewProvider . resolveWebviewView ( view , { } as never , { } as never ) ;
407+
408+ const html = getHtml ( ) ;
409+ expect ( html ) . toContain ( 'it's' ) ;
410+ expect ( html ) . not . toContain ( 'it\'s' ) ;
411+ } ) ;
327412} ) ;
0 commit comments