@@ -261,6 +261,7 @@ function HaloPreinstalledDropdown({
261261 setup = { divProps [ 'data-setup' ] || '' }
262262 code = { decodeURIComponent ( divProps [ 'data-code' ] || '' ) }
263263 testResult = { testInfo ?. result }
264+ playbookId = { playbookId }
264265 />
265266 ) ;
266267 }
@@ -641,17 +642,24 @@ function transformSetupBlocks(content: string): string {
641642/**
642643 * Test coverage badge block — renders a badge header on top of a code block.
643644 * Only shown when running in dev:coverage mode.
645+ * When a test fails, shows a "View Logs" button to inspect stdout/stderr.
644646 */
645647function TestCoverageBlock ( {
646- testId, timeout, isHidden, setup, code, testResult,
648+ testId, timeout, isHidden, setup, code, testResult, playbookId ,
647649} : {
648650 testId : string ;
649651 timeout : string ;
650652 isHidden : boolean ;
651653 setup : string ;
652654 code : string ;
653655 testResult ?: TestResultInfo ;
656+ playbookId ?: string ;
654657} ) {
658+ const [ logsOpen , setLogsOpen ] = useState ( false ) ;
659+ const [ logs , setLogs ] = useState < { stdout : string ; stderr : string } | null > ( null ) ;
660+ const [ logsLoading , setLogsLoading ] = useState ( false ) ;
661+ const [ logsError , setLogsError ] = useState < string | null > ( null ) ;
662+
655663 // Parse the fenced code block: ```lang\ncontent\n```
656664 const langMatch = code . match ( / ` ` ` ( \w + ) ? \s * \n / ) ;
657665 const language = langMatch ?. [ 1 ] || "" ;
@@ -668,6 +676,45 @@ function TestCoverageBlock({
668676 else { resultStatus = "fail" ; resultLabel = "Failed" ; }
669677 }
670678
679+ // Show logs button for failed or skipped tests
680+ const showLogsButton = testResult && ! testResult . success ;
681+
682+ const handleViewLogs = useCallback ( async ( ) => {
683+ if ( logsOpen ) {
684+ setLogsOpen ( false ) ;
685+ return ;
686+ }
687+
688+ // If logs already fetched, just toggle open
689+ if ( logs ) {
690+ setLogsOpen ( true ) ;
691+ return ;
692+ }
693+
694+ // Fetch logs from API
695+ if ( ! playbookId ) return ;
696+ setLogsLoading ( true ) ;
697+ setLogsError ( null ) ;
698+
699+ try {
700+ const res = await fetch ( `/api/playbooks/${ playbookId } /logs/${ testId } ` ) ;
701+ if ( ! res . ok ) {
702+ const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
703+ setLogsError ( data . error || "Failed to load logs" ) ;
704+ setLogsOpen ( true ) ;
705+ return ;
706+ }
707+ const data = await res . json ( ) ;
708+ setLogs ( data ) ;
709+ setLogsOpen ( true ) ;
710+ } catch {
711+ setLogsError ( "Failed to fetch logs" ) ;
712+ setLogsOpen ( true ) ;
713+ } finally {
714+ setLogsLoading ( false ) ;
715+ }
716+ } , [ logsOpen , logs , playbookId , testId ] ) ;
717+
671718 return (
672719 < div className = { `tc-block ${ isHidden ? "tc-hidden" : "" } ${ resultStatus ? `tc-result-${ resultStatus } ` : "" } ` } >
673720 < div className = { `tc-badge-header ${ isHidden ? "tc-badge-hidden" : "" } ${ resultStatus === "fail" ? "tc-badge-fail" : "" } ${ resultStatus === "skip" ? "tc-badge-skip" : "" } ` } >
@@ -679,12 +726,71 @@ function TestCoverageBlock({
679726 { setup && (
680727 < span className = "tc-pill tc-pill-setup" > ⚙ { setup } </ span >
681728 ) }
729+ { showLogsButton && (
730+ < button
731+ className = { `tc-logs-btn ${ logsOpen ? "tc-logs-btn-active" : "" } ` }
732+ onClick = { handleViewLogs }
733+ disabled = { logsLoading }
734+ title = { logsOpen ? "Hide logs" : "View test logs" }
735+ >
736+ { logsLoading ? (
737+ < span className = "tc-logs-spinner" />
738+ ) : (
739+ < svg width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" >
740+ < path d = "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
741+ < polyline points = "14 2 14 8 20 8" />
742+ < line x1 = "16" y1 = "13" x2 = "8" y2 = "13" />
743+ < line x1 = "16" y1 = "17" x2 = "8" y2 = "17" />
744+ </ svg >
745+ ) }
746+ { logsOpen ? "Hide Logs" : "View Logs" }
747+ </ button >
748+ ) }
682749 { testResult && (
683750 < span className = { `tc-pill tc-pill-result tc-pill-result-${ resultStatus } ` } >
684751 { resultLabel } { testResult . duration ? ` (${ testResult . duration . toFixed ( 1 ) } s)` : "" }
685752 </ span >
686753 ) }
687754 </ div >
755+ { /* Collapsible log viewer */ }
756+ { logsOpen && (
757+ < div className = "tc-logs-panel" >
758+ { logsError && ! logs ? (
759+ < div className = "tc-logs-error" >
760+ < svg width = "14" height = "14" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" strokeWidth = "2" >
761+ < circle cx = "12" cy = "12" r = "10" />
762+ < line x1 = "12" y1 = "8" x2 = "12" y2 = "12" />
763+ < line x1 = "12" y1 = "16" x2 = "12.01" y2 = "16" />
764+ </ svg >
765+ { logsError }
766+ </ div >
767+ ) : (
768+ < >
769+ { testResult ?. error && (
770+ < div className = "tc-logs-error-message" >
771+ < span className = "tc-logs-section-label" > Error</ span >
772+ < pre className = "tc-logs-pre" > { testResult . error } </ pre >
773+ </ div >
774+ ) }
775+ { logs ?. stderr && (
776+ < div className = "tc-logs-section tc-logs-stderr" >
777+ < span className = "tc-logs-section-label" > stderr</ span >
778+ < pre className = "tc-logs-pre" > { logs . stderr } </ pre >
779+ </ div >
780+ ) }
781+ { logs ?. stdout && (
782+ < div className = "tc-logs-section tc-logs-stdout" >
783+ < span className = "tc-logs-section-label" > stdout</ span >
784+ < pre className = "tc-logs-pre" > { logs . stdout } </ pre >
785+ </ div >
786+ ) }
787+ { logs && ! logs . stdout && ! logs . stderr && ! testResult ?. error && (
788+ < div className = "tc-logs-empty" > No log output available for this test.</ div >
789+ ) }
790+ </ >
791+ ) }
792+ </ div >
793+ ) }
688794 { hasHideLines ? (
689795 < div className = "code-block-wrapper" >
690796 < pre className = "code-block tc-code-with-hide" >
@@ -1093,6 +1199,7 @@ export default function PlaybookPage({ params }: { params: Promise<{ id: string
10931199 setup = { props [ 'data-setup' ] || '' }
10941200 code = { decodeURIComponent ( props [ 'data-code' ] || '' ) }
10951201 testResult = { testInfo ?. result }
1202+ playbookId = { id }
10961203 />
10971204 ) ;
10981205 }
0 commit comments