lighthouse-reporting 1.3.1 → 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
@@ -92,10 +92,12 @@ import {
92
92
  LighthouseResult,
93
93
  StorybookIndexStory,
94
94
  storybookPlaywright,
95
+ writeScoresToJson
95
96
  } from 'lighthouse-reporting'
96
97
 
97
98
  playwrightLighthouseTest.setTimeout(60000)
98
- const reportDir = path.join(process.cwd(), process.env.LH_REPORT_DIR as string)
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')
99
101
  const htmlFilePath = path.join(reportDir, 'index.html')
100
102
 
101
103
  const stories = storybookPlaywright.getStories('./storybook-static/index.json', (story) => {
@@ -147,6 +149,8 @@ const runLighthouse = async (story: StorybookIndexStory, context: BrowserContext
147
149
  const scores = getScores(result)
148
150
  await writeCsvResult(reportDir, name, scores, thresholds)
149
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)
150
154
  }
151
155
  ```
152
156
 
@@ -162,6 +166,8 @@ import { PlaywrightTestConfig } from '@playwright/test'
162
166
 
163
167
  const baseURL = 'http://127.0.0.1:6009'
164
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
+
165
171
 
166
172
  const config: PlaywrightTestConfig = {
167
173
  use: {
@@ -209,49 +215,13 @@ export default config
209
215
  <summary>global-setup.ts</summary>
210
216
 
211
217
  ```ts
212
- import { PlaywrightTestConfig } from '@playwright/test'
213
-
214
- const baseURL = 'http://127.0.0.1:6009'
215
- // process.env.LH_REPORT_DIR = 'lighthouse-storybook' // adjust lighthouse output folder if required
218
+ import { lighthouseSetup } from 'lighthouse-reporting'
216
219
 
217
- const config: PlaywrightTestConfig = {
218
- use: {
219
- viewport: { width: 1280, height: 820 },
220
- ignoreHTTPSErrors: true,
221
- acceptDownloads: false,
222
- trace: 'off',
223
- baseURL,
224
- screenshot: { mode: 'off' },
225
- },
226
- projects: [
227
- {
228
- name: 'chromium',
229
- use: {
230
- browserName: 'chromium',
231
- launchOptions: { args: ['--disable-gpu'] },
232
- },
233
- retries: 0,
234
- },
235
- ],
236
- expect: { toMatchSnapshot: { threshold: 0.2 } },
237
- reporter: 'line',
238
- testDir: 'test/storybook',
239
- testMatch: '*.spec.ts',
240
- fullyParallel: true,
241
- globalSetup: './src/global-setup.ts',
242
- globalTeardown: './src/global-teardown.ts',
243
- forbidOnly: true,
244
- webServer: [
245
- {
246
- command: 'npx http-server ./storybook-static --port 6009 --silent',
247
- url: `${baseURL}/index.json`,
248
- timeout: 15 * 1000,
249
- reuseExistingServer: false,
250
- ignoreHTTPSErrors: true,
251
- },
252
- ],
220
+ async function globalSetup() {
221
+ await lighthouseSetup()
253
222
  }
254
- export default config
223
+
224
+ export default globalSetup
255
225
  ```
256
226
 
257
227
  </details>
@@ -260,10 +230,13 @@ export default config
260
230
  <summary>global-teardown.ts</summary>
261
231
 
262
232
  ```ts
263
- import { lighthousePlaywrightTeardown } from 'lighthouse-reporting'
233
+ import { lighthousePlaywrightTeardown, buildAverageCsv } from 'lighthouse-reporting'
234
+
235
+ const lhScoresDir = path.join(process.cwd(), process.env.LH_SCORES_DIR || 'lh-scores')
264
236
 
265
237
  async function globalTeardown() {
266
238
  await lighthousePlaywrightTeardown()
239
+ await buildAverageCsv(lhScoresDir)
267
240
  }
268
241
 
269
242
  export default globalTeardown
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.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": "^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",
42
+ "semantic-release": "^21.0.5",
43
+ "typescript": "^5.1.3",
44
44
  "vite": "^4.3.9",
45
- "vite-plugin-static-copy": "^0.15.0"
45
+ "vite-plugin-static-copy": "^0.16.0"
46
46
  },
47
47
  "dependencies": {
48
48
  "fs-extra": "^11.1.1"