lighthouse-reporting 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +287 -2
- package/dist/index.cjs +2 -0
- package/dist/index.js +4 -2
- package/dist/lighthouse-reporting.umd.cjs +46 -7
- package/dist/lighthouseReports.cjs +40 -2
- package/dist/lighthouseReports.d.ts +43 -1
- package/dist/lighthouseReports.js +41 -3
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -4,10 +4,295 @@
|
|
|
4
4
|
|
|
5
5
|
The reports include trend history support, allowing you to track performance improvements over time.
|
|
6
6
|
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img alt="HTML" src="./docs/html_report.png" width="45%">
|
|
9
|
+
|
|
10
|
+
<img alt="Plot" src="./docs/plot_report.png" width="45%">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
7
13
|
## Usage
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
Examples of usage with Playwright + Lighthouse (and Storybook).
|
|
16
|
+
|
|
17
|
+
### In your frontend or testing framework
|
|
18
|
+
The example of usage [playwright](https://github.com/microsoft/playwright) and [playwright-lighthouse](https://github.com/abhinaba-ghosh/playwright-lighthouse) together.
|
|
19
|
+
|
|
20
|
+
<details>
|
|
21
|
+
<summary>lighthouse_pages.spec</summary>
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import path from 'path'
|
|
25
|
+
import { playAudit } from 'playwright-lighthouse'
|
|
26
|
+
import { playwrightLighthouseTest, getScores, writeCsvResult, writeHtmlListEntryWithRetry, LighthouseResult } from 'lighthouse-reporting'
|
|
27
|
+
import { MyPage1 } from '../../pages/my-page-1.page.js'
|
|
28
|
+
import { MyPage2 } from '../../pages/my-page-1.page.js'
|
|
29
|
+
|
|
30
|
+
playwrightLighthouseTest.setTimeout(60000)
|
|
31
|
+
const reportDir = path.join(process.cwd(), 'lighthouse')
|
|
32
|
+
const htmlFilePath = path.join(reportDir, 'index.html')
|
|
33
|
+
|
|
34
|
+
const swimlanes = ['performance']
|
|
35
|
+
const lighthousePages = [
|
|
36
|
+
{ name: 'MyPage1', po: MyPage1, thresholds: { performance: 80, accessibility: 88, seo: 92 }, swimlanes },
|
|
37
|
+
{ name: 'MyPage2', po: MyPage2, thresholds: { performance: 90, accessibility: 100, seo: 90 }, swimlanes },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
lighthousePages.forEach(({ name, po, thresholds, swimlanes }) => {
|
|
41
|
+
playwrightLighthouseTest(name, async ({ port, baseURL }) => {
|
|
42
|
+
const onlyCategories = ['accessibility', 'seo', 'performance']
|
|
43
|
+
|
|
44
|
+
const result: LighthouseResult = await playAudit({
|
|
45
|
+
url: baseURL + po.getPath('123'),
|
|
46
|
+
port,
|
|
47
|
+
thresholds,
|
|
48
|
+
reports: {
|
|
49
|
+
formats: {
|
|
50
|
+
html: true,
|
|
51
|
+
},
|
|
52
|
+
name,
|
|
53
|
+
directory: reportDir,
|
|
54
|
+
},
|
|
55
|
+
opts: {
|
|
56
|
+
onlyCategories,
|
|
57
|
+
screenEmulation: { disabled: true },
|
|
58
|
+
},
|
|
59
|
+
disableLogs: true,
|
|
60
|
+
ignoreError: true,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const scores = getScores(result)
|
|
64
|
+
await writeCsvResult(reportDir, name, scores, thresholds, swimlanes)
|
|
65
|
+
await writeHtmlListEntryWithRetry(htmlFilePath, name, scores, thresholds, result.comparisonError)
|
|
66
|
+
|
|
67
|
+
if (result.comparisonError) {
|
|
68
|
+
throw new Error(result.comparisonError)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
</details>
|
|
75
|
+
|
|
76
|
+
### Or in Storybook
|
|
77
|
+
|
|
78
|
+
An example for storybook with lighthouse and screenshot testing
|
|
79
|
+
|
|
80
|
+
<details>
|
|
81
|
+
<summary>storybook.spec.ts</summary>
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import path from 'path'
|
|
85
|
+
import { BrowserContext } from '@playwright/test'
|
|
86
|
+
import { playAudit } from 'playwright-lighthouse'
|
|
87
|
+
import {
|
|
88
|
+
playwrightLighthouseTest,
|
|
89
|
+
getScores,
|
|
90
|
+
writeCsvResult,
|
|
91
|
+
writeHtmlListEntryWithRetry,
|
|
92
|
+
LighthouseResult,
|
|
93
|
+
StorybookIndexStory,
|
|
94
|
+
storybookPlaywright,
|
|
95
|
+
writeScoresToJson
|
|
96
|
+
} from 'lighthouse-reporting'
|
|
97
|
+
|
|
98
|
+
playwrightLighthouseTest.setTimeout(60000)
|
|
99
|
+
const lhScoresDir = path.join(process.cwd(), process.env.LH_SCORES_DIR || 'lh-scores')
|
|
100
|
+
const reportDir = path.join(process.cwd(), process.env.LH_REPORT_DIR || 'lighthouse')
|
|
101
|
+
const htmlFilePath = path.join(reportDir, 'index.html')
|
|
102
|
+
|
|
103
|
+
const stories = storybookPlaywright.getStories('./storybook-static/index.json', (story) => {
|
|
104
|
+
// skip docs, etc
|
|
105
|
+
if (story.type !== 'story') {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
// only include stories with test tag
|
|
109
|
+
if (!story.tags.includes('test')) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
return true
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
stories.forEach((story) => {
|
|
116
|
+
playwrightLighthouseTest(`${story.title} - ${story.name}`, async ({ context, port, baseURL }) => {
|
|
117
|
+
await runLighthouse(story, context, port, baseURL)
|
|
118
|
+
await storybookPlaywright.captureScreenshot(story, context)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const runLighthouse = async (story: StorybookIndexStory, context: BrowserContext, port: number, baseURL?: string) => {
|
|
123
|
+
const onlyCategories = ['accessibility']
|
|
124
|
+
const thresholds = { accessibility: 100 }
|
|
125
|
+
const name = story.id
|
|
126
|
+
|
|
127
|
+
const page = context.pages()[0]
|
|
128
|
+
await page.goto(`/iframe.html?id=${story.id}`)
|
|
129
|
+
|
|
130
|
+
const result: LighthouseResult = await playAudit({
|
|
131
|
+
url: baseURL + `/iframe.html?id=${story.id}`,
|
|
132
|
+
port,
|
|
133
|
+
thresholds,
|
|
134
|
+
reports: {
|
|
135
|
+
formats: {
|
|
136
|
+
html: true,
|
|
137
|
+
},
|
|
138
|
+
name,
|
|
139
|
+
directory: reportDir,
|
|
140
|
+
},
|
|
141
|
+
opts: {
|
|
142
|
+
onlyCategories,
|
|
143
|
+
screenEmulation: { disabled: true },
|
|
144
|
+
},
|
|
145
|
+
disableLogs: true,
|
|
146
|
+
ignoreError: true,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const scores = getScores(result)
|
|
150
|
+
await writeCsvResult(reportDir, name, scores, thresholds)
|
|
151
|
+
await writeHtmlListEntryWithRetry(htmlFilePath, name, scores, thresholds, result.comparisonError)
|
|
152
|
+
// write score results in JSON, allows generating the Average csv report
|
|
153
|
+
await writeScoresToJson(lhScoresDir, name, scores, result)
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
</details>
|
|
158
|
+
|
|
159
|
+
### Configs examples
|
|
160
|
+
|
|
161
|
+
<details>
|
|
162
|
+
<summary>playwright.storybook.config.ts</summary>
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { PlaywrightTestConfig } from '@playwright/test'
|
|
166
|
+
|
|
167
|
+
const baseURL = 'http://127.0.0.1:6009'
|
|
168
|
+
// process.env.LH_REPORT_DIR = 'lighthouse-storybook' // adjust lighthouse output folder if required
|
|
169
|
+
// process.env.LH_SCORES_DIR = 'lh-scores' // to write and store scores in json format or write average report
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
const config: PlaywrightTestConfig = {
|
|
173
|
+
use: {
|
|
174
|
+
viewport: { width: 1280, height: 820 },
|
|
175
|
+
ignoreHTTPSErrors: true,
|
|
176
|
+
acceptDownloads: false,
|
|
177
|
+
trace: 'off',
|
|
178
|
+
baseURL,
|
|
179
|
+
screenshot: { mode: 'off' },
|
|
180
|
+
},
|
|
181
|
+
projects: [
|
|
182
|
+
{
|
|
183
|
+
name: 'chromium',
|
|
184
|
+
use: {
|
|
185
|
+
browserName: 'chromium',
|
|
186
|
+
launchOptions: { args: ['--disable-gpu'] },
|
|
187
|
+
},
|
|
188
|
+
retries: 0,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
expect: { toMatchSnapshot: { threshold: 0.2 } },
|
|
192
|
+
reporter: 'line',
|
|
193
|
+
testDir: 'test/storybook',
|
|
194
|
+
testMatch: '*.spec.ts',
|
|
195
|
+
fullyParallel: true,
|
|
196
|
+
globalSetup: './src/global-setup.ts',
|
|
197
|
+
globalTeardown: './src/global-teardown.ts',
|
|
198
|
+
forbidOnly: true,
|
|
199
|
+
webServer: [
|
|
200
|
+
{
|
|
201
|
+
command: 'npx http-server ./storybook-static --port 6009 --silent',
|
|
202
|
+
url: `${baseURL}/index.json`,
|
|
203
|
+
timeout: 15 * 1000,
|
|
204
|
+
reuseExistingServer: false,
|
|
205
|
+
ignoreHTTPSErrors: true,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
}
|
|
209
|
+
export default config
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
</details>
|
|
213
|
+
|
|
214
|
+
<details>
|
|
215
|
+
<summary>global-setup.ts</summary>
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { lighthouseSetup } from 'lighthouse-reporting'
|
|
219
|
+
|
|
220
|
+
async function globalSetup() {
|
|
221
|
+
await lighthouseSetup()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default globalSetup
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
</details>
|
|
228
|
+
|
|
229
|
+
<details>
|
|
230
|
+
<summary>global-teardown.ts</summary>
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { lighthousePlaywrightTeardown, buildAverageCsv } from 'lighthouse-reporting'
|
|
234
|
+
|
|
235
|
+
const lhScoresDir = path.join(process.cwd(), process.env.LH_SCORES_DIR || 'lh-scores')
|
|
236
|
+
|
|
237
|
+
async function globalTeardown() {
|
|
238
|
+
await lighthousePlaywrightTeardown()
|
|
239
|
+
await buildAverageCsv(lhScoresDir)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export default globalTeardown
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
</details>
|
|
246
|
+
|
|
247
|
+
## Jenkins
|
|
248
|
+
|
|
249
|
+
Plugins used [HTML Publisher](https://plugins.jenkins.io/htmlpublisher/) and [Plot](https://plugins.jenkins.io/plot/).
|
|
250
|
+
|
|
251
|
+
<details>
|
|
252
|
+
<summary>Jenkinsfile</summary>
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
stage('Lighthouse') {
|
|
256
|
+
steps {
|
|
257
|
+
# run playwright-lighthouse tests
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
post {
|
|
261
|
+
always {
|
|
262
|
+
# html report per build
|
|
263
|
+
publishHTML(target: [
|
|
264
|
+
reportName : 'Lighthouse',
|
|
265
|
+
reportDir : "$WORKSPACE/lighthouse",
|
|
266
|
+
reportFiles : 'index.html',
|
|
267
|
+
keepAll : true,
|
|
268
|
+
alwaysLinkToLastBuild: true,
|
|
269
|
+
allowMissing : false
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
# csv trend history report
|
|
273
|
+
script {
|
|
274
|
+
csvFiles = findFiles(glob: 'lighthouse/*.csv')
|
|
275
|
+
for (csvFile in csvFiles) {
|
|
276
|
+
filePath = "${csvFile}"
|
|
277
|
+
plot(csvFileName: filePath.substring(filePath.lastIndexOf("/") + 1),
|
|
278
|
+
csvSeries: [[displayTableFlag: false, exclusionValues: '', file: filePath, inclusionFlag: 'OFF', url: '']],
|
|
279
|
+
exclZero: true,
|
|
280
|
+
group: 'my app',
|
|
281
|
+
numBuilds: '100',
|
|
282
|
+
style: 'line',
|
|
283
|
+
title: filePath.substring(filePath.lastIndexOf("/") + 1, filePath.indexOf(".")),
|
|
284
|
+
yaxis: 'score',
|
|
285
|
+
yaxisMaximum: '101',
|
|
286
|
+
yaxisMinimum: '15') # adjust scale
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
</details>
|
|
10
295
|
|
|
11
|
-
|
|
296
|
+
## GitHub Actions
|
|
12
297
|
|
|
13
298
|
TODO
|
package/dist/index.cjs
CHANGED
|
@@ -13,8 +13,10 @@ require("get-port");
|
|
|
13
13
|
require("@playwright/test");
|
|
14
14
|
exports.lighthousePlaywrightTeardown = hooks.lighthousePlaywrightTeardown;
|
|
15
15
|
exports.lighthouseSetup = hooks.lighthouseSetup;
|
|
16
|
+
exports.buildAverageCsv = lighthouseReports.buildAverageCsv;
|
|
16
17
|
exports.getScores = lighthouseReports.getScores;
|
|
17
18
|
exports.writeCsvResult = lighthouseReports.writeCsvResult;
|
|
18
19
|
exports.writeHtmlListEntryWithRetry = lighthouseReports.writeHtmlListEntryWithRetry;
|
|
20
|
+
exports.writeScoresToJson = lighthouseReports.writeScoresToJson;
|
|
19
21
|
exports.playwrightLighthouseTest = playwrightLighthouseTest.playwrightLighthouseTest;
|
|
20
22
|
exports.storybookPlaywright = storybookPlaywright.storybookPlaywright;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { lighthousePlaywrightTeardown, lighthouseSetup } from "./hooks.js";
|
|
2
|
-
import { getScores, writeCsvResult, writeHtmlListEntryWithRetry } from "./lighthouseReports.js";
|
|
2
|
+
import { buildAverageCsv, getScores, writeCsvResult, writeHtmlListEntryWithRetry, writeScoresToJson } from "./lighthouseReports.js";
|
|
3
3
|
import { playwrightLighthouseTest } from "./playwrightLighthouseTest.js";
|
|
4
4
|
import { storybookPlaywright } from "./storybookPlaywright.js";
|
|
5
5
|
import "os";
|
|
@@ -10,11 +10,13 @@ import "./constants-226e9774.js";
|
|
|
10
10
|
import "get-port";
|
|
11
11
|
import "@playwright/test";
|
|
12
12
|
export {
|
|
13
|
+
buildAverageCsv,
|
|
13
14
|
getScores,
|
|
14
15
|
lighthousePlaywrightTeardown,
|
|
15
16
|
lighthouseSetup,
|
|
16
17
|
playwrightLighthouseTest,
|
|
17
18
|
storybookPlaywright,
|
|
18
19
|
writeCsvResult,
|
|
19
|
-
writeHtmlListEntryWithRetry
|
|
20
|
+
writeHtmlListEntryWithRetry,
|
|
21
|
+
writeScoresToJson
|
|
20
22
|
};
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
async function lighthousePlaywrightTeardown() {
|
|
22
22
|
await fse.remove(path.join(os.tmpdir(), PW_TMP_DIR));
|
|
23
23
|
}
|
|
24
|
-
const writeCsvResult = async (reportDir2, name, scores, thresholds, swimlanes = []) => {
|
|
24
|
+
const writeCsvResult = async (reportDir2, name, scores, thresholds = {}, swimlanes = []) => {
|
|
25
25
|
let csvData = Object.keys(scores).join(",");
|
|
26
26
|
if (swimlanes.length > 0) {
|
|
27
27
|
csvData += "," + swimlanes.map((swimlane) => `${swimlane}_threshold`);
|
|
28
28
|
}
|
|
29
29
|
csvData += "\n";
|
|
30
30
|
csvData += Object.values(scores).join(",");
|
|
31
|
-
if (swimlanes.length > 0) {
|
|
31
|
+
if (swimlanes.length > 0 && Object.keys(thresholds).length > 0) {
|
|
32
32
|
csvData += "," + swimlanes.map((swimlane) => thresholds[swimlane]);
|
|
33
33
|
}
|
|
34
34
|
await fse.writeFile(path.join(reportDir2, `${name}.csv`), csvData);
|
|
@@ -61,6 +61,42 @@
|
|
|
61
61
|
prev[key] = Math.floor(c.score * 100);
|
|
62
62
|
return prev;
|
|
63
63
|
}, {});
|
|
64
|
+
const writeScoresToJson = async (lhScoresDir, name, scores, result) => {
|
|
65
|
+
const json = Object.entries(scores).reduce((prev, [k, score]) => {
|
|
66
|
+
prev[k] = { score };
|
|
67
|
+
return prev;
|
|
68
|
+
}, {});
|
|
69
|
+
const accessibilityViolations = result.artifacts.Accessibility.violations.map((v) => {
|
|
70
|
+
return {
|
|
71
|
+
title: result.lhr.audits[v.id].title,
|
|
72
|
+
nodes: v.nodes.length
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
if (accessibilityViolations.length > 0) {
|
|
76
|
+
json.accessibility.issues = accessibilityViolations;
|
|
77
|
+
}
|
|
78
|
+
await fse.writeFile(path.join(lhScoresDir, `${name}.json`), JSON.stringify(json, null, 2));
|
|
79
|
+
};
|
|
80
|
+
const buildAverageCsv = async (lhScoresDir) => {
|
|
81
|
+
const files = await fse.readdir(lhScoresDir);
|
|
82
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
83
|
+
const scores = {};
|
|
84
|
+
for (const fileName of jsonFiles) {
|
|
85
|
+
const score = await fse.readJson(
|
|
86
|
+
path.join(lhScoresDir, fileName)
|
|
87
|
+
);
|
|
88
|
+
Object.entries(score).forEach(([k, v]) => {
|
|
89
|
+
if (!scores[k]) {
|
|
90
|
+
scores[k] = 0;
|
|
91
|
+
}
|
|
92
|
+
scores[k] += v.score;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
Object.entries(scores).forEach(([k, v]) => {
|
|
96
|
+
scores[k] = v / jsonFiles.length;
|
|
97
|
+
});
|
|
98
|
+
await writeCsvResult(lhScoresDir, "_AVERAGE_", scores);
|
|
99
|
+
};
|
|
64
100
|
class Locked extends Error {
|
|
65
101
|
constructor(port) {
|
|
66
102
|
super(`${port} is locked`);
|
|
@@ -71,7 +107,7 @@
|
|
|
71
107
|
young: /* @__PURE__ */ new Set()
|
|
72
108
|
};
|
|
73
109
|
const releaseOldLockedPortsIntervalMs = 1e3 * 15;
|
|
74
|
-
let
|
|
110
|
+
let timeout;
|
|
75
111
|
const getLocalHosts = () => {
|
|
76
112
|
const interfaces = os.networkInterfaces();
|
|
77
113
|
const results = /* @__PURE__ */ new Set([void 0, "0.0.0.0"]);
|
|
@@ -137,13 +173,14 @@
|
|
|
137
173
|
exclude = new Set(excludeIterable);
|
|
138
174
|
}
|
|
139
175
|
}
|
|
140
|
-
if (
|
|
141
|
-
|
|
176
|
+
if (timeout === void 0) {
|
|
177
|
+
timeout = setTimeout(() => {
|
|
178
|
+
timeout = void 0;
|
|
142
179
|
lockedPorts.old = lockedPorts.young;
|
|
143
180
|
lockedPorts.young = /* @__PURE__ */ new Set();
|
|
144
181
|
}, releaseOldLockedPortsIntervalMs);
|
|
145
|
-
if (
|
|
146
|
-
|
|
182
|
+
if (timeout.unref) {
|
|
183
|
+
timeout.unref();
|
|
147
184
|
}
|
|
148
185
|
}
|
|
149
186
|
const hosts = getLocalHosts();
|
|
@@ -214,6 +251,7 @@
|
|
|
214
251
|
await test.expect(page).toHaveScreenshot(`${story.id}.png`, screenshotOptions);
|
|
215
252
|
}
|
|
216
253
|
};
|
|
254
|
+
exports2.buildAverageCsv = buildAverageCsv;
|
|
217
255
|
exports2.getScores = getScores;
|
|
218
256
|
exports2.lighthousePlaywrightTeardown = lighthousePlaywrightTeardown;
|
|
219
257
|
exports2.lighthouseSetup = lighthouseSetup;
|
|
@@ -221,5 +259,6 @@
|
|
|
221
259
|
exports2.storybookPlaywright = storybookPlaywright;
|
|
222
260
|
exports2.writeCsvResult = writeCsvResult;
|
|
223
261
|
exports2.writeHtmlListEntryWithRetry = writeHtmlListEntryWithRetry;
|
|
262
|
+
exports2.writeScoresToJson = writeScoresToJson;
|
|
224
263
|
Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
|
|
225
264
|
});
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const path = require("path");
|
|
4
4
|
const fse = require("fs-extra");
|
|
5
|
-
const writeCsvResult = async (reportDir, name, scores, thresholds, swimlanes = []) => {
|
|
5
|
+
const writeCsvResult = async (reportDir, name, scores, thresholds = {}, swimlanes = []) => {
|
|
6
6
|
let csvData = Object.keys(scores).join(",");
|
|
7
7
|
if (swimlanes.length > 0) {
|
|
8
8
|
csvData += "," + swimlanes.map((swimlane) => `${swimlane}_threshold`);
|
|
9
9
|
}
|
|
10
10
|
csvData += "\n";
|
|
11
11
|
csvData += Object.values(scores).join(",");
|
|
12
|
-
if (swimlanes.length > 0) {
|
|
12
|
+
if (swimlanes.length > 0 && Object.keys(thresholds).length > 0) {
|
|
13
13
|
csvData += "," + swimlanes.map((swimlane) => thresholds[swimlane]);
|
|
14
14
|
}
|
|
15
15
|
await fse.writeFile(path.join(reportDir, `${name}.csv`), csvData);
|
|
@@ -42,6 +42,44 @@ const getScores = (result) => Object.entries(result.lhr.categories).reduce((prev
|
|
|
42
42
|
prev[key] = Math.floor(c.score * 100);
|
|
43
43
|
return prev;
|
|
44
44
|
}, {});
|
|
45
|
+
const writeScoresToJson = async (lhScoresDir, name, scores, result) => {
|
|
46
|
+
const json = Object.entries(scores).reduce((prev, [k, score]) => {
|
|
47
|
+
prev[k] = { score };
|
|
48
|
+
return prev;
|
|
49
|
+
}, {});
|
|
50
|
+
const accessibilityViolations = result.artifacts.Accessibility.violations.map((v) => {
|
|
51
|
+
return {
|
|
52
|
+
title: result.lhr.audits[v.id].title,
|
|
53
|
+
nodes: v.nodes.length
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
if (accessibilityViolations.length > 0) {
|
|
57
|
+
json.accessibility.issues = accessibilityViolations;
|
|
58
|
+
}
|
|
59
|
+
await fse.writeFile(path.join(lhScoresDir, `${name}.json`), JSON.stringify(json, null, 2));
|
|
60
|
+
};
|
|
61
|
+
const buildAverageCsv = async (lhScoresDir) => {
|
|
62
|
+
const files = await fse.readdir(lhScoresDir);
|
|
63
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
64
|
+
const scores = {};
|
|
65
|
+
for (const fileName of jsonFiles) {
|
|
66
|
+
const score = await fse.readJson(
|
|
67
|
+
path.join(lhScoresDir, fileName)
|
|
68
|
+
);
|
|
69
|
+
Object.entries(score).forEach(([k, v]) => {
|
|
70
|
+
if (!scores[k]) {
|
|
71
|
+
scores[k] = 0;
|
|
72
|
+
}
|
|
73
|
+
scores[k] += v.score;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
Object.entries(scores).forEach(([k, v]) => {
|
|
77
|
+
scores[k] = v / jsonFiles.length;
|
|
78
|
+
});
|
|
79
|
+
await writeCsvResult(lhScoresDir, "_AVERAGE_", scores);
|
|
80
|
+
};
|
|
81
|
+
exports.buildAverageCsv = buildAverageCsv;
|
|
45
82
|
exports.getScores = getScores;
|
|
46
83
|
exports.writeCsvResult = writeCsvResult;
|
|
47
84
|
exports.writeHtmlListEntryWithRetry = writeHtmlListEntryWithRetry;
|
|
85
|
+
exports.writeScoresToJson = writeScoresToJson;
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
interface NodeDetails {
|
|
2
|
+
lhId: string;
|
|
3
|
+
devtoolsNodePath: string;
|
|
4
|
+
selector: string;
|
|
5
|
+
boudingRect: {
|
|
6
|
+
[k: string]: number;
|
|
7
|
+
};
|
|
8
|
+
snippet: string;
|
|
9
|
+
nodeLabel: string;
|
|
10
|
+
}
|
|
11
|
+
interface RuleExecutionError {
|
|
12
|
+
name: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
interface AxeRuleResult {
|
|
16
|
+
id: string;
|
|
17
|
+
impact?: string;
|
|
18
|
+
tags: Array<string>;
|
|
19
|
+
nodes: Array<{
|
|
20
|
+
target: Array<string>;
|
|
21
|
+
failureSummary?: string;
|
|
22
|
+
node: NodeDetails;
|
|
23
|
+
relatedNodes: NodeDetails[];
|
|
24
|
+
}>;
|
|
25
|
+
error?: RuleExecutionError;
|
|
26
|
+
}
|
|
1
27
|
export interface LighthouseResult {
|
|
2
28
|
lhr: {
|
|
3
29
|
categories: {
|
|
@@ -5,12 +31,28 @@ export interface LighthouseResult {
|
|
|
5
31
|
score: number;
|
|
6
32
|
};
|
|
7
33
|
};
|
|
34
|
+
audits: {
|
|
35
|
+
[k: string]: {
|
|
36
|
+
title: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
artifacts: {
|
|
41
|
+
Accessibility: {
|
|
42
|
+
violations: Array<AxeRuleResult>;
|
|
43
|
+
};
|
|
8
44
|
};
|
|
9
45
|
comparisonError?: string;
|
|
10
46
|
}
|
|
11
|
-
export declare const writeCsvResult: (reportDir: string, name: string, scores: Record<string, number>, thresholds
|
|
47
|
+
export declare const writeCsvResult: (reportDir: string, name: string, scores: Record<string, number>, thresholds?: Record<string, number>, swimlanes?: Array<string>) => Promise<void>;
|
|
12
48
|
/**
|
|
13
49
|
* workaround conflicts when multiple threads write the same file
|
|
14
50
|
*/
|
|
15
51
|
export declare const writeHtmlListEntryWithRetry: (htmlFilePath: string, name: string, scores: Record<string, number>, thresholds: Record<string, number>, comparisonError?: string, addReportLink?: boolean) => Promise<void>;
|
|
16
52
|
export declare const getScores: (result: LighthouseResult) => Record<string, number>;
|
|
53
|
+
export declare const writeScoresToJson: (lhScoresDir: string, name: string, scores: Record<string, number>, result: LighthouseResult) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Generate average csv file. Make sure to use writeScoresToJson in your test!
|
|
56
|
+
*/
|
|
57
|
+
export declare const buildAverageCsv: (lhScoresDir: string) => Promise<void>;
|
|
58
|
+
export {};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fse from "fs-extra";
|
|
3
|
-
const writeCsvResult = async (reportDir, name, scores, thresholds, swimlanes = []) => {
|
|
3
|
+
const writeCsvResult = async (reportDir, name, scores, thresholds = {}, swimlanes = []) => {
|
|
4
4
|
let csvData = Object.keys(scores).join(",");
|
|
5
5
|
if (swimlanes.length > 0) {
|
|
6
6
|
csvData += "," + swimlanes.map((swimlane) => `${swimlane}_threshold`);
|
|
7
7
|
}
|
|
8
8
|
csvData += "\n";
|
|
9
9
|
csvData += Object.values(scores).join(",");
|
|
10
|
-
if (swimlanes.length > 0) {
|
|
10
|
+
if (swimlanes.length > 0 && Object.keys(thresholds).length > 0) {
|
|
11
11
|
csvData += "," + swimlanes.map((swimlane) => thresholds[swimlane]);
|
|
12
12
|
}
|
|
13
13
|
await fse.writeFile(path.join(reportDir, `${name}.csv`), csvData);
|
|
@@ -40,8 +40,46 @@ const getScores = (result) => Object.entries(result.lhr.categories).reduce((prev
|
|
|
40
40
|
prev[key] = Math.floor(c.score * 100);
|
|
41
41
|
return prev;
|
|
42
42
|
}, {});
|
|
43
|
+
const writeScoresToJson = async (lhScoresDir, name, scores, result) => {
|
|
44
|
+
const json = Object.entries(scores).reduce((prev, [k, score]) => {
|
|
45
|
+
prev[k] = { score };
|
|
46
|
+
return prev;
|
|
47
|
+
}, {});
|
|
48
|
+
const accessibilityViolations = result.artifacts.Accessibility.violations.map((v) => {
|
|
49
|
+
return {
|
|
50
|
+
title: result.lhr.audits[v.id].title,
|
|
51
|
+
nodes: v.nodes.length
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
if (accessibilityViolations.length > 0) {
|
|
55
|
+
json.accessibility.issues = accessibilityViolations;
|
|
56
|
+
}
|
|
57
|
+
await fse.writeFile(path.join(lhScoresDir, `${name}.json`), JSON.stringify(json, null, 2));
|
|
58
|
+
};
|
|
59
|
+
const buildAverageCsv = async (lhScoresDir) => {
|
|
60
|
+
const files = await fse.readdir(lhScoresDir);
|
|
61
|
+
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
62
|
+
const scores = {};
|
|
63
|
+
for (const fileName of jsonFiles) {
|
|
64
|
+
const score = await fse.readJson(
|
|
65
|
+
path.join(lhScoresDir, fileName)
|
|
66
|
+
);
|
|
67
|
+
Object.entries(score).forEach(([k, v]) => {
|
|
68
|
+
if (!scores[k]) {
|
|
69
|
+
scores[k] = 0;
|
|
70
|
+
}
|
|
71
|
+
scores[k] += v.score;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
Object.entries(scores).forEach(([k, v]) => {
|
|
75
|
+
scores[k] = v / jsonFiles.length;
|
|
76
|
+
});
|
|
77
|
+
await writeCsvResult(lhScoresDir, "_AVERAGE_", scores);
|
|
78
|
+
};
|
|
43
79
|
export {
|
|
80
|
+
buildAverageCsv,
|
|
44
81
|
getScores,
|
|
45
82
|
writeCsvResult,
|
|
46
|
-
writeHtmlListEntryWithRetry
|
|
83
|
+
writeHtmlListEntryWithRetry,
|
|
84
|
+
writeScoresToJson
|
|
47
85
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lighthouse-reporting",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"build": "vite build && vite build -c vite.umd.config.ts && tsc -p ./tsconfig.build.json",
|
|
@@ -24,25 +24,25 @@
|
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
26
|
"@playwright/test": "^1",
|
|
27
|
-
"get-port": "
|
|
27
|
+
"get-port": ">=7"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@playwright/test": "^1.34.3",
|
|
31
31
|
"@types/fs-extra": "^11.0.1",
|
|
32
|
-
"@types/node": "^20.
|
|
33
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
34
|
-
"@typescript-eslint/parser": "^5.
|
|
35
|
-
"eslint": "^8.
|
|
32
|
+
"@types/node": "^20.3.1",
|
|
33
|
+
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
|
34
|
+
"@typescript-eslint/parser": "^5.60.0",
|
|
35
|
+
"eslint": "^8.43.0",
|
|
36
36
|
"eslint-config-prettier": "^8.8.0",
|
|
37
37
|
"eslint-plugin-prettier": "^4.2.1",
|
|
38
|
-
"get-port": "^
|
|
38
|
+
"get-port": "^7.0.0",
|
|
39
39
|
"husky": "^8.0.3",
|
|
40
40
|
"npm-run-all": "^4.1.5",
|
|
41
41
|
"prettier": "^2.8.8",
|
|
42
|
-
"semantic-release": "^21.0.
|
|
43
|
-
"typescript": "^5.
|
|
44
|
-
"vite": "^4.3.
|
|
45
|
-
"vite-plugin-static-copy": "^0.
|
|
42
|
+
"semantic-release": "^21.0.5",
|
|
43
|
+
"typescript": "^5.1.3",
|
|
44
|
+
"vite": "^4.3.9",
|
|
45
|
+
"vite-plugin-static-copy": "^0.16.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"fs-extra": "^11.1.1"
|