1
1
import { AxePuppeteer } from '@axe-core/puppeteer' ;
2
- import { Result } from 'axe-core' ;
2
+ import { Result as AxeResult } from 'axe-core' ;
3
3
import puppeteer from 'puppeteer' ;
4
+ import lighthouse , { RunnerResult as LighthouseRunnerResult } from 'lighthouse' ;
4
5
import { callWithTimeout } from '../../utils/timeout.js' ;
5
6
import { AutoCsp } from './auto-csp.js' ;
6
7
import { CspViolation } from './auto-csp-types.js' ;
7
- import { ServeTestingProgressLogFn } from './worker-types.js' ;
8
+ import { LighthouseAudit , LighthouseResult , ServeTestingProgressLogFn } from './worker-types.js' ;
8
9
9
10
/**
10
11
* Uses Puppeteer to take a screenshot of the main page, perform Axe testing,
@@ -18,13 +19,15 @@ export async function runAppInPuppeteer(
18
19
includeAxeTesting : boolean ,
19
20
progressLog : ServeTestingProgressLogFn ,
20
21
enableAutoCsp : boolean ,
22
+ includeLighthouseData : boolean ,
21
23
) {
22
24
const runtimeErrors : string [ ] = [ ] ;
23
25
24
26
// Undefined by default so it gets flagged correctly as `skipped` if there's no data.
25
27
let cspViolations : CspViolation [ ] | undefined ;
26
28
let screenshotBase64Data : string | undefined ;
27
- let axeViolations : Result [ ] | undefined ;
29
+ let axeViolations : AxeResult [ ] | undefined ;
30
+ let lighthouseResult : LighthouseResult | undefined ;
28
31
29
32
try {
30
33
const browser = await puppeteer . launch ( {
@@ -139,6 +142,35 @@ export async function runAppInPuppeteer(
139
142
) ;
140
143
progressLog ( 'success' , 'Screenshot captured and encoded' ) ;
141
144
}
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
+
142
174
await browser . close ( ) ;
143
175
} catch ( screenshotError : any ) {
144
176
let details : string = screenshotError . message ;
@@ -150,5 +182,52 @@ export async function runAppInPuppeteer(
150
182
progressLog ( 'error' , 'Could not take screenshot' , details ) ;
151
183
}
152
184
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 ;
154
233
}
0 commit comments