11#!/usr/bin/env node
22/**
3- * generate-flowchart.js - Enhanced with test categorization
4- * Creates a visual flowchart showing:
5- * - Test organization by category
6- * - Pass/fail status with visual indicators
7- * - Performance metrics per test
8- * - Test distribution statistics
3+ * generate-flowchart.js (L→R layout, detached legend)
4+ * Reads playwright-metrics.json from root or artifacts/.
95 */
106const fs = require ( 'fs' ) ;
117const path = require ( 'path' ) ;
@@ -27,241 +23,88 @@ fs.mkdirSync(ART, { recursive: true });
2723const safe = s => ( s . replace ( / [ ^ A - Z a - z 0 - 9 _ ] / g, '_' ) . replace ( / ^ _ + | _ + $ / g, '' ) || 'id' ) . replace ( / ^ [ ^ A - Z a - z ] / , 'id_$&' ) ;
2824const esc = s => s . replace ( / \\ / g, '\\\\' ) . replace ( / " / g, '\\"' ) ;
2925
30- /* Categorize tests based on file path and name */
31- const categorizeTest = ( filePath , testName ) => {
32- const lowerPath = filePath . toLowerCase ( ) ;
33- const lowerName = testName . toLowerCase ( ) ;
34-
35- // Check for common test categories
36- if ( lowerPath . includes ( 'smoke' ) || lowerName . includes ( 'smoke' ) ) return 'smoke' ;
37- if ( lowerPath . includes ( 'e2e' ) || lowerName . includes ( 'e2e' ) ) return 'e2e' ;
38- if ( lowerPath . includes ( 'unit' ) || lowerName . includes ( 'unit' ) ) return 'unit' ;
39- if ( lowerPath . includes ( 'integration' ) || lowerName . includes ( 'integration' ) ) return 'integration' ;
40- if ( lowerPath . includes ( 'api' ) || lowerName . includes ( 'api' ) ) return 'api' ;
41- if ( lowerPath . includes ( 'performance' ) || lowerName . includes ( 'performance' ) ) return 'performance' ;
42-
43- // Default category based on file location
44- if ( lowerPath . includes ( 'component' ) ) return 'component' ;
45- return 'general' ;
46- } ;
47-
48- /* build mermaid source with enhanced visualization */
26+ /* build mermaid source (unchanged from earlier) */
4927const m = [ ] ;
5028m . push ( `%%{init:{ "theme":"base","themeVariables":{
5129 "primaryColor":"#1976D2","primaryTextColor":"#fff","primaryBorderColor":"#0D47A1",
52- "lineColor":"#5E35B1","tertiaryColor":"#E8F5E9",
53- "mainBkg":"#263238","darkMode":true} }}%%` ) ;
54- m . push ( 'flowchart TB' ) ;
30+ "lineColor":"#5E35B1","tertiaryColor":"#E8F5E9"} }}%%` ) ;
31+ m . push ( 'flowchart LR' ) ;
5532
56- // Enhanced style definitions
5733m . push ( ' classDef fileStyle fill:#E3F2FD,stroke:#1976D2,stroke-width:2px,color:#0D47A1,font-weight:bold' ) ;
5834m . push ( ' classDef suiteStyle fill:#F3E5F5,stroke:#7B1FA2,stroke-width:1px,color:#4A148C,font-weight:bold' ) ;
5935m . push ( ' classDef passStyle fill:#C8E6C9,stroke:#43A047,stroke-width:2px,color:#1B5E20' ) ;
6036m . push ( ' classDef failStyle fill:#FFCDD2,stroke:#E53935,stroke-width:2px,color:#B71C1C,font-weight:bold' ) ;
6137m . push ( ' classDef skipStyle fill:#FFF9C4,stroke:#FBC02D,stroke-width:2px,color:#F57F17' ) ;
6238m . push ( ' classDef rootStyle fill:#1976D2,stroke:#0D47A1,stroke-width:4px,color:#FFF,font-weight:bold' ) ;
63- m . push ( ' classDef categoryStyle fill:#37474F,stroke:#263238,stroke-width:2px,color:#ECEFF1,font-weight:bold' ) ;
64- m . push ( ' classDef perfWarn fill:#FF8A65,stroke:#D84315,stroke-width:2px,color:#FFF' ) ;
65- m . push ( ' classDef perfGood fill:#81C784,stroke:#388E3C,stroke-width:2px,color:#FFF' ) ;
6639
67- // Collect and categorize all tests
68- const tests = [ ] ;
69- const categories = { } ;
70- const fileTestCounts = { } ;
40+ m . push ( ' LEGEND_ANCHOR[" "]:::rootStyle' ) ;
41+ m . push ( ' subgraph legendBox["📋 Legend"]' ) ;
42+ m . push ( ' direction TB' ) ;
43+ m . push ( ' P["✅ Passed"]:::passStyle' ) ;
44+ m . push ( ' F["❌ Failed"]:::failStyle' ) ;
45+ m . push ( ' S["⏭️ Skipped"]:::skipStyle' ) ;
46+ m . push ( ' end' ) ;
47+ m . push ( ' LEGEND_ANCHOR -.-> legendBox' ) ;
48+
49+ m . push ( ' ROOT["🧪 Playwright Test Run"]:::rootStyle' ) ;
7150
51+ const tests = [ ] ;
7252METRICS . suites . forEach ( f => {
7353 const fileTitle = f . title || path . basename ( f . file ) ;
74- fileTestCounts [ fileTitle ] = { total : 0 , passed : 0 , failed : 0 , skipped : 0 } ;
75-
7654 f . suites . forEach ( s => {
7755 s . specs . forEach ( sp => {
78- const test = {
56+ tests . push ( {
7957 file :fileTitle ,
8058 suite :s . title || 'NO_SUITE' ,
8159 spec :sp . title || 'NO_SPEC' ,
8260 status :sp . tests [ 0 ] ?. results [ 0 ] ?. status ?? 'unknown' ,
83- dur :sp . tests [ 0 ] ?. results [ 0 ] ?. duration ?? 0 ,
84- error :sp . tests [ 0 ] ?. results [ 0 ] ?. error ?. message
85- } ;
86- test . category = categorizeTest ( f . file , test . spec ) ;
87-
88- tests . push ( test ) ;
89- fileTestCounts [ fileTitle ] . total ++ ;
90-
91- // Count by status
92- if ( [ 'expected' , 'passed' ] . includes ( test . status ) ) {
93- fileTestCounts [ fileTitle ] . passed ++ ;
94- } else if ( test . status === 'failed' ) {
95- fileTestCounts [ fileTitle ] . failed ++ ;
96- } else if ( test . status === 'skipped' ) {
97- fileTestCounts [ fileTitle ] . skipped ++ ;
98- }
99-
100- // Group by category
101- if ( ! categories [ test . category ] ) {
102- categories [ test . category ] = [ ] ;
103- }
104- categories [ test . category ] . push ( test ) ;
61+ dur :sp . tests [ 0 ] ?. results [ 0 ] ?. duration ?? 0
62+ } ) ;
10563 } ) ;
10664 } ) ;
10765} ) ;
108-
109- // Calculate summary statistics
11066const summary = {
11167 total :tests . length ,
11268 passed :tests . filter ( t => [ 'expected' , 'passed' ] . includes ( t . status ) ) . length ,
11369 failed :tests . filter ( t => t . status === 'failed' ) . length ,
11470 skipped :tests . filter ( t => t . status === 'skipped' ) . length ,
11571 dur :METRICS . stats ?. duration ?? 0
11672} ;
117-
118- // Performance analysis
119- const avgDuration = summary . total > 0 ? Math . round ( summary . dur / summary . total ) : 0 ;
120- const slowTests = tests . filter ( t => t . dur > avgDuration * 2 ) ;
121-
122- // Root node
123- m . push ( ` ROOT["🧪 Playwright Test Suite<br/><small>${ new Date ( ) . toLocaleDateString ( ) } </small>"]:::rootStyle` ) ;
124-
125- // Summary banner
126- m . push ( ` BANNER["📊 Total: ${ summary . total } | ✅ ${ summary . passed } | ❌ ${ summary . failed } | ⏭️ ${ summary . skipped } <br/>⏱️ ${ ( summary . dur / 1000 ) . toFixed ( 1 ) } s | Avg: ${ avgDuration } ms/test"]` ) ;
73+ m . push ( ` BANNER["📊 ${ summary . total } • ✅ ${ summary . passed } • ❌ ${ summary . failed } • ⏭️ ${ summary . skipped } • ⏱️ ${ summary . dur } s"]` ) ;
12774m . push ( ' ROOT --> BANNER' ) ;
12875
129- // Category distribution subgraph
130- m . push ( ' subgraph CATEGORIES["📂 Test Categories"]' ) ;
131- m . push ( ' direction LR' ) ;
132- Object . entries ( categories ) . forEach ( ( [ cat , catTests ] ) => {
133- const catId = safe ( `CAT_${ cat } ` ) ;
134- const catPassed = catTests . filter ( t => [ 'expected' , 'passed' ] . includes ( t . status ) ) . length ;
135- const catFailed = catTests . filter ( t => t . status === 'failed' ) . length ;
136- const catIcon = {
137- 'smoke' : '🔥' ,
138- 'e2e' : '🌐' ,
139- 'unit' : '🧩' ,
140- 'integration' : '🔗' ,
141- 'api' : '📡' ,
142- 'performance' : '⚡' ,
143- 'component' : '🧱' ,
144- 'general' : '📝'
145- } [ cat ] || '📁' ;
146-
147- m . push ( ` ${ catId } ["${ catIcon } ${ cat . toUpperCase ( ) } <br/>${ catTests . length } tests<br/>✅ ${ catPassed } ❌ ${ catFailed } "]:::categoryStyle` ) ;
148- } ) ;
149- m . push ( ' end' ) ;
150- m . push ( ' BANNER --> CATEGORIES' ) ;
151-
152- // Performance warnings if any
153- if ( slowTests . length > 0 ) {
154- m . push ( ` PERF_WARN["⚠️ ${ slowTests . length } Slow Test${ slowTests . length > 1 ? 's' : '' } <br/><small>>2x avg duration</small>"]:::perfWarn` ) ;
155- m . push ( ' BANNER --> PERF_WARN' ) ;
156- }
157-
158- // Legend
159- m . push ( ' subgraph LEGEND["📋 Legend"]' ) ;
160- m . push ( ' direction TB' ) ;
161- m . push ( ' L1["✅ Passed Test"]:::passStyle' ) ;
162- m . push ( ' L2["❌ Failed Test"]:::failStyle' ) ;
163- m . push ( ' L3["⏭️ Skipped Test"]:::skipStyle' ) ;
164- m . push ( ' L4["⚡ Performance Issue"]:::perfWarn' ) ;
165- m . push ( ' end' ) ;
166- m . push ( ' ROOT --> LEGEND' ) ;
167-
168- // Main test flow
16976const files = [ ...new Set ( tests . map ( t => t . file ) ) ] ;
170- files . forEach ( ( file , fileIndex ) => {
77+ let prev = 'BANNER' ;
78+ files . forEach ( file => {
17179 const fid = safe ( file ) ;
172- const counts = fileTestCounts [ file ] ;
173- const fileStatus = counts . failed > 0 ? '❌' : counts . skipped === counts . total ? '⏭️' : '✅' ;
174-
175- m . push ( ` ${ fid } ["📁 ${ esc ( file ) } <br/>${ fileStatus } ${ counts . passed } /${ counts . total } passed"]:::fileStyle` ) ;
176- m . push ( ` CATEGORIES --> ${ fid } ` ) ;
80+ m . push ( ` ${ fid } ["📁 ${ esc ( file ) } "]:::fileStyle` ) ;
81+ m . push ( ` ${ prev } --> ${ fid } ` ) ;
82+ prev = fid ;
17783
178- m . push ( ` subgraph ${ fid } _tests[" ${ esc ( file ) } Tests" ]` ) ;
84+ m . push ( ` subgraph ${ fid } _grp[ ]` ) ;
17985 m . push ( ' direction TB' ) ;
180-
181- // Group tests by suite within file
18286 const suites = [ ...new Set ( tests . filter ( t => t . file === file ) . map ( t => t . suite ) ) ] ;
183- suites . forEach ( ( suite , suiteIndex ) => {
87+ suites . forEach ( suite => {
18488 const sid = safe ( `${ fid } _${ suite } ` ) ;
185- const suiteTests = tests . filter ( t => t . file === file && t . suite === suite ) ;
186- const suitePassed = suiteTests . filter ( t => [ 'expected' , 'passed' ] . includes ( t . status ) ) . length ;
187- const suiteFailed = suiteTests . filter ( t => t . status === 'failed' ) . length ;
188-
189- m . push ( ` ${ sid } ["📦 ${ esc ( suite ) } <br/><small>${ suitePassed } /${ suiteTests . length } passed</small>"]:::suiteStyle` ) ;
190-
191- // Individual tests
192- suiteTests . forEach ( ( t , testIndex ) => {
193- const spid = safe ( `${ sid } _${ t . spec } _${ testIndex } ` ) ;
89+ m . push ( ` ${ sid } ["📦 ${ esc ( suite ) } "]:::suiteStyle` ) ;
90+ m . push ( ` ${ fid } --> ${ sid } ` ) ;
91+ tests . filter ( t => t . file === file && t . suite === suite ) . forEach ( t => {
92+ const spid = safe ( `${ sid } _${ t . spec } ` ) ;
19493 const cls = t . status === 'failed' ?'failStyle' :t . status === 'skipped' ?'skipStyle' :'passStyle' ;
19594 const icon = t . status === 'failed' ?'❌' :t . status === 'skipped' ?'⏭️' :'✅' ;
196- const perfIcon = t . dur > avgDuration * 2 ? ' 🐌' : t . dur < avgDuration / 2 ? ' 🚀' : '' ;
197-
198- let testLabel = `${ icon } ${ esc ( t . spec ) } ${ perfIcon } <br/><small>${ t . dur } ms` ;
199- if ( t . category !== 'general' ) {
200- testLabel += ` | ${ t . category } ` ;
201- }
202- testLabel += '</small>' ;
203-
204- // Add error message for failed tests
205- if ( t . status === 'failed' && t . error ) {
206- const shortError = t . error . split ( '\n' ) [ 0 ] . substring ( 0 , 50 ) ;
207- testLabel += `<br/><small style="color:red">${ esc ( shortError ) } ...</small>` ;
208- }
209-
210- m . push ( ` ${ sid } --> ${ spid } ["${ testLabel } "]:::${ cls } ` ) ;
211-
212- // Link slow tests to performance warning
213- if ( t . dur > avgDuration * 2 && slowTests . length > 0 ) {
214- m . push ( ` ${ spid } -.-> PERF_WARN` ) ;
215- }
95+ m . push ( ` ${ sid } --> ${ spid } ["${ icon } ${ esc ( t . spec ) } <br/><small>${ t . dur } ms</small>"]:::${ cls } ` ) ;
21696 } ) ;
21797 } ) ;
21898 m . push ( ' end' ) ;
21999} ) ;
220100
221- // Add test distribution pie chart data as comment
222- m . push ( `
223- %% Test Distribution Data
224- %% Categories: ${ Object . entries ( categories ) . map ( ( [ k , v ] ) => `${ k } :${ v . length } ` ) . join ( ', ' ) }
225- %% Status: Passed:${ summary . passed } , Failed:${ summary . failed } , Skipped:${ summary . skipped }
226- %% Performance: Slow:${ slowTests . length } , Normal:${ tests . length - slowTests . length }
227- ` ) ;
228-
229- /* write & render PNG with larger dimensions for better readability */
101+ /* write & render PNG */
230102fs . writeFileSync ( `${ ART } /flowchart.mmd` , m . join ( '\n' ) ) ;
231103fs . writeFileSync ( 'puppeteer.json' , '{ "args":["--no-sandbox","--disable-setuid-sandbox"] }' ) ;
232-
233- console . log ( '📊 Generating enhanced flowchart...' ) ;
234104execSync (
235105 'npx -y @mermaid-js/mermaid-cli@10.6.1 ' +
236106 '-p puppeteer.json -i artifacts/flowchart.mmd -o artifacts/flowchart.png ' +
237- '-w 10000 -H 3000 -b white' ,
107+ '-w 8000 -H 2600 -b white' ,
238108 { stdio :'inherit' }
239109) ;
240-
241- // Also generate a test categorization summary
242- const categorySummary = {
243- totalTests : tests . length ,
244- categories : Object . entries ( categories ) . map ( ( [ name , tests ] ) => ( {
245- name,
246- count : tests . length ,
247- passed : tests . filter ( t => [ 'expected' , 'passed' ] . includes ( t . status ) ) . length ,
248- failed : tests . filter ( t => t . status === 'failed' ) . length ,
249- skipped : tests . filter ( t => t . status === 'skipped' ) . length ,
250- avgDuration : tests . reduce ( ( sum , t ) => sum + t . dur , 0 ) / tests . length
251- } ) ) ,
252- slowTests : slowTests . map ( t => ( {
253- name : `${ t . file } > ${ t . suite } > ${ t . spec } ` ,
254- duration : t . dur ,
255- category : t . category
256- } ) ) ,
257- failedTests : tests . filter ( t => t . status === 'failed' ) . map ( t => ( {
258- name : `${ t . file } > ${ t . suite } > ${ t . spec } ` ,
259- category : t . category ,
260- error : t . error
261- } ) )
262- } ;
263-
264- fs . writeFileSync ( `${ ART } /test-categorization.json` , JSON . stringify ( categorySummary , null , 2 ) ) ;
265-
266- console . log ( '✅ Enhanced flow-chart with categorization → artifacts/flowchart.png' ) ;
267- console . log ( '📊 Test categorization summary → artifacts/test-categorization.json' ) ;
110+ console . log ( '✅ Flow-chart with detached legend → artifacts/flowchart.png' ) ;
0 commit comments