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 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
+ &nbsp; &nbsp; &nbsp; &nbsp;
10
+ <img alt="Plot" src="./docs/plot_report.png" width="45%">
11
+ </p>
12
+
7
13
  ## Usage
8
14
 
9
- ### Jenkins
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
- ### GitHub Actions
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 interval;
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 (interval === void 0) {
141
- interval = setInterval(() => {
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 (interval.unref) {
146
- interval.unref();
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: Record<string, number>, swimlanes?: Array<string>) => Promise<void>;
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.0",
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": "^6"
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.2.4",
33
- "@typescript-eslint/eslint-plugin": "^5.59.7",
34
- "@typescript-eslint/parser": "^5.59.7",
35
- "eslint": "^8.41.0",
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": "^6.1.2",
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.2",
43
- "typescript": "^5.0.4",
44
- "vite": "^4.3.8",
45
- "vite-plugin-static-copy": "^0.15.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"