11import { AxePuppeteer } from '@axe-core/puppeteer' ;
2- import { Result } from 'axe-core' ;
2+ import { Result as AxeResult } from 'axe-core' ;
33import puppeteer from 'puppeteer' ;
4+ import lighthouse , { RunnerResult as LighthouseRunnerResult } from 'lighthouse' ;
45import { callWithTimeout } from '../../utils/timeout.js' ;
56import { AutoCsp } from './auto-csp.js' ;
67import { CspViolation } from './auto-csp-types.js' ;
7- import { ServeTestingProgressLogFn } from './worker-types.js' ;
8+ import { LighthouseAudit , LighthouseResult , ServeTestingProgressLogFn } from './worker-types.js' ;
89
910/**
1011 * Uses Puppeteer to take a screenshot of the main page, perform Axe testing,
@@ -18,13 +19,15 @@ export async function runAppInPuppeteer(
1819 includeAxeTesting : boolean ,
1920 progressLog : ServeTestingProgressLogFn ,
2021 enableAutoCsp : boolean ,
22+ includeLighthouseData : boolean ,
2123) {
2224 const runtimeErrors : string [ ] = [ ] ;
2325
2426 // Undefined by default so it gets flagged correctly as `skipped` if there's no data.
2527 let cspViolations : CspViolation [ ] | undefined ;
2628 let screenshotBase64Data : string | undefined ;
27- let axeViolations : Result [ ] | undefined ;
29+ let axeViolations : AxeResult [ ] | undefined ;
30+ let lighthouseResult : LighthouseResult | undefined ;
2831
2932 try {
3033 const browser = await puppeteer . launch ( {
@@ -139,6 +142,35 @@ export async function runAppInPuppeteer(
139142 ) ;
140143 progressLog ( 'success' , 'Screenshot captured and encoded' ) ;
141144 }
145+
146+ if ( includeLighthouseData ) {
147+ try {
148+ progressLog ( 'eval' , `Gathering Lighthouse data from ${ hostUrl } ` ) ;
149+ const lighthouseData = await lighthouse (
150+ hostUrl ,
151+ undefined ,
152+ {
153+ extends : 'lighthouse:default' ,
154+ settings : {
155+ // Exclude accessibility since it's already covered by Axe above.
156+ onlyCategories : [ 'performance' , 'best-practices' ] ,
157+ } ,
158+ } ,
159+ page ,
160+ ) ;
161+
162+ lighthouseResult = lighthouseData ? processLighthouseData ( lighthouseData ) : undefined ;
163+
164+ if ( lighthouseResult ) {
165+ progressLog ( 'success' , 'Lighthouse data has been collected' ) ;
166+ } else {
167+ progressLog ( 'error' , 'Lighthouse did not produce usable data' ) ;
168+ }
169+ } catch ( lighthouseError : any ) {
170+ progressLog ( 'error' , 'Could not gather Lighthouse data' , lighthouseError . message ) ;
171+ }
172+ }
173+
142174 await browser . close ( ) ;
143175 } catch ( screenshotError : any ) {
144176 let details : string = screenshotError . message ;
@@ -150,5 +182,52 @@ export async function runAppInPuppeteer(
150182 progressLog ( 'error' , 'Could not take screenshot' , details ) ;
151183 }
152184
153- return { screenshotBase64Data, runtimeErrors, axeViolations, cspViolations} ;
185+ return { screenshotBase64Data, runtimeErrors, axeViolations, cspViolations, lighthouseResult} ;
186+ }
187+
188+ function processLighthouseData ( data : LighthouseRunnerResult ) : LighthouseResult | undefined {
189+ const availableAudits = new Map < string , LighthouseAudit > ( ) ;
190+ const result : LighthouseResult = { categories : [ ] , uncategorized : [ ] } ;
191+
192+ for ( const audit of Object . values ( data . lhr . audits ) ) {
193+ const type = audit . details ?. type ;
194+ const displayMode = audit . scoreDisplayMode ;
195+ const isAllowedType =
196+ ! type ||
197+ type === 'list' ||
198+ type === 'opportunity' ||
199+ ( type === 'checklist' && Object . keys ( audit . details ?. items || { } ) . length > 0 ) ||
200+ ( type === 'table' && audit . details ?. items . length ) ;
201+ const isAllowedDisplayMode = displayMode === 'binary' || displayMode === 'numeric' ;
202+
203+ if ( audit . score != null && isAllowedType && isAllowedDisplayMode ) {
204+ availableAudits . set ( audit . id , audit ) ;
205+ }
206+ }
207+
208+ for ( const category of Object . values ( data . lhr . categories ) ) {
209+ const auditsForCategory : LighthouseAudit [ ] = [ ] ;
210+
211+ for ( const ref of category . auditRefs ) {
212+ const audit = availableAudits . get ( ref . id ) ;
213+
214+ if ( audit ) {
215+ auditsForCategory . push ( audit ) ;
216+ availableAudits . delete ( ref . id ) ;
217+ }
218+ }
219+
220+ result . categories . push ( {
221+ id : category . id ,
222+ displayName : category . title ,
223+ description : category . description || '' ,
224+ score : category . score || 0 ,
225+ audits : auditsForCategory ,
226+ } ) ;
227+ }
228+
229+ // Track all remaining audits as uncategorized.
230+ result . uncategorized . push ( ...availableAudits . values ( ) ) ;
231+
232+ return result . categories . length === 0 && result . uncategorized . length === 0 ? undefined : result ;
154233}
0 commit comments