├── CHANGELOG.md
├── .husky
└── pre-push
├── PULL_REQUEST_TEMPLATE.md
├── playwright-demo
├── .gitignore
├── tsconfig.json
├── extensions
│ ├── aggregator
│ │ ├── aggregator-config.ts
│ │ ├── aggregatorExtension.ts
│ │ └── aggregator-args-builder.ts
│ ├── ecoIndex
│ │ ├── ecoIndexConfig.ts
│ │ └── ecoIndexExtension.ts
│ ├── extensionWrapper.ts
│ └── lighthouse
│ │ ├── lighthouseExtension.ts
│ │ ├── lighthouseConfig.ts
│ │ └── lighthouseReport.ts
├── README.md
├── package.json
├── Tests
│ └── welcomeAxaPage.spec.ts
└── playwright.config.ts
├── cypress-demo
├── cypress
│ ├── fixtures
│ │ └── example.json
│ ├── e2e
│ │ └── demo.cy.js
│ └── support
│ │ ├── e2e.js
│ │ └── commands.js
├── package.json
├── config.js
└── cypress.config.js
├── src
├── config.json
├── reporters
│ ├── utils
│ │ ├── computeCssClassForMetrics.js
│ │ ├── displayPageErrorIcon.js
│ │ ├── statusGreen.spec.js
│ │ ├── statusPerPage.js
│ │ ├── statusGreen.js
│ │ ├── statusPerPage.spec.js
│ │ └── __snapshots__
│ │ │ └── statusGreen.spec.js.snap
│ ├── templates
│ │ ├── templateStatusGreen.html
│ │ ├── templateDoughnut.html
│ │ ├── water.svg
│ │ ├── templatePerPage.html
│ │ ├── template.html
│ │ ├── templatePageMetrics.html
│ │ ├── templateGreenItMetrics.html
│ │ ├── sheet.svg
│ │ ├── co2.svg
│ │ └── style.css
│ ├── readTemplate.js
│ ├── globalTag.js
│ ├── pageTag.js
│ ├── translate
│ │ ├── en-GB.json
│ │ └── Fr-fr.json
│ ├── generatorReports.spec.js
│ └── generatorReports.js
├── lighthouse
│ ├── aggregatorService.spec.js
│ └── aggregatorService.js
├── main.js
├── globlalAggregation
│ ├── aggregatorService.js
│ └── aggregatorService.spec.js
├── cli.js
└── ecoIndex
│ ├── aggregatorService.js
│ └── aggregatorService.spec.js
├── .gitignore
├── ISSUE_TEMPLATE.md
├── .eslintrc.js
├── .github
└── workflows
│ ├── auto-assign.yml
│ ├── build.yml
│ └── npm-publish.yml
├── CONTRIBUTING.md
├── .vscode
└── launch.json
├── package.json
├── LICENSE.md
├── CODE_OF_CONDUCT.md
├── example
└── ecoindex
│ └── example1.json
└── readme.md
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | corrige le nan dans ecoindex
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run lint && npm run test
5 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## A picture tells a thousand words
2 |
3 | ## Before this PR
4 |
5 | ## After this PR
--------------------------------------------------------------------------------
/playwright-demo/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /test-results/
3 | /playwright-report/
4 | /blob-report/
5 | /playwright/.cache/
6 | /reports/
--------------------------------------------------------------------------------
/playwright-demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "NodeNext",
4 | "resolveJsonModule": true
5 | }
6 | }
--------------------------------------------------------------------------------
/cypress-demo/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/playwright-demo/extensions/aggregator/aggregator-config.ts:
--------------------------------------------------------------------------------
1 | export const AGGREGATOR_OPTIONS = {
2 | reports: 'html',
3 | verbose: true,
4 | srcLighthouse: './reports/lighthouse',
5 | srcEcoIndex: './reports/eco-index',
6 | outputPath: './reports/green-it',
7 | };
8 |
--------------------------------------------------------------------------------
/src/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "srcEcoIndex": "C:\\Workspace\\reports\\ecoindex",
3 | "srcLighthouse": "C:\\Workspace\\reports\\lighthouse",
4 | "reports": "html",
5 | "lang": "fr-FR",
6 | "pass": 95,
7 | "fail": 90,
8 | "verbose": false,
9 | "minify":true
10 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | report.html
7 |
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | coverage
12 | reports
13 | screenshots
14 | .DS_Store
15 | videos
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Issue and Steps to Reproduce
2 |
3 |
4 |
5 | ### Versions
6 |
7 | ### Screenshots
8 |
9 | #### Expected
10 |
11 | #### Actual
12 |
13 | ### Additional Details
14 |
15 | - Installed packages:
--------------------------------------------------------------------------------
/src/reporters/utils/computeCssClassForMetrics.js:
--------------------------------------------------------------------------------
1 | const FAILED_CSS_CLASS = "lh-audit--fail lh-audit__display-text";
2 |
3 | const computeCssClassForMetrics = (metric) => {
4 | if (metric.status === "error") {
5 | return FAILED_CSS_CLASS;
6 | }
7 | return "";
8 | };
9 |
10 | module.exports = computeCssClassForMetrics;
11 |
--------------------------------------------------------------------------------
/src/reporters/templates/templateStatusGreen.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%- mySvg.firtSheet %>
4 |
5 |
6 | <%- mySvg.secondSheet %>
7 |
8 |
9 | <%- mySvg.ThreeSheet %>
10 |
11 |
--------------------------------------------------------------------------------
/playwright-demo/extensions/ecoIndex/ecoIndexConfig.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | export const ECOINDEX_OPTIONS = {
3 | ecoIndex: 50,
4 | grade: 'B',
5 | visits: 2000,
6 | checkThresholds: false,
7 | beforeScript: (globals) => {
8 | },
9 | afterScript: (globals) => {},
10 | output: ['json'],
11 | outputPathDir: './reports/eco-index',
12 | };
--------------------------------------------------------------------------------
/playwright-demo/extensions/aggregator/aggregatorExtension.ts:
--------------------------------------------------------------------------------
1 | import aggregateGreenItReports from '@cnumr/lighthouse-eco-index-aggregator/src/main.js';
2 | import { buildAggregatorArgs } from "./aggregator-args-builder.js";
3 |
4 | export class AggregatorExtension {
5 | async generateFinalReports() {
6 | await aggregateGreenItReports(buildAggregatorArgs());
7 | }
8 | }
--------------------------------------------------------------------------------
/src/reporters/readTemplate.js:
--------------------------------------------------------------------------------
1 | const folderTemplate = "templates";
2 | const fs = require("fs");
3 | const path = require("path");
4 | const readTemplate = (templateFile) => {
5 | const templatePath = path.join(__dirname, folderTemplate, templateFile);
6 | return fs.readFileSync(templatePath, "utf-8").toString();
7 | };
8 | module.exports = {
9 | readTemplate,
10 | };
11 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es2021": true
6 | },
7 | "overrides": [
8 | ],
9 | "parserOptions": {
10 | "ecmaVersion": "latest"
11 | },
12 | "rules": {
13 | "semi": ["error", "always"],
14 | "quotes": ["error", "double"],
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/.github/workflows/auto-assign.yml:
--------------------------------------------------------------------------------
1 | name: Issue assignment
2 |
3 | on:
4 | issues:
5 | types: [opened]
6 |
7 | jobs:
8 | auto-assign:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: 'Auto-assign issue'
12 | uses: pozil/auto-assign-issue@v1
13 | with:
14 | repo-token: ${{ secrets.GITHUB_TOKEN }}
15 | assignees: EmmanuelDemey
16 |
--------------------------------------------------------------------------------
/playwright-demo/README.md:
--------------------------------------------------------------------------------
1 | To use this demo :
2 | - npm install
3 | - npm run test or with vscode run the test explorer (you can add the Playwright Test for VSCode to help you write your own test)
4 | - the test in the Tests folder welcomeAxaPage.spec.ts will be run wait a couple of minutes.
5 | - In the [reports folder](reports) you will see [the report file aggregate](reports/green-it/report.html) with all the lighthouse and ecoindex analyzes.
--------------------------------------------------------------------------------
/cypress-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cypress-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "cypress": "^12.3.0",
14 | "@cypress-audit/lighthouse": "^1.4.2",
15 | "eco-index-audit": "^4.2.0"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/cypress-demo/cypress/e2e/demo.cy.js:
--------------------------------------------------------------------------------
1 | const url = "https://www.google.com/";
2 | describe("template spec", () => {
3 | it("ecoindex", () => {
4 | cy.visit(url);
5 |
6 | cy.task("checkEcoIndex", {
7 | url
8 | }).its("ecoIndex", {timeout: 0}).should("be.greaterThan", 70);
9 | });
10 |
11 | it("lighthouse", () => {
12 | cy.visit(url);
13 | cy.lighthouse(undefined, { output: "html"});
14 | });
15 | });
--------------------------------------------------------------------------------
/playwright-demo/extensions/aggregator/aggregator-args-builder.ts:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import { AGGREGATOR_OPTIONS } from './aggregator-config.js';
3 |
4 | const { reports, verbose, srcLighthouse, srcEcoIndex, outputPath } = AGGREGATOR_OPTIONS;
5 |
6 | export const buildAggregatorArgs = () => ({
7 | reports,
8 | verbose,
9 | srcLighthouse,
10 | srcEcoIndex,
11 | outputPath,
12 | });
13 |
14 | export const shouldRunAggregator = () => fs.existsSync(srcLighthouse) && fs.existsSync(srcEcoIndex);
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - master
8 | pull_request:
9 | types: [opened, synchronize, reopened]
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | with:
16 | fetch-depth: 0
17 | - name: Lint
18 | run: |
19 | npm ci
20 | npm run lint
21 | - name: Test
22 | run: |
23 | npm run test
24 |
--------------------------------------------------------------------------------
/cypress-demo/config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const fs = require("fs");
3 |
4 | const lighthouseOutputPathDir = path.join(__dirname, "reports/lighthouse");
5 | const ecoIndexOutputPathDir = path.join(__dirname, "reports/ecoindex");
6 |
7 | module.exports = {
8 | reports: [
9 | "html",
10 | (_options, result) => {
11 | console.log(result);
12 | },
13 | ],
14 | verbose: true,
15 | srcLighthouse: lighthouseOutputPathDir,
16 | srcEcoIndex: ecoIndexOutputPathDir,
17 | outputPath: "report_final",
18 | };
19 |
--------------------------------------------------------------------------------
/playwright-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lighthouse-eco-index-aggregatore-playwright-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "playwright test"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "@cnumr/eco-index-audit": "4.7.2",
14 | "@cnumr/lighthouse-eco-index-aggregator": "1.1.0",
15 | "@playwright/test": "^1.40.0",
16 | "@types/node": "^20.9.3",
17 | "lighthouse": "^11.3.0"
18 | },
19 | "type": "module"
20 | }
--------------------------------------------------------------------------------
/src/reporters/utils/displayPageErrorIcon.js:
--------------------------------------------------------------------------------
1 | const HTML_ICON_FAIL = "";
2 | const HTML_ICON_PASS = "";
3 |
4 | const pageInErrorOrWarning = (page,pass) => {
5 |
6 | if (
7 | page.ecoIndex < pass ||
8 | page.performance < pass ||
9 | page.accessibility < pass ||
10 | page.bestPractices < pass
11 | ) {
12 | return HTML_ICON_FAIL;
13 | }
14 | return HTML_ICON_PASS;
15 | };
16 |
17 | module.exports = pageInErrorOrWarning;
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to lighthouse-eco-index-aggregator
2 |
3 | First, ensure you have the [latest `npm`](https://docs.npmjs.com/).
4 |
5 | To get started with the repository:
6 |
7 | ```sh
8 | git clone https://github.com/AxaGuilDEv/lighthouse-ecoindex-aggregator.git
9 | cd lighthouse-ecoindex-aggregator
10 | npm install
11 | npm start
12 | ```
13 |
14 | You are now ready to contribute!
15 |
16 | ## Pull Request
17 |
18 | Please respect the following [PULL_REQUEST_TEMPLATE.md](./PULL_REQUEST_TEMPLATE.md)
19 |
20 | ## Issue
21 |
22 | Please respect the following [ISSUE_TEMPLATE.md](./ISSUE_TEMPLATE.md)
23 |
--------------------------------------------------------------------------------
/src/reporters/templates/templateDoughnut.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | <%= Value %>
10 | <%= Label %>
11 |
12 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Launch Program",
11 | "skipFiles": [
12 | "/**"
13 | ],
14 | "program": "${workspaceFolder}\\src\\cli.js",
15 | "args": ["--srcLighthouse", "./example/lighthouse", "--srcEcoIndex","./example/ecoindex" ,"--reports","html", "--outputPath" , "./example/report_final"]
16 | }
17 | ]
18 | }
--------------------------------------------------------------------------------
/cypress-demo/cypress/support/e2e.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/e2e.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
--------------------------------------------------------------------------------
/playwright-demo/extensions/ecoIndex/ecoIndexExtension.ts:
--------------------------------------------------------------------------------
1 | import { AnalyseConfiguration, PwExtension } from "../extensionWrapper.js";
2 | import check from "@cnumr/eco-index-audit";
3 | import { ECOINDEX_OPTIONS } from "./ecoIndexConfig.js";
4 |
5 |
6 | export class EcoIndexExtension implements PwExtension {
7 | async analyse(config: AnalyseConfiguration) {
8 | const page = config.page
9 | var cookies = await page.context().cookies();
10 | await check(
11 | {
12 | outputFileName: config.stepName,
13 | url: page.url(),
14 | cookies,
15 | remote_debugging_address : "127.0.0.1",
16 | remote_debugging_port: 9222,
17 | waitForSelector: config.selectorToWaitBeforeAnalyse,
18 | ...ECOINDEX_OPTIONS
19 | },
20 | true
21 | );
22 | }
23 | }
--------------------------------------------------------------------------------
/src/reporters/globalTag.js:
--------------------------------------------------------------------------------
1 | const globalNoteTag = "GlobalNote";
2 | const globalPerformanceTag = "GlobalPerformance";
3 | const globalAccessibilityTag = "GlobalAccessibility";
4 | const globalBestPracticesTag = "GlobalBestPractices";
5 | const globalEcoIndexTag = "GlobalEcoIndex";
6 | const htmlPerPageBlock = "PerPages";
7 | const performanceBlock = "PerformanceBlock";
8 | const accessibilityBlock = "AccessibilityBlock";
9 | const bestPracticesBlock = "BestPracticesBlock";
10 | const ecoIndexBlock = "EcoIndexBlock";
11 |
12 |
13 | module.exports = {
14 | globalNoteTag,
15 | globalPerformanceTag,
16 | globalAccessibilityTag,
17 | globalBestPracticesTag,
18 | globalEcoIndexTag,
19 | performanceBlock,
20 | accessibilityBlock,
21 | bestPracticesBlock,
22 | ecoIndexBlock,
23 | htmlPerPageBlock
24 | };
--------------------------------------------------------------------------------
/src/reporters/utils/statusGreen.spec.js:
--------------------------------------------------------------------------------
1 | const { statusGreen } = require("./statusGreen");
2 |
3 | describe("statusGreen", () => {
4 | const note = 80;
5 | it("should one sheet", async () => {
6 | const options = { verbose: true, lang: "Fr-fr", pass: 90, fail: 85 };
7 | var result = await statusGreen(note, options);
8 | expect(result).toMatchSnapshot();
9 | });
10 |
11 | it("should two sheet", async () => {
12 | const options = { verbose: true, lang: "Fr-fr", pass: 90, fail: 60 };
13 | var result = await statusGreen(note, options);
14 | expect(result).toMatchSnapshot();
15 | });
16 |
17 | it("should three sheet", async () => {
18 | const options = { verbose: true, lang: "Fr-fr", pass: 20, fail: 10 };
19 | var result = await statusGreen(note, options);
20 | expect(result).toMatchSnapshot();
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lighthouse-eco-index-aggregator",
3 | "version": "1.0.2",
4 | "description": "Tool for aggregating lighthouse and eco-index results",
5 | "main": "src/cli.js",
6 | "private": false,
7 | "files": [
8 | "src",
9 | "readme.md",
10 | "globalReports.html",
11 | "LICENSE.md",
12 | "package.json",
13 | "package-lock.json",
14 | ".eslintrc.js"
15 | ],
16 | "scripts": {
17 | "start": "node src/cli.js --srcLighthouse=./example/lighthouse --srcEcoIndex=./example/ecoindex --reports=html --outputPath=./example/report_final",
18 | "lint": "eslint",
19 | "test": "jest --coverage src/",
20 | "prepare": "husky install"
21 | },
22 | "license": "MIT",
23 | "dependencies": {
24 | "command-line-args": "^5.2.1",
25 | "command-line-usage": "^5.0.0",
26 | "ejs": "^3.1.8",
27 | "fs-extra": "^11.1.1",
28 | "html-minifier": "^4.0.0"
29 | },
30 | "devDependencies": {
31 | "eslint": "^8.36.0",
32 | "husky": "^8.0.3",
33 | "jest": "^29.5.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/cypress-demo/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
27 | //import "cypress-audit/commands";
28 | import "@cypress-audit/lighthouse/commands"
--------------------------------------------------------------------------------
/src/reporters/utils/statusPerPage.js:
--------------------------------------------------------------------------------
1 | const ClassFail = "lh-audit--fail";
2 | const ClassPass = "lh-audit--pass";
3 | const ClassAverage = "lh-audit--average";
4 |
5 | const statusPerPage = (page,{pass,fail}) => {
6 | if (
7 | page?.performance > pass &&
8 | page?.bestPractices > pass &&
9 | page?.accessibility > pass &&
10 | page?.ecoIndex > pass
11 | ){
12 | return ClassPass;
13 | }
14 | else if (
15 | (page?.performance <= pass && page?.performance > fail) ||
16 | (page?.bestPractices <= pass && page?.bestPractices > fail) ||
17 | (page?.accessibility <= pass && page?.accessibility > fail) ||
18 | (page?.ecoIndex <= pass && page?.ecoIndex > fail)
19 | ){
20 | return ClassAverage;
21 | }
22 | else if (
23 | (page?.performance <= fail) ||
24 | (page?.bestPractices <= fail) ||
25 | (page?.accessibility <= fail) ||
26 | (page?.ecoIndex <= fail)
27 | ){
28 | return ClassFail;
29 | }
30 | else return "";
31 | };
32 |
33 | module.exports = {
34 | statusPerPage,
35 | ClassFail,
36 | ClassPass,
37 | ClassAverage
38 | };
39 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Axa France IARD / Axa France VIE
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/playwright-demo/extensions/extensionWrapper.ts:
--------------------------------------------------------------------------------
1 | import { Page } from "playwright-core";
2 | import { LighthouseExtension } from "./lighthouse/lighthouseExtension.js";
3 | import { EcoIndexExtension } from "./ecoIndex/ecoIndexExtension.js";
4 | import { AggregatorExtension } from "./aggregator/aggregatorExtension.js";
5 |
6 | export type AnalyseConfiguration = {
7 | stepName: string;
8 | selectorToWaitBeforeAnalyse?:string;
9 | page: Page;
10 | };
11 |
12 |
13 | export interface PwExtension {
14 | analyse(config: AnalyseConfiguration): Promise;
15 | }
16 |
17 | export class ExtensionWrapper {
18 | extensions: PwExtension[] = [];
19 |
20 | constructor(extensions?: PwExtension[]) {
21 | this.extensions = extensions ?? getExtensions();
22 | }
23 |
24 | public analyse = async (config: AnalyseConfiguration) =>
25 | await Promise.all(this.extensions.map(async (o) => await o.analyse(config)));
26 |
27 | public generateFinalReports = () =>{
28 | console.log("aggrege");
29 | new AggregatorExtension().generateFinalReports();
30 | }
31 | }
32 |
33 | const getExtensions = (): PwExtension[] => {
34 | const lh = [new EcoIndexExtension(), new LighthouseExtension()];
35 | return [...lh];
36 | };
37 |
--------------------------------------------------------------------------------
/playwright-demo/extensions/lighthouse/lighthouseExtension.ts:
--------------------------------------------------------------------------------
1 | import { LIGHTHOUSE_CONFIG, LIGHTHOUSE_OPTIONS, LIGHTHOUSE_PATHS, LIGHTHOUSE_REPORT_OPTIONS, LIGHTHOUSE_THRESHOLDS, LighthouseReportOptions} from "./lighthouseConfig.js";
2 | import lighthouse from 'lighthouse';
3 | import { launch } from 'puppeteer';
4 | import {
5 | AnalyseConfiguration,
6 | PwExtension,
7 | } from "../extensionWrapper.js";
8 | import { lighthouseReport } from "./lighthouseReport.js";
9 |
10 |
11 | export class LighthouseExtension implements PwExtension {
12 | async analyse(config: AnalyseConfiguration) {
13 | const page = config.page;
14 | const puppeteerBrowser = await launch({headless: 'new'});
15 |
16 | const puppeteerPage = await puppeteerBrowser.newPage();
17 | const cookies = await page.context().cookies();
18 | await puppeteerPage.setCookie(...cookies);
19 | const url = page.url();
20 |
21 | await puppeteerPage.goto(url);
22 |
23 | if (config.selectorToWaitBeforeAnalyse)
24 | {
25 | await puppeteerPage.waitForSelector(config.selectorToWaitBeforeAnalyse);
26 | }
27 |
28 | const lighthouseAudit = await lighthouse(url, LIGHTHOUSE_OPTIONS, LIGHTHOUSE_CONFIG, puppeteerPage);
29 | const reportOption = LIGHTHOUSE_REPORT_OPTIONS;
30 | reportOption.fileName = config.stepName;
31 |
32 |
33 | lighthouseReport(reportOption, LIGHTHOUSE_PATHS, lighthouseAudit);
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/src/reporters/pageTag.js:
--------------------------------------------------------------------------------
1 | const PageSizeTag = "PageSize";
2 | const PageSizeRecommendationTag = "PageSizeRecommendation";
3 | const PageComplexityTag = "PageComplexity";
4 | const PageComplexityRecommendationTag = "PageComplexityRecommendation";
5 | const lighthouseReportPathTag = "LighthouseReportPath";
6 | const NumberOfRequestTag = "NumberOfRequest";
7 | const NumberOfRequestRecommendationTag = "NumberOfRequestRecommendation";
8 | const GreenhouseGasesTag = "GreenhouseGases";
9 | const GreenhouseGasesKmTag = "GreenhouseGasesKm";
10 | const WaterTag = "Water";
11 | const WaterShowerTag = "WaterShower";
12 |
13 | const greenItMetricsBlock = "GreenItMetricsBlock";
14 | const pageMetricsBlock = "PageMetricsBlock";
15 | const IconPerPageTag = "IconPerPageTag";
16 |
17 | const numberPageTag = "numberPageTag";
18 | const pageNameTag = "PageName";
19 | const statusClassPerPageTag = "StatusClassPerPageTag";
20 |
21 | module.exports ={
22 | PageSizeTag ,
23 | PageSizeRecommendationTag,
24 | PageComplexityTag ,
25 | PageComplexityRecommendationTag ,
26 | lighthouseReportPathTag ,
27 | NumberOfRequestTag ,
28 | NumberOfRequestRecommendationTag ,
29 | GreenhouseGasesTag ,
30 | GreenhouseGasesKmTag,
31 | WaterTag ,
32 | WaterShowerTag,
33 | greenItMetricsBlock,
34 | pageMetricsBlock ,
35 | IconPerPageTag,
36 | numberPageTag,
37 | pageNameTag,
38 | statusClassPerPageTag
39 | };
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/playwright-demo/Tests/welcomeAxaPage.spec.ts:
--------------------------------------------------------------------------------
1 | import { test, expect } from '@playwright/test';
2 | import { ExtensionWrapper } from "../extensions/extensionWrapper.js";
3 |
4 | const extensions = new ExtensionWrapper();
5 |
6 |
7 | test('test welcome page', async ({ page }) => {
8 | await page.goto('https://www.axa.fr/');
9 | await page.getByRole('button', { name: 'Accepter et fermer' }).click();
10 | await extensions.analyse({page, stepName: "1_Welcome_page_AXA", selectorToWaitBeforeAnalyse:'.o-herobanner__content' });
11 | await page.waitForTimeout(1000);
12 | await page.getByRole('button', { name: 'Véhicules' }).click();
13 | await page.getByRole('link', { name: 'Assurance auto' }).click();
14 |
15 | await expect(page.locator('h1')).toContainText('Assurance auto');
16 |
17 | await page.getByText('Estimation express').click({
18 | button: 'right'
19 | });
20 |
21 | await extensions.analyse({page, stepName: "2_Auto_page_AXA", selectorToWaitBeforeAnalyse:'.universe-auto' });
22 |
23 | });
24 |
25 |
26 |
27 | test('test health page', async ({ page }) => {
28 | await page.goto('https://www.axa.fr/complementaire-sante.html');
29 | await page.getByRole('button', { name: 'Accepter et fermer' }).click();
30 | await extensions.analyse({page, stepName: "3_health_page_AXA", selectorToWaitBeforeAnalyse:'.universe-health' });
31 | });
32 |
33 | test.afterAll(async ({}, testinfo) => {
34 | if (testinfo.status === 'passed')
35 | {
36 | await extensions.generateFinalReports();
37 | }
38 | });
39 |
--------------------------------------------------------------------------------
/src/reporters/translate/en-GB.json:
--------------------------------------------------------------------------------
1 | {
2 | "LabelGlobalTitle": "Global synthesis",
3 | "SeeReport":"To view the full Lighthouse report on this page, please visit",
4 | "DescriptionEcoIndex":"To calculate the ecoindex, we rely on three properties: the number of HTTP requests, the weight of the page, and the complexity of the DOM. Here are the results for this page",
5 | "DescriptionMetrics" : "From the previous metrics, an estimate of greenhouse gas emissions and water consumption is calculated. Here is the result for this page:",
6 | "LabelDescriptionAudit":"Here are the estimates of greenhouse gas emissions and water consumption for the entire audited site.",
7 | "LabelResultByPage":"Résults per page",
8 | "LabelPage":"Page",
9 | "LabelMetrics": "Metrics",
10 | "LabelTheReport": "The Report",
11 | "LabelValue": "Value",
12 | "LabelMaxRecommandedValue": "Maximum recommended value",
13 | "LabelNumberRequest": "Number of requests",
14 | "LabelWeightPage": "Weight page",
15 | "LabelComplexityDom": "DOM Complexity",
16 | "LabelGases": "Greenhouse gases",
17 | "LabelWaterConsummation": "Water consumption",
18 | "LabelConvertedValue" : "Converted value",
19 | "LabelAccessibility":"Accessibility",
20 | "LabelBestPractices" : "Best Practices",
21 | "LabelEcoIndex" : "EcoIndex",
22 | "LabelPerformance" : "Performances",
23 | "LabelShowers":"Showers",
24 | "LabelStep":"Step",
25 | "LabelWordFor" : "for",
26 | "LabelWordVisits" : "visits"
27 |
28 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, we pledge to respect all
4 | people who contribute through reporting issues, posting feature requests,
5 | updating documentation, submitting pull requests or patches, and other
6 | activities.
7 |
8 | We are committed to making participation in this project a harassment-free
9 | experience for everyone, regardless of level of experience, gender, gender
10 | identity and expression, sexual orientation, disability, personal appearance,
11 | body size, race, age, or religion.
12 |
13 | Examples of unacceptable behavior by participants include the use of sexual
14 | language or imagery, derogatory comments or personal attacks, trolling, public
15 | or private harassment, insults, or other unprofessional conduct.
16 |
17 | Project maintainers have the right and responsibility to remove, edit, or
18 | reject comments, commits, code, wiki edits, issues, and other contributions
19 | that are not aligned to this Code of Conduct. Project maintainers who do not
20 | follow the Code of Conduct may be removed from the project team.
21 |
22 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
23 | reported by opening an issue or contacting one or more of the project
24 | maintainers.
25 |
26 | This Code of Conduct is adapted from the
27 | [Contributor Covenant](http://contributor-covenant.org), version 1.0.0,
28 | available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
29 |
--------------------------------------------------------------------------------
/src/reporters/utils/statusGreen.js:
--------------------------------------------------------------------------------
1 | const ejs = require("ejs");
2 | const { readTemplate } = require("../readTemplate");
3 | const statusGreen = (note, { pass, fail, verbose }) => {
4 | const cssPassClass = "green-audit--pass";
5 | const cssFailClass = "green-audit--fail";
6 | const svgTemplate = readTemplate("sheet.svg");
7 | const svgClassTag = "svgClassTag";
8 | const firstSheetClass = cssPassClass;
9 | let secondSheetClass = cssFailClass;
10 | let threeSheetClass = cssFailClass;
11 |
12 | if (note >= pass) {
13 | secondSheetClass = cssPassClass;
14 | threeSheetClass = cssPassClass;
15 | } else if (note < pass && note >= fail) {
16 | secondSheetClass = cssPassClass;
17 | }
18 | if (verbose) {
19 | console.log("firstSheetClass", firstSheetClass);
20 | console.log("secondSheetClass", secondSheetClass);
21 | console.log("threeSheetClass", threeSheetClass);
22 | }
23 | const firtSheet = ejs.render(svgTemplate, {
24 | [svgClassTag]: firstSheetClass,
25 | });
26 |
27 | const secondSheet = ejs.render(svgTemplate, {
28 | [svgClassTag]: secondSheetClass,
29 | });
30 |
31 | const ThreeSheet = ejs.render(svgTemplate, {
32 | [svgClassTag]: threeSheetClass,
33 | });
34 |
35 | let template = readTemplate("templateStatusGreen.html");
36 | return ejs.render(template, {
37 | mySvg: {
38 | firtSheet: firtSheet,
39 | secondSheet: secondSheet,
40 | ThreeSheet: ThreeSheet,
41 | },
42 | });
43 | };
44 |
45 | module.exports = {
46 | statusGreen,
47 | };
48 |
--------------------------------------------------------------------------------
/src/reporters/translate/Fr-fr.json:
--------------------------------------------------------------------------------
1 | {
2 | "LabelGlobalTitle": "Synthèse Globale",
3 | "SeeReport": "Pour voir le rapport complet Lighthouse de cette page, veuillez visiter",
4 | "DescriptionEcoIndex": "Pour calculer l'ecoindex, nous nous basons sur trois propriétés : le nombre de requetes HTTP, le poid de la page, ainsi que la complexité du DOM. Voici les résultats pour cette page",
5 | "DescriptionMetrics": "A partir des métriques précédente, une estimation des emissions de gaz à effet de serre ainsi que la consommation d'eau est calculée. Voici le résultat pour cette page:",
6 | "LabelDescriptionAudit": "Voici les estimations d'emission de Gaz à effet de serre et de consomation d'eau pour l'ensemble du site audité.",
7 | "LabelResultByPage": "Résultats par page",
8 | "LabelPage": "Page",
9 | "LabelMetrics": "Métriques",
10 | "LabelTheReport": "Ce Rapport",
11 | "LabelValue": "Valeur",
12 | "LabelMaxRecommandedValue": "Valeur Préconisée Maximale",
13 | "LabelNumberRequest": "Nombre de Requetes",
14 | "LabelWeightPage": "Poid de la page",
15 | "LabelComplexityDom": "Complexité du DOM",
16 | "LabelGases": "Gaz à effet de serre",
17 | "LabelWaterConsummation": "Consommation en eau",
18 | "LabelConvertedValue": "Valeur convertie",
19 | "LabelAccessibility": "Accessibilité",
20 | "LabelBestPractices": "Bonnes pratiques",
21 | "LabelEcoIndex": "EcoIndex",
22 | "LabelPerformance": "Performances",
23 | "LabelShowers": "Douches",
24 | "LabelStep": "Contrôle",
25 | "LabelWordFor": "pour",
26 | "LabelWordVisits": "visites"
27 | }
28 |
--------------------------------------------------------------------------------
/src/reporters/templates/water.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/lighthouse/aggregatorService.spec.js:
--------------------------------------------------------------------------------
1 | const aggregate = require("./aggregatorService");
2 | const fs = require("fs");
3 | const path = require("path");
4 | jest.mock("fs");
5 |
6 | describe("AggregatorService", () => {
7 | const inputValue = {
8 | categories: {
9 | performance: {
10 | score: 0.76,
11 | },
12 | accessibility: {
13 | score: 0.6,
14 | },
15 | "best-practices": {
16 | score: 0.78,
17 | },
18 | },
19 | };
20 |
21 | it("should return the lightouse output", async () => {
22 | const options = { srcLighthouse: "test", verbose: true };
23 | const output = {
24 | performance: 76,
25 | accessibility: 60,
26 | bestPractices: 78,
27 | perPages: [
28 | {
29 | pageName: "foo",
30 | lighthouseReport: path.join("lighthouse", "foo.html"),
31 | performance: 76,
32 | accessibility: 60,
33 | bestPractices: 78,
34 | },
35 | {
36 | pageName: "bar",
37 | lighthouseReport: path.join("lighthouse", "bar.html"),
38 | performance: 76,
39 | accessibility: 60,
40 | bestPractices: 78,
41 | },
42 | ],
43 | };
44 | jest.spyOn(fs, "readdirSync").mockImplementation(() => {
45 | return ["foo.json", "bar.json"];
46 | });
47 | jest.spyOn(fs, "existsSync").mockImplementation(() => true);
48 | jest.spyOn(fs, "readFileSync").mockImplementation(() => {
49 | return JSON.stringify(inputValue);
50 | });
51 |
52 | const result = await aggregate(options);
53 | expect(result).toEqual(output);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/src/reporters/templates/templatePerPage.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
20 |
27 |
37 | <%- PageMetricsBlock %>
38 | <%- GreenItMetricsBlock %>
39 |
40 |
--------------------------------------------------------------------------------
/src/reporters/generatorReports.spec.js:
--------------------------------------------------------------------------------
1 | const { generateReports } = require("./generatorReports");
2 | const fs = require("fs");
3 | const path = require("path");
4 |
5 | describe("generatorReports", () => {
6 | const options = { verbose: true, lang: "Fr-fr", pass: 90, fail: 20, outputPath: "./" };
7 |
8 | const output = {
9 | ecoIndex: 86,
10 | grade: "A",
11 | greenhouseGases: 1.56,
12 | water: 2,
13 | performance: 10,
14 | accessibility: 20,
15 | bestPractices: 30,
16 | waterNumberOfVisits:100,
17 | gasesNumberOfVisits:100,
18 | perPages: [
19 | {
20 | pageName: "test1",
21 | lighthouseReport: "reports/test1.html",
22 | accessibility: 70,
23 | bestPractices: 70,
24 | performance: 70,
25 | ecoIndex: 86,
26 | grade: "A",
27 | greenhouseGases: 2500,
28 | water: 2,
29 | waterNumberOfVisits:100,
30 | gasesNumberOfVisits:100,
31 | metrics: [
32 | {
33 | name: "number_requests",
34 | value: 9,
35 | status: "info",
36 | recommandation: "< 30 requests",
37 | },
38 | {
39 | name: "page_size",
40 | value: 5243,
41 | status: "error",
42 | recommandation: "< 1000kb",
43 | },
44 | {
45 | name: "Page_complexity",
46 | value: 110,
47 | status: "info",
48 | recommandation: "Between 300 and 500 nodes",
49 | },
50 | ],
51 | greenhouseGasesKm: 2500,
52 | waterShower: 250,
53 | },
54 | ],
55 | };
56 |
57 | it("replace all tag", async () => {
58 | await generateReports(options, output);
59 | const result = fs.readFileSync("report.html").toString();
60 | expect(result).toMatchSnapshot();
61 | fs.rmSync("report.html");
62 | });
63 | });
64 |
--------------------------------------------------------------------------------
/example/ecoindex/example1.json:
--------------------------------------------------------------------------------
1 | {
2 | "score": 80,
3 | "grade": "A",
4 | "estimatation_co2": {
5 | "comment": "Pour un total de 2000 visites par mois, ceci correspond à 31km en voiture (Peugeot 208 5P 1.6 BlueHDi FAP (75ch) BVM5)",
6 | "commentDetails": {
7 | "numberOfVisit": 2000,
8 | "value_km": 31
9 | }
10 | },
11 | "estimatation_water": {
12 | "comment": "Pour un total de 2000 visites par mois, ceci correspond à 1 douche",
13 | "commentDetails": {
14 | "numberOfVisit": 2000,
15 | "value_shower": 1
16 | }
17 | },
18 | "pages": [
19 | {
20 | "url": "https://devfest.gdglille.org/",
21 | "ecoIndex": 80,
22 | "grade": "A",
23 | "greenhouseGasesEmission": 1.4,
24 | "waterConsumption": 2.1,
25 | "metrics": [
26 | {
27 | "name": "number_requests",
28 | "value": 18,
29 | "status": "info",
30 | "recommandation": "< 30 requests"
31 | },
32 | {
33 | "name": "page_size",
34 | "value": 277,
35 | "status": "warning",
36 | "recommandation": "< 1000kb"
37 | },
38 | {
39 | "name": "Page_complexity",
40 | "value": 190,
41 | "status": "error",
42 | "recommandation": "Between 300 and 500 nodes"
43 | }
44 | ],
45 | "estimatation_co2": {
46 | "comment": "Pour un total de 2000 visites par mois, ceci correspond à 31km en voiture (Peugeot 208 5P 1.6 BlueHDi FAP (75ch) BVM5)",
47 | "commentDetails": {
48 | "numberOfVisit": 2000,
49 | "value_km": 31
50 | }
51 | },
52 | "estimatation_water": {
53 | "comment": "Pour un total de 2000 visites par mois, ceci correspond à 1 douche",
54 | "commentDetails": {
55 | "numberOfVisit": 2000,
56 | "value_shower": 1
57 | }
58 | }
59 | }
60 | ]
61 | }
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | const aggregatorServiceLighthouse = require("./lighthouse/aggregatorService");
2 | const aggregatorServiceEcoIndex = require("./ecoIndex/aggregatorService");
3 | const aggregatorGlobalService = require("./globlalAggregation/aggregatorService");
4 | const { generateReports, generateReportsSonar } = require("./reporters/generatorReports");
5 |
6 | const path = require("path");
7 | const fs = require("fs");
8 |
9 | const defaultThreshold = {
10 | pass: 90,
11 | fail: 30
12 | };
13 |
14 | const formatReports = reports => {
15 | if(!reports){
16 | return [];
17 | }
18 | return Array.isArray(reports) ? reports : [reports];
19 | };
20 |
21 | module.exports = async (_options) => {
22 | let options = {
23 | ...defaultThreshold,
24 | ..._options
25 | };
26 |
27 | if(options.config){
28 | options = {
29 | ...options,
30 | ...require(options.config)
31 | };
32 | }
33 |
34 | const resultsGlobalLighthouse = await aggregatorServiceLighthouse(options);
35 | const resultsGlobalEcoindex = await aggregatorServiceEcoIndex(options);
36 | const resultsGlobal = aggregatorGlobalService(options, resultsGlobalLighthouse, resultsGlobalEcoindex);
37 |
38 |
39 | const reports = formatReports(options.reports);
40 | const destFolder = path.join(process.cwd(), options.outputPath ?? "globalReports");
41 | if(fs.existsSync(destFolder)){
42 | fs.rmSync(destFolder, { recursive: true });
43 | }
44 | fs.mkdirSync(destFolder, { recursive: true });
45 | options.outputPath = destFolder;
46 |
47 | await Promise.all(reports.map(report => {
48 | if(typeof report !== "string"){
49 | return report(options, resultsGlobal);
50 | }
51 | if (report === "html") {
52 | return generateReports(options, resultsGlobal);
53 | }
54 | if (report === "sonar") {
55 | return generateReportsSonar(options, resultsGlobal);
56 | }
57 | }));
58 |
59 |
60 | return resultsGlobal;
61 | };
62 |
--------------------------------------------------------------------------------
/playwright-demo/extensions/lighthouse/lighthouseConfig.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 |
3 | import { Config, Flags } from "lighthouse";
4 |
5 | export const LIGHTHOUSE_THRESHOLDS: Record = {
6 | performance: 80,
7 | accessibility: 80,
8 | 'best-practices':80
9 | };
10 |
11 | export const LIGHTHOUSE_OPTIONS: Flags = {
12 | formFactor: 'desktop',
13 | screenEmulation: {
14 | mobile: false,
15 | disabled: false,
16 | width: 1920,
17 | height: 1080,
18 | deviceScaleFactor: 1,
19 | },
20 | throttling: {
21 | rttMs: 40,
22 | throughputKbps: 11024,
23 | cpuSlowdownMultiplier: 1,
24 | requestLatencyMs: 0,
25 | downloadThroughputKbps: 0,
26 | uploadThroughputKbps: 0,
27 | },
28 | output: 'html',
29 | };
30 |
31 | export const LIGHTHOUSE_CONFIG: Config = {
32 | extends: 'lighthouse:default',
33 | settings: {
34 | pauseAfterLoadMs: 1000,
35 | networkQuietThresholdMs: 1000
36 | }
37 | };
38 |
39 | export interface LighthousePaths {
40 | reportsPath: string;
41 | lighthouseReportsPath: string;
42 | }
43 |
44 | export const LIGHTHOUSE_PATHS: LighthousePaths = {
45 | reportsPath: './reports/',
46 | lighthouseReportsPath: './reports/lighthouse/',
47 | };
48 |
49 | export interface LighthouseReportOptions {
50 | minifyHtmlReports: boolean;
51 | htmlMinifierOptions: Record;
52 | fileName : string;
53 | }
54 |
55 | export const LIGHTHOUSE_REPORT_OPTIONS: LighthouseReportOptions = {
56 | minifyHtmlReports: true,
57 | fileName:'',
58 | htmlMinifierOptions: {
59 | includeAutoGeneratedTags: true,
60 | removeAttributeQuotes: true,
61 | removeComments: true,
62 | removeRedundantAttributes: true,
63 | removeScriptTypeAttributes: true,
64 | removeStyleLinkTypeAttributes: true,
65 | sortClassName: true,
66 | useShortDoctype: true,
67 | collapseWhitespace: true,
68 | minifyCSS: true,
69 | minifyJS: true,
70 | },
71 | };
72 |
--------------------------------------------------------------------------------
/playwright-demo/extensions/lighthouse/lighthouseReport.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import fs from "fs";
3 | import { minify } from "html-minifier";
4 | import { LighthousePaths, LighthouseReportOptions } from './lighthouseConfig.js';
5 |
6 | const createLighthouseReportsDirectories = (paths: LighthousePaths) => {
7 | if (!fs.existsSync(paths.reportsPath)) {
8 | fs.mkdirSync(paths.reportsPath);
9 | }
10 |
11 | if (!fs.existsSync(paths.lighthouseReportsPath)) {
12 | fs.mkdirSync(paths.lighthouseReportsPath, {recursive : true});
13 | }
14 | };
15 |
16 | const cleanLighthouseReportsFiles = (options: LighthouseReportOptions, paths : LighthousePaths) => {
17 | if (fs.existsSync(`${paths.lighthouseReportsPath}${options.fileName}.json`)) {
18 | fs.unlinkSync(`${paths.lighthouseReportsPath}${options.fileName}.json`);
19 | }
20 | if (fs.existsSync(`${paths.lighthouseReportsPath}${options.fileName}.html`)) {
21 | fs.unlinkSync(`${paths.lighthouseReportsPath}${options.fileName}.html`);
22 | }
23 | };
24 |
25 | const writeLighthouseReportJsonFile = (options: LighthouseReportOptions, paths : LighthousePaths, lighthouseAudit) => {
26 | const reportContent = JSON.stringify(lighthouseAudit.lhr);
27 | fs.writeFileSync(`${paths.lighthouseReportsPath}${options.fileName}.json`, reportContent, { flag: 'a+' });
28 | };
29 |
30 | const writeLighthouseReportHtmlFile = (options : LighthouseReportOptions, paths : LighthousePaths, lighthouseAudit) => {
31 | let reportContent = lighthouseAudit.report;
32 | if (options && options.minifyHtmlReports) {
33 | reportContent = minify(reportContent, options.htmlMinifierOptions);
34 | }
35 | fs.writeFileSync(`${paths.lighthouseReportsPath}${options.fileName}.html`, reportContent, {
36 | flag: 'a+',
37 | });
38 | };
39 | export const lighthouseReport = (options : LighthouseReportOptions, paths : LighthousePaths, lighthouseAudit) => {
40 | createLighthouseReportsDirectories(paths);
41 | cleanLighthouseReportsFiles(options, paths);
42 | writeLighthouseReportJsonFile(options, paths, lighthouseAudit);
43 | writeLighthouseReportHtmlFile(options, paths, lighthouseAudit);
44 | return lighthouseReport;
45 | };
--------------------------------------------------------------------------------
/playwright-demo/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 |
3 | /**
4 | * Read environment variables from file.
5 | * https://github.com/motdotla/dotenv
6 | */
7 |
8 | /**
9 | * See https://playwright.dev/docs/test-configuration.
10 | */
11 | export default defineConfig({
12 | testDir: '',
13 | /* Run tests in files in parallel */
14 | fullyParallel: true,
15 | /* Fail the build on CI if you accidentally left test.only in the source code. */
16 | forbidOnly: !!process.env.CI,
17 | /* Retry on CI only */
18 | retries: process.env.CI ? 2 : 0,
19 | /* Opt out of parallel tests on CI. */
20 | workers: process.env.CI ? 1 : undefined,
21 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
22 | reporter: [['html', { outputFolder: 'reports/playwright', open: 'never' }]],
23 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
24 | use: {
25 | /* Base URL to use in actions like `await page.goto('/')`. */
26 | // baseURL: 'http://127.0.0.1:3000',
27 |
28 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
29 | trace: 'on',
30 | },
31 |
32 | /* Configure projects for major browsers */
33 | projects: [
34 | {
35 | timeout:60*5*1000,
36 | name: "chromium",
37 | use: {
38 | headless: true,
39 | ...devices["Desktop Chrome"],
40 | serviceWorkers:'allow',
41 | launchOptions: {
42 | args: ["--remote-debugging-port=9222"],
43 | },
44 | },
45 | },
46 |
47 | /* Test against mobile viewports. */
48 | // {
49 | // name: 'Mobile Chrome',
50 | // use: { ...devices['Pixel 5'] },
51 | // },
52 | // {
53 | // name: 'Mobile Safari',
54 | // use: { ...devices['iPhone 12'] },
55 | // },
56 |
57 | /* Test against branded browsers. */
58 | // {
59 | // name: 'Microsoft Edge',
60 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
61 | // },
62 | // {
63 | // name: 'Google Chrome',
64 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
65 | // },
66 | ],
67 |
68 | /* Run your local dev server before starting the tests */
69 | // webServer: {
70 | // command: 'npm run start',
71 | // url: 'http://127.0.0.1:3000',
72 | // reuseExistingServer: !process.env.CI,
73 | // },
74 | });
75 |
--------------------------------------------------------------------------------
/cypress-demo/cypress.config.js:
--------------------------------------------------------------------------------
1 | const {defineConfig} = require("cypress");
2 | const {lighthouse, prepareAudit} = require("@cypress-audit/lighthouse");
3 | const fs = require("fs");
4 | const path = require("path");
5 | const aggregate = require("../src/main");
6 |
7 | const { prepareAudit: prepareEcoIndexAudit, checkEcoIndex } = require("eco-index-audit/src/cypress");
8 | const { cwd } = require("process");
9 |
10 |
11 | const lighthouseOutputPathDir = path.join(__dirname, "reports/lighthouse");
12 | const ecoIndexOutputPathDir = path.join(__dirname, "reports/ecoindex");
13 |
14 | if(fs.existsSync(lighthouseOutputPathDir)){
15 | fs.rmdirSync(lighthouseOutputPathDir, { recursive: true, force: true });
16 | }
17 | if(fs.existsSync(ecoIndexOutputPathDir)){
18 | fs.rmdirSync(ecoIndexOutputPathDir, { recursive: true, force: true });
19 | }
20 |
21 | fs.mkdirSync(ecoIndexOutputPathDir, {recursive: true});
22 | fs.mkdirSync(lighthouseOutputPathDir, {recursive: true});
23 |
24 |
25 | module.exports = defineConfig({
26 | e2e: {
27 | setupNodeEvents(on) {
28 | on("after:run", async () => {
29 | await aggregate({
30 | config: path.resolve(cwd(), "./config.js")
31 | });
32 | });
33 |
34 | on("before:browser:launch", (_browser = {}, launchOptions) => {
35 | prepareAudit(launchOptions);
36 | prepareEcoIndexAudit(launchOptions);
37 | });
38 | on("task", {
39 | lighthouse: lighthouse(result => {
40 | const url = result.lhr.finalUrl;
41 | fs.writeFileSync(
42 | path.resolve(__dirname, path.join(lighthouseOutputPathDir, `${url.replace("://", "_").replace("/", "_")}.json`)),
43 | JSON.stringify(result.lhr, undefined, 2));
44 | fs.writeFileSync(
45 | path.resolve(__dirname, path.join(lighthouseOutputPathDir, `${url.replace("://", "_").replace("/", "_")}.html`)),
46 | result.report);
47 | }),
48 | checkEcoIndex: ({ url }) => checkEcoIndex({
49 | url,
50 | options: {
51 | output: ["json"],
52 | outputPathDir: ecoIndexOutputPathDir,
53 | outputFileName: url.replace("://", "_").replace("/", "_"),
54 | }
55 | })
56 | });
57 | },
58 | },
59 | });
60 |
--------------------------------------------------------------------------------
/src/reporters/templates/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
16 |
17 |
37 |
38 |
39 |
65 |
66 |
--------------------------------------------------------------------------------
/src/reporters/templates/templatePageMetrics.html:
--------------------------------------------------------------------------------
1 |
2 | <%= Translations.DescriptionEcoIndex %>
3 |
4 |
5 |
6 |
7 | |
8 |
9 | <%= Translations.LabelMetrics %>
10 |
11 | |
12 |
13 |
14 | <%= Translations.LabelValue %>
15 |
16 | |
17 |
18 |
19 | <%= Translations.LabelMaxRecommandedValue %>
20 |
21 | |
22 |
23 |
24 |
25 |
26 | |
27 |
28 | <%= Translations.LabelNumberRequest %>
29 |
30 | |
31 |
32 | <%= NumberOfRequest %>
33 | |
34 |
35 | <%= NumberOfRequestRecommendation %>
36 | |
37 |
38 |
39 |
40 | |
41 | <%= Translations.LabelWeightPage %>
42 | |
43 |
44 |
45 | <%= PageSize %>
46 | |
47 |
48 | <%= PageSizeRecommendation %>
49 | |
50 |
51 |
52 |
53 | |
54 | <%= Translations.LabelComplexityDom %>
55 | |
56 |
57 | <%= PageComplexity %>
58 | |
59 |
60 | <%= PageComplexityRecommendation %>
61 | |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/globlalAggregation/aggregatorService.js:
--------------------------------------------------------------------------------
1 | module.exports = (options, resultsGlobalLighthouse, resultsGlobalEcoindex) => {
2 | if (options?.verbose) {
3 | console.log("Global aggregations.");
4 | }
5 | return globalBaseEcoIndex(
6 | options,
7 | resultsGlobalEcoindex,
8 | resultsGlobalLighthouse
9 | );
10 | };
11 |
12 | const globalBaseEcoIndex = (options, ecoindex, lighthouse = {}) => {
13 | if (options?.verbose) {
14 | console.log("Aggregate lighthouse and ecoindex results");
15 | }
16 | const resultAggregatePerPage = (ecoindex?.perPages ?? []).map(
17 | (pageEcoIndex) => {
18 | const pageLighthouse = lighthouse.perPages.find(
19 | (pageLighthouse) => pageEcoIndex.pageName === pageLighthouse.pageName
20 | );
21 | return {
22 | pageName: pageEcoIndex.pageName,
23 | lighthouseReport: pageLighthouse?.lighthouseReport ?? "",
24 | accessibility: pageLighthouse?.accessibility ?? 0,
25 | bestPractices: pageLighthouse?.bestPractices ?? 0,
26 | performance: pageLighthouse?.performance ?? 0,
27 | ecoIndex: pageEcoIndex.ecoIndex,
28 | grade: pageEcoIndex.grade,
29 | greenhouseGases: pageEcoIndex.greenhouseGases,
30 | water: pageEcoIndex.water,
31 | metrics: pageEcoIndex.metrics,
32 | greenhouseGasesKm: pageEcoIndex.greenhouseGasesKm,
33 | waterShower: pageEcoIndex?.waterShower,
34 | waterNumberOfVisits: pageEcoIndex?.waterNumberOfVisits,
35 | gasesNumberOfVisits: pageEcoIndex?.gasesNumberOfVisits,
36 | };
37 | }
38 | );
39 |
40 | return {
41 | globalNote: Math.round(
42 | ((ecoindex.ecoIndex ?? 0) +
43 | (lighthouse.performance ?? 0) +
44 | (lighthouse.accessibility ?? 0) +
45 | (lighthouse.bestPractices ?? 0)) /
46 | 4
47 | ),
48 | ecoIndex: ecoindex.ecoIndex ?? 0,
49 | grade: ecoindex.grade ?? "G",
50 | greenhouseGases: ecoindex.greenhouseGases
51 | ? Math.round(ecoindex.greenhouseGases * 100) / 100
52 | : 0,
53 | water: ecoindex.water ? Math.round(ecoindex.water * 100) / 100 : 0,
54 | greenhouseGasesKm: ecoindex.greenhouseGasesKm ?? 0,
55 | waterShower: ecoindex.waterShower ? ecoindex.waterShower : 0,
56 | performance: lighthouse.performance ?? 0,
57 | accessibility: lighthouse.accessibility ?? 0,
58 | bestPractices: lighthouse.bestPractices ?? 0,
59 | waterNumberOfVisits: ecoindex?.waterNumberOfVisits
60 | ? ecoindex?.waterNumberOfVisits
61 | : 0,
62 | gasesNumberOfVisits: ecoindex?.gasesNumberOfVisits
63 | ? ecoindex?.gasesNumberOfVisits
64 | : 0,
65 | perPages: resultAggregatePerPage,
66 | };
67 | };
68 |
--------------------------------------------------------------------------------
/src/reporters/templates/templateGreenItMetrics.html:
--------------------------------------------------------------------------------
1 |
2 | <%- Translations.DescriptionMetrics %>
3 |
4 |
5 |
6 |
7 | |
8 |
9 | <%= Translations.LabelMetrics %>
10 |
11 | |
12 |
13 |
14 | <%= Translations.LabelValue %>
15 |
16 | |
17 |
18 |
19 | <%= Translations.LabelConvertedValue %>
20 |
21 | |
22 |
23 |
24 |
25 |
26 | |
27 |
28 | <%- mySvg.svgIconCo2 %>
29 |
30 | <%= Translations.LabelGases %>
31 |
32 |
33 | (<%- Translations.LabelWordFor %> <%= gasesNumberOfVisits %> <%- Translations.LabelWordVisits %>)
34 |
35 |
36 | |
37 |
38 |
39 | <%= greenhouseGases %>
40 |
41 | |
42 |
43 |
44 | <%= greenhouseGasesKm %> km
45 |
46 | |
47 |
48 |
49 |
50 | |
51 |
52 |
53 | <%- mySvg.svgIconWater %>
54 |
55 |
56 | <%= Translations.LabelWaterConsummation %>
57 |
58 |
59 | (<%- Translations.LabelWordFor %> <%= waterNumberOfVisits %> <%- Translations.LabelWordVisits %>)
60 |
61 |
62 | |
63 |
64 |
65 | <%= water %>
66 |
67 | |
68 |
69 |
70 | <%= waterShower %>
71 | <%= Translations.LabelShowers %>
72 |
73 | |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/lighthouse/aggregatorService.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 |
4 | module.exports = async options => {
5 | if (!options.srcLighthouse || !fs.existsSync(options.srcLighthouse)) {
6 | console.error("lighthouse folder not found!");
7 | process.exit(1);
8 | }
9 |
10 | const lighthouseReportsFiles = listFiles(options);
11 | return readFiles(options, lighthouseReportsFiles);
12 | };
13 |
14 | const readFiles = (options, lighthouseJsonReportsFiles) => {
15 | let globalPerformance = 0;
16 | let globalAccessibility = 0;
17 | let globalBestPractices = 0;
18 | const perPages = [];
19 |
20 | lighthouseJsonReportsFiles.forEach(fileName => {
21 | const pageName = fileName.replace(".json", "");
22 | const pathFile = path.join(options.srcLighthouse, fileName);
23 | const data = fs.readFileSync(pathFile);
24 | const result = JSON.parse(data);
25 | const performance = result?.categories?.performance?.score
26 | ? Math.round(result.categories.performance.score * 100)
27 | : 0;
28 | const accessibility = result?.categories?.accessibility?.score
29 | ? Math.round(result?.categories.accessibility.score * 100)
30 | : 0;
31 | const bestPractices = result?.categories["best-practices"]?.score
32 | ? Math.round(result?.categories["best-practices"].score * 100)
33 | : 0;
34 |
35 | globalPerformance += performance;
36 | globalAccessibility += accessibility;
37 | globalBestPractices += bestPractices;
38 | perPages.push({
39 | pageName,
40 | lighthouseReport: path.join("lighthouse", `${pageName}.html`),
41 | accessibility,
42 | bestPractices,
43 | performance,
44 | });
45 | });
46 | if (globalPerformance !== 0) {
47 | globalPerformance = Math.ceil(
48 | globalPerformance / lighthouseJsonReportsFiles.length
49 | );
50 | }
51 | if (globalAccessibility !== 0) {
52 | globalAccessibility = Math.ceil(
53 | globalAccessibility / lighthouseJsonReportsFiles.length
54 | );
55 | }
56 | if (globalBestPractices !== 0) {
57 | globalBestPractices = Math.ceil(
58 | globalBestPractices / lighthouseJsonReportsFiles.length
59 | );
60 | }
61 |
62 | if (options.verbose) {
63 | console.log("global performance:", globalPerformance);
64 | console.log("global accessibility:", globalAccessibility);
65 | console.log("global bestPractices:", globalBestPractices);
66 | }
67 | return {
68 | performance: globalPerformance,
69 | accessibility: globalAccessibility,
70 | bestPractices: globalBestPractices,
71 | perPages,
72 | };
73 | };
74 | const listFiles = options => {
75 | const lighthouseJsonReportsFiles = [];
76 | const files = fs.readdirSync(options.srcLighthouse);
77 | files.forEach(file => {
78 | if (path.extname(file) === ".json") {
79 | if (options.verbose) {
80 | console.log("Add file in list for aggregation: ", file);
81 | }
82 | lighthouseJsonReportsFiles.push(file);
83 | }
84 | });
85 | return lighthouseJsonReportsFiles;
86 | };
87 |
--------------------------------------------------------------------------------
/src/reporters/utils/statusPerPage.spec.js:
--------------------------------------------------------------------------------
1 | const {
2 | statusPerPage,
3 | ClassFail,
4 | ClassPass,
5 | ClassAverage,
6 | } = require("./statusPerPage");
7 |
8 | describe("statusPerPage", () => {
9 | const options = { pass: 90, fail: 35 };
10 |
11 | it("All pass", () => {
12 | const input = {
13 | performance: 96,
14 | accessibility: 96,
15 | ecoIndex: 96,
16 | bestPractices: 96,
17 | };
18 | const result = statusPerPage(input, options);
19 | expect(result).toEqual(ClassPass);
20 | });
21 |
22 | it("Performance Fail", () => {
23 | const input = {
24 | performance: 30,
25 | accessibility: 96,
26 | ecoIndex: 96,
27 | bestPractices: 96,
28 | };
29 | const result = statusPerPage(input, options);
30 | expect(result).toEqual(ClassFail);
31 | });
32 |
33 | it("Accessibility Fail", () => {
34 | const input = {
35 | performance: 96,
36 | accessibility: 30,
37 | ecoIndex: 96,
38 | bestPractices: 96,
39 | };
40 | const result = statusPerPage(input, options);
41 | expect(result).toEqual(ClassFail);
42 | });
43 |
44 | it("BestPerformances Fail", () => {
45 | const input = {
46 | performance: 96,
47 | accessibility: 96,
48 | ecoIndex: 96,
49 | bestPractices: 30,
50 | };
51 | const result = statusPerPage(input, options);
52 | expect(result).toEqual(ClassFail);
53 | });
54 | it("EcoIndex Fail", () => {
55 | const input = {
56 | performance: 96,
57 | accessibility: 96,
58 | ecoIndex: 96,
59 | bestPractices: 30,
60 | };
61 | const result = statusPerPage(input, options);
62 | expect(result).toEqual(ClassFail);
63 | });
64 | it("PerforMance Average", () => {
65 | const input = {
66 | performance: 56,
67 | accessibility: 96,
68 | ecoIndex: 96,
69 | bestPractices: 30,
70 | };
71 | const result = statusPerPage(input, options);
72 | expect(result).toEqual(ClassAverage);
73 | });
74 | it("Accessibility Average", () => {
75 | const input = {
76 | performance: 96,
77 | accessibility: 56,
78 | ecoIndex: 96,
79 | bestPractices: 30,
80 | };
81 | const result = statusPerPage(input, options);
82 | expect(result).toEqual(ClassAverage);
83 | });
84 | it("BestPractices Average", () => {
85 | const input = {
86 | performance: 96,
87 | accessibility: 96,
88 | ecoIndex: 96,
89 | bestPractices: 56,
90 | };
91 | const result = statusPerPage(input, options);
92 | expect(result).toEqual(ClassAverage);
93 | });
94 | it("EcoIndex Average", () => {
95 | const input = {
96 | performance: 96,
97 | accessibility: 96,
98 | ecoIndex: 60,
99 | bestPractices: 96,
100 | };
101 | const result = statusPerPage(input, options);
102 | expect(result).toEqual(ClassAverage);
103 | });
104 |
105 | it("No Applicable", () => {
106 | const input = {};
107 | const result = statusPerPage(input, options);
108 | expect(result).toEqual("");
109 | });
110 | });
111 |
--------------------------------------------------------------------------------
/src/cli.js:
--------------------------------------------------------------------------------
1 | const commandLineArgs = require("command-line-args");
2 | const commandLineUsage = require("command-line-usage");
3 | const aggregate = require("./main");
4 |
5 | const optionDefinitions = [
6 | { name: "verbose", alias: "v", type: Boolean },
7 | { name: "reports", type: String, multiple: true },
8 | { name: "srcLighthouse", type: String, multiple: false },
9 | { name: "srcEcoIndex", type: String, multiple: false },
10 | { name: "outputPath", type: String, multiple: false },
11 | { name: "lang", type: String, multiple: false },
12 | { name: "help", alias: "h", type: Boolean },
13 | { name: "config", type: String, multiple: false },
14 | { name: "pass", type: Number, multiple: false },
15 | { name: "fail", type: Number, multiple: false },
16 | { name: "Minify", alias: "m", type: Boolean },
17 | { name: "sonarFilePath", type: String },
18 | ];
19 |
20 | const sections = [
21 | {
22 | header: "A typical app",
23 | content: "Generates reports aggration lighthouse and ecoindex",
24 | },
25 | {
26 | header: "Options",
27 | optionList: [
28 | {
29 | name: "verbose",
30 | typeLabel: "{underline bool}",
31 | description: "Verbose a task",
32 | },
33 | {
34 | name: "outputPath",
35 | typeLabel: "{underline string}",
36 | description: "The path of the generated HTML report",
37 | },
38 | {
39 | name: "help",
40 | typeLabel: "{underline bool}",
41 | description: "Print this usage guide.",
42 | },
43 | {
44 | name: "srcLighthouse",
45 | typeLabel: "{underline string}",
46 | description: "folder with json reports lighthouse",
47 | },
48 | {
49 | name: "srcEcoIndex",
50 | typeLabel: "{underline string}",
51 | description: "folder with json reports ecoIndex",
52 | },
53 | {
54 | name: "lang",
55 | typeLabel: "{underline string}",
56 | description: "define language report",
57 | value: "en-GB",
58 | },
59 | {
60 | name: "config",
61 | typeLabel: "{underline string}",
62 | description: "define path config",
63 | },
64 | {
65 | name: "pass",
66 | typeLabel: "{underline num}",
67 | description: "define limit pass",
68 | },
69 | {
70 | name: "fail",
71 | typeLabel: "{underline num}",
72 | description: "define limit fail",
73 | },
74 | {
75 | name: "Minify",
76 | typeLabel: "{underline bool}",
77 | description: "used minify file",
78 | },
79 | {
80 | name: "sonarFilePath",
81 | typeLabel: "{underline string}",
82 | description: "the file to a static file managed by sonar",
83 | },
84 | ],
85 | },
86 | ];
87 |
88 | (async () => {
89 | const usage = commandLineUsage(sections);
90 | let options = commandLineArgs(optionDefinitions);
91 |
92 | if (
93 | options?.help ||
94 | (!options?.srcLighthouse && !options?.srcEcoIndex && !options?.config)
95 | ) {
96 | console.log(usage);
97 | return;
98 | }
99 |
100 | if (!options?.pass) {
101 | options.pass = 90;
102 | }
103 | if (!options?.fail) {
104 | options.fail = 30;
105 | }
106 |
107 | if (options?.verbose) {
108 | console.log(options);
109 | }
110 |
111 | await aggregate(options);
112 | })();
113 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: lighthouse-ecoindex-aggregator
5 |
6 | on:
7 | push:
8 | branches: [master]
9 | pull_request:
10 | branches: [master]
11 |
12 | jobs:
13 | skip_ci:
14 | runs-on: ubuntu-latest
15 | outputs:
16 | canSkip: ${{ steps.check.outputs.canSkip }}
17 | steps:
18 | - id: check
19 | uses: Legorooj/skip-ci@main
20 | build:
21 | runs-on: ubuntu-latest
22 | if: ${{ needs.skip_ci.outputs.canSkip != 'true' }}
23 | steps:
24 | - uses: actions/checkout@v2
25 | with:
26 | token: ${{ secrets.GIT_TOKEN }}
27 |
28 | - name: Bump version and push tag
29 | id: tag_version
30 | if: github.ref == 'refs/heads/master'
31 | uses: mathieudutour/github-tag-action@v6.0
32 | with:
33 | github_token: ${{ secrets.GITHUB_TOKEN }}
34 |
35 | - uses: actions/setup-node@v2
36 | with:
37 | node-version: 18
38 |
39 | - name: set user name
40 | run: |
41 | git config --global user.email "ci@axa.fr"
42 | git config --global user.name "ci@axa.fr"
43 |
44 | - name: npm version ${{ steps.tag_version.outputs.new_tag }}
45 | if: github.ref == 'refs/heads/master'
46 | run: npm version ${{ steps.tag_version.outputs.new_tag }}
47 |
48 | - name: npm ci
49 | run: npm ci
50 |
51 | - name: npm test
52 | run: npm test -- --runInBand --coverage --watchAll=false
53 |
54 | - id: publish-aggregator
55 | uses: JS-DevTools/npm-publish@v1
56 | if: github.ref == 'refs/heads/master'
57 | with:
58 | token: ${{ secrets.NPM_TOKEN }}
59 | package: ./package.json
60 |
61 | # - name: SonarCloud Scan
62 | # uses: sonarsource/sonarcloud-github-action@master
63 | # if: github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.head.repo.fork
64 | # env:
65 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
66 | # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
67 | # with:
68 | # args: >
69 | # -Dsonar.organization=axaguildev
70 | # -Dsonar.projectKey=AxaGuilDEv_lighthouse-ecoindex-aggregator
71 | # -Dsonar.exclusions=**/*.spec.js,**/*.stories.js,Scripts/**,**/*.scss,**/__snapshots__/**,**/*[Tt]ests.cs,**/node_modules/**,**/ClientApp/build/**,**/ClientApp/.storybook/**,**/ClientApp/storybook-static/**,**/obj/**,**/__mocks__/**,**/ClientApp/src/serviceWorker.ts
72 | # -Dsonar.javascript.lcov.reportPaths=**/coverage/lcov.info
73 |
74 | - name: Commit updates package.json
75 | uses: stefanzweifel/git-auto-commit-action@v4
76 | if: github.ref == 'refs/heads/master'
77 | with:
78 | commit_message: "[skip ci] Update version package.json"
79 | commit_user_name: GitHub
80 | commit_user_email: github-action@bot.com
81 | commit_author: GitHub
82 | push_options: "--force"
83 |
84 | - name: Create a GitHub release
85 | uses: ncipollo/release-action@v1
86 | if: github.ref == 'refs/heads/master'
87 | with:
88 | tag: ${{ steps.tag_version.outputs.new_tag }}
89 | name: Release ${{ steps.tag_version.outputs.new_tag }}
90 | body: ${{ steps.tag_version.outputs.changelog }}
91 |
--------------------------------------------------------------------------------
/src/ecoIndex/aggregatorService.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 |
4 | module.exports = async (options) => {
5 | if (!options.srcEcoIndex || !fs.existsSync(options.srcEcoIndex)) {
6 | console.error("ecoindex folder not found!");
7 | process.exit(1);
8 | }
9 | const ecoIndexJsonReportsFiles = listFiles(options);
10 | const results = readFiles(options, ecoIndexJsonReportsFiles);
11 | return results;
12 | };
13 |
14 | const readFiles = (options, ecoIndexJsonReportsFiles) => {
15 | const perPages = [];
16 | let ecoIndex = 0;
17 | let greenhouseGases = 0;
18 | let water = 0;
19 | let greenhouseGasesKm = 0;
20 | let waterShower = 0;
21 | let waterNumberOfVisits = 0;
22 | let gasesNumberOfVisits = 0;
23 |
24 | ecoIndexJsonReportsFiles.forEach((fileName) => {
25 | const pageName = fileName.replace(".json", "");
26 | const pathFile = path.join(options.srcEcoIndex, fileName);
27 | const data = fs.readFileSync(pathFile);
28 | const result = JSON.parse(data);
29 | if (options.verbose) {
30 | console.log("read file:", fileName);
31 | }
32 | if (result.pages[0]) {
33 | const page = result.pages[0];
34 | ecoIndex += page.ecoIndex;
35 | greenhouseGases += parseFloat(page.greenhouseGasesEmission);
36 | water += parseFloat(page.waterConsumption);
37 | greenhouseGasesKm += page.estimatation_co2?.commentDetails?.value_km ?? 0;
38 | waterShower += page.estimatation_water?.commentDetails?.value_shower ?? 0;
39 | waterNumberOfVisits +=
40 | page.estimatation_water?.commentDetails?.numberOfVisit;
41 | gasesNumberOfVisits +=
42 | page.estimatation_co2?.commentDetails?.numberOfVisit;
43 | perPages.push({
44 | pageName,
45 | ecoIndex: page.ecoIndex,
46 | grade: page.grade,
47 | greenhouseGases: page.greenhouseGasesEmission,
48 | water: page.waterConsumption,
49 | greenhouseGasesKm: page.estimatation_co2?.commentDetails?.value_km ?? 0,
50 | waterShower: page.estimatation_water?.commentDetails?.value_shower ?? 0,
51 | metrics: page.metrics,
52 | waterNumberOfVisits:
53 | page.estimatation_water?.commentDetails?.numberOfVisit,
54 | gasesNumberOfVisits:
55 | page.estimatation_co2?.commentDetails?.numberOfVisit,
56 | });
57 | }
58 | });
59 | if (ecoIndex !== 0) {
60 | ecoIndex = Math.round(ecoIndex / ecoIndexJsonReportsFiles.length);
61 | }
62 | const grade = globalEvalutation(ecoIndex);
63 |
64 | if (options.verbose) {
65 | console.log("global ecoIndex:", ecoIndex);
66 | console.log("global grade:", grade);
67 | console.log("global greenhouse gases:", greenhouseGases);
68 | console.log("global greenhouse water:", water);
69 | console.log("global greenhouse gases in km:", greenhouseGasesKm);
70 | console.log("global greenhouse water:", waterShower);
71 | }
72 | return {
73 | ecoIndex,
74 | grade,
75 | greenhouseGasesKm,
76 | greenhouseGases,
77 | water,
78 | waterShower,
79 | perPages,
80 | gasesNumberOfVisits,
81 | waterNumberOfVisits
82 | };
83 | };
84 | const listFiles = (options) => {
85 | const ecoIndexJsonReportsFiles = [];
86 | const files = fs.readdirSync(options.srcEcoIndex);
87 | files.forEach((file) => {
88 | if (path.extname(file) === ".json") {
89 | if (options.verbose) {
90 | console.log("Add file in list for aggregation: ", file);
91 | }
92 | ecoIndexJsonReportsFiles.push(file);
93 | }
94 | });
95 | return ecoIndexJsonReportsFiles;
96 | };
97 |
98 | const globalEvalutation = (ecoIndex) => {
99 | if (ecoIndex > 75) return "A";
100 | if (ecoIndex > 65) return "B";
101 | if (ecoIndex > 50) return "C";
102 | if (ecoIndex > 35) return "D";
103 | if (ecoIndex > 20) return "E";
104 | if (ecoIndex > 5) return "F";
105 | return "G";
106 | };
107 |
--------------------------------------------------------------------------------
/src/reporters/templates/sheet.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
39 |
--------------------------------------------------------------------------------
/src/reporters/templates/co2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/ecoIndex/aggregatorService.spec.js:
--------------------------------------------------------------------------------
1 | const aggregate = require("./aggregatorService");
2 | const fs = require("fs");
3 | jest.mock("fs");
4 |
5 | describe("AggregatorService", () => {
6 | const inputValue = {
7 | score: 71,
8 | grade: "B",
9 | estimatation_co2: {
10 | comment:
11 | "Pour un total de undefined visites par mois, ceci correspond à NaNkm en voiture (Peugeot 208 5P 1.6 BlueHDi FAP (75ch) BVM5)",
12 | commentDetails: {
13 | value_km: "10",
14 | numberOfVisit: 2000,
15 | },
16 | },
17 | estimatation_water: {
18 | comment:
19 | "Pour un total de undefined visites par mois, ceci correspond à NaN douche",
20 | commentDetails: {
21 | value_shower: "10",
22 | numberOfVisit: 1000,
23 | },
24 | },
25 | pages: [
26 | {
27 | url: "http://localhost:3000",
28 | ecoIndex: 71,
29 | grade: "B",
30 | greenhouseGasesEmission: 1.58,
31 | waterConsumption: 2.37,
32 | metrics: [
33 | {
34 | name: "number_requests",
35 | value: 9,
36 | status: "info",
37 | recommandation: "< 30 requests",
38 | },
39 | {
40 | name: "page_size",
41 | value: 5243,
42 | },
43 | ],
44 | estimatation_co2: {
45 | comment:
46 | "Pour un total de undefined visites par mois, ceci correspond à NaNkm en voiture (Peugeot 208 5P 1.6 BlueHDi FAP (75ch) BVM5)",
47 | commentDetails: {
48 | value_km: null,
49 | numberOfVisit: 2000,
50 | },
51 | },
52 | estimatation_water: {
53 | comment:
54 | "Pour un total de undefined visites par mois, ceci correspond à NaN douche",
55 | commentDetails: {
56 | value_shower: null,
57 | numberOfVisit: 1000,
58 | },
59 | },
60 | },
61 | ],
62 | };
63 |
64 | it("should return global ecoIndex", async () => {
65 | const options = {
66 | srcEcoIndex: "test",
67 | srcLighthouse: "test",
68 | verbose: true,
69 | };
70 | const output = {
71 | ecoIndex: 71,
72 | grade: "B",
73 | greenhouseGases: 3.16,
74 | greenhouseGasesKm: 0,
75 | waterNumberOfVisits: 2000,
76 | gasesNumberOfVisits: 4000,
77 | water: 4.74,
78 | waterShower: 0,
79 | perPages: [
80 | {
81 | ecoIndex: 71,
82 | grade: "B",
83 | greenhouseGases: 1.58,
84 | greenhouseGasesKm: 0,
85 | waterNumberOfVisits: 1000,
86 | gasesNumberOfVisits: 2000,
87 | metrics: [
88 | {
89 | name: "number_requests",
90 | recommandation: "< 30 requests",
91 | status: "info",
92 | value: 9,
93 | },
94 | {
95 | name: "page_size",
96 | value: 5243,
97 | },
98 | ],
99 | pageName: "foo",
100 | water: 2.37,
101 | waterShower: 0,
102 | },
103 | {
104 | ecoIndex: 71,
105 | grade: "B",
106 | greenhouseGases: 1.58,
107 | greenhouseGasesKm: 0,
108 | waterNumberOfVisits: 1000,
109 | gasesNumberOfVisits: 2000,
110 | metrics: [
111 | {
112 | name: "number_requests",
113 | recommandation: "< 30 requests",
114 | status: "info",
115 | value: 9,
116 | },
117 | {
118 | name: "page_size",
119 | value: 5243,
120 | },
121 | ],
122 | pageName: "bar",
123 | water: 2.37,
124 | waterShower: 0,
125 | },
126 | ],
127 | };
128 | jest.spyOn(fs, "readdirSync").mockImplementation(() => {
129 | return ["foo.json", "bar.json"];
130 | });
131 | jest.spyOn(fs, "existsSync").mockImplementation(() => true);
132 | jest.spyOn(fs, "readFileSync").mockImplementation(() => {
133 | return JSON.stringify(inputValue);
134 | });
135 |
136 | const result = await aggregate(options);
137 | expect(result).toEqual(output);
138 | });
139 | });
140 |
--------------------------------------------------------------------------------
/src/globlalAggregation/aggregatorService.spec.js:
--------------------------------------------------------------------------------
1 | const aggregate = require("./aggregatorService");
2 |
3 | describe("AggregatorService", () => {
4 | it("should return the default output if the array is empty", () => {
5 | const options = {};
6 | const resultsGlobalLighthouse = {};
7 | const resultsGlobalEcoindex = {};
8 |
9 | const output = {
10 | ecoIndex: 0,
11 | grade: "G",
12 | greenhouseGases: 0,
13 | greenhouseGasesKm: 0,
14 | water: 0,
15 | waterShower: 0,
16 | performance: 0,
17 | accessibility: 0,
18 | bestPractices: 0,
19 | globalNote: 0,
20 | waterNumberOfVisits: 0,
21 | gasesNumberOfVisits: 0,
22 | perPages: [],
23 | };
24 |
25 | expect(aggregate(options, resultsGlobalLighthouse, resultsGlobalEcoindex)).toEqual(output);
26 | });
27 |
28 | it("should return the base is lighthouse ", () => {
29 | const options = {verbose: true};
30 | const resultsGlobalLighthouse = {
31 | accessibility: 70,
32 | bestPractices: 70,
33 | performance: 70,
34 | perPages: [
35 | {
36 | pageName: "test1",
37 | lighthouseReport: "reports/test1.html",
38 | accessibility: 70,
39 | bestPractices: 70,
40 | performance: 70,
41 | },
42 | ],
43 | };
44 | const resultsGlobalEcoindex = {
45 | ecoIndex: 86,
46 | grade: "A",
47 | greenhouseGases: 1.56,
48 | water: 2,
49 | metrics: [],
50 | greenhouseGasesKm: 2500,
51 | waterShower: 250,
52 | waterNumberOfVisits: 100,
53 | gasesNumberOfVisits: 100,
54 | perPages: [
55 | {
56 | pageName: "test1",
57 | ecoIndex: 86,
58 | grade: "A",
59 | greenhouseGases: 2500,
60 | water: 2,
61 | metrics: [],
62 | greenhouseGasesKm: 2500,
63 | waterShower: 250,
64 | waterNumberOfVisits: 100,
65 | gasesNumberOfVisits: 100,
66 | },
67 | ],
68 | };
69 |
70 | const output = {
71 | ecoIndex: 86,
72 | globalNote: 74,
73 | grade: "A",
74 | greenhouseGases: 1.56,
75 | greenhouseGasesKm: 2500,
76 | water: 2,
77 | waterShower: 250,
78 | performance: 70,
79 | accessibility: 70,
80 | bestPractices: 70,
81 | waterNumberOfVisits: 100,
82 | gasesNumberOfVisits: 100,
83 | perPages: [
84 | {
85 | pageName: "test1",
86 | lighthouseReport: "reports/test1.html",
87 | accessibility: 70,
88 | bestPractices: 70,
89 | performance: 70,
90 | ecoIndex: 86,
91 | grade: "A",
92 | greenhouseGases: 2500,
93 | water: 2,
94 | metrics: [],
95 | greenhouseGasesKm: 2500,
96 | waterShower: 250,
97 | waterNumberOfVisits: 100,
98 | gasesNumberOfVisits: 100,
99 | },
100 | ],
101 | };
102 | let result = aggregate(options, resultsGlobalLighthouse, resultsGlobalEcoindex);
103 | expect(result).toEqual(output);
104 | });
105 |
106 | it("should return the base is ecoIndex ", () => {
107 | const options = {verbose: true};
108 | const resultsGlobalLighthouse = {
109 | perPages: [],
110 | };
111 | const resultsGlobalEcoindex = {
112 | ecoIndex: 86,
113 | grade: "A",
114 | greenhouseGases: 1.56,
115 | water: 2,
116 | metrics: [],
117 | greenhouseGasesKm: 2500,
118 | waterShower: 250,
119 | waterNumberOfVisits: 100,
120 | gasesNumberOfVisits: 100,
121 | perPages: [
122 | {
123 | pageName: "test1",
124 | ecoIndex: 86,
125 | grade: "A",
126 | greenhouseGases: 2500,
127 | water: 2,
128 | metrics: [],
129 | greenhouseGasesKm: 2500,
130 | waterShower: 250,
131 | waterNumberOfVisits: 100,
132 | gasesNumberOfVisits: 100,
133 | },
134 | ],
135 | };
136 |
137 | const output = {
138 | ecoIndex: 86,
139 | globalNote: 22,
140 | grade: "A",
141 | greenhouseGases: 1.56,
142 | water: 2,
143 | performance: 0,
144 | accessibility: 0,
145 | bestPractices: 0,
146 | greenhouseGasesKm: 2500,
147 | waterShower: 250,
148 | waterNumberOfVisits: 100,
149 | gasesNumberOfVisits: 100,
150 | perPages: [
151 | {
152 | pageName: "test1",
153 | lighthouseReport: "",
154 | accessibility: 0,
155 | bestPractices: 0,
156 | performance: 0,
157 | ecoIndex: 86,
158 | grade: "A",
159 | greenhouseGases: 2500,
160 | greenhouseGasesKm: 2500,
161 | water: 2,
162 | metrics: [],
163 | waterShower: 250,
164 | waterNumberOfVisits: 100,
165 | gasesNumberOfVisits: 100,
166 | },
167 | ],
168 | };
169 | let result = aggregate(options, resultsGlobalLighthouse, resultsGlobalEcoindex);
170 | expect(result).toEqual(output);
171 | });
172 | });
173 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Lighthouse EcoIndex Aggregator
2 |
3 | This generator tool can be used if you need to generate a global reports for all pages audited by **lighthouse** and **ecoindex** tools. After the generation, we will have access to a global HTML report. As an example, you can use this tool at the end of a Cypress test suite in order to generate
4 | the final report of your tests.
5 |
6 | 
7 |
8 | At the end of the readme, we will explain how to generate lighthouse and ecoindex reports, used by this aggregator.
9 |
10 | ## Package
11 |
12 | - lighthouse-eco-index-aggregator [](https://badge.fury.io/js/lighthouse-eco-index-aggregator)
13 |
14 | ```shell
15 | npm install -D lighthouse-eco-index-aggregator
16 | ```
17 |
18 | ## Options
19 |
20 | | Nom | Type | Description |
21 | | ------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
22 | | config | string | Option is used for define configuration file (JSON or JavaScript) |
23 | | fail | number | Option is used for define limit fail |
24 | | h | boolean | Option is used for see informations cli |
25 | | lang | string | Option is used for translated report values possible used is "fr-FR" or "en-GB" default is "en-GB" |
26 | | m | boolean | Option is used for minify file output it's true by default |
27 | | outputPath | string | Option is used in order to define the target folder when the report will be generated |
28 | | pass | number | Option is used for define limit pass |
29 | | reports | string[] | Option is used for defined the format of the generated report. Possible values "html", "sonar" or a funciton |
30 | | sonarFilePath | string | Option is used when generating the sonar report, in order to make the issue visible on SonarCloud |
31 | | srcEcoIndex | string | Option is used for defined ecoIndex reports path |
32 | | srcLighthouse | string | Option is used for defined lighthouse reports path |
33 | | v | boolean | Option is used for verbose task |
34 |
35 | ## Example usage
36 |
37 | ```shell
38 | npx lighthouse-eco-index-aggregator --srcLighthouse="./reports/lighthouse" --srcEcoIndex="./reports/ecoindex" --reports="html" --outputPath="report_final"
39 | ```
40 |
41 | You can also used this module programmatically
42 |
43 | ```js
44 | const aggregate = require("lighthouse-eco-index-aggregator/src/main");
45 |
46 | console.log(
47 | aggregate({
48 | srcLighthouse: "./reports/lighthouse",
49 | srcEcoIndex: "./reports/ecoindex",
50 | outputPath: "report_final",
51 | })
52 | );
53 | ```
54 |
55 | ## How to generate Lighthouse and EcoIndex reports
56 |
57 | This aggregator tool can also be used directly inside a cypress or playwright test. For example, we can generate the global report once the Cypress or Playwright tests suite has finished.
58 |
59 | ### To generate playwright suite tests
60 |
61 | Please take a look here :
62 | [generate playwright suite tests ](playwright-demo/README.md)
63 |
64 | Your playwright test will look like [this](playwright-demo/Tests/welcomeAxaPage.spec.ts)
65 |
66 |
67 | ### To generate cypress suite tests
68 |
69 | ```javascript
70 | // cypress.config.js
71 | const aggregate = require("lighthouse-eco-index-aggregator/src/main");
72 | const path = require("path");
73 |
74 | const lighthouseOutputPathDir = path.join(__dirname, "reports/lighthouse");
75 | const ecoIndexOutputPathDir = path.join(__dirname, "reports/ecoindex");
76 | const globalOutputPathDir = path.join(__dirname, "report_final");
77 |
78 | module.exports = defineConfig({
79 | e2e: {
80 | setupNodeEvents(on, config) {
81 | on("after:run", async () => {
82 | await aggregate({
83 | reports: ["html"],
84 | verbose: true,
85 | srcLighthouse: lighthouseOutputPathDir,
86 | srcEcoIndex: ecoIndexOutputPathDir,
87 | outputPath: globalOutputPathDir,
88 | });
89 | });
90 | },
91 | },
92 | });
93 | ```
94 |
95 | But in order to generate this global report, we need the lighthouse and ecoindex reports available in the `lighthouse` and `ecoindex` subfolders. In order to do so, we will use two extra NPM packages :
96 |
97 | - [Cypress Audit](https://github.com/mfrachet/cypress-audit) in order to run Lighthouse
98 | - [Eco Index Audit](https://github.com/EmmanuelDemey/eco-index-audit) in order to run EcoIndex.
99 |
100 | You will find a sample Cypress tests suite in the `cypress-demo` folder. Please have a look to the `demo.cy.js` and `cypress.config.js`.
101 |
102 | In order to run the Cypress tests suite, you have to execute the following commands :
103 |
104 | ```shell
105 | cd cypress-demo
106 | npm i
107 | npx cypress run -b chrome
108 | ```
109 |
110 | ## Sonar report
111 |
112 | This tool can also generate a external sonar report you can add to the Sonar configuration (via the `sonar.externalIssuesReportPaths` option).
113 |
114 | You need to define the path to one of your file managed by Sonar, in order to make the rule visible in Sonar Cloud and use the `sonar` reporter.
115 |
116 | ```bash
117 | node ./src/cli.js --srcLighthouse="./reports/lighthouse" --srcEcoIndex="./reports/ecoindex" --reports="sonar" --sonarFilePath="./package.json"
118 | ```
119 |
120 | ## Generate any type of report
121 |
122 | In fact, the `output` option can receive a javaScript function. Thanks to this possibility, you can send the result anywhere (Elastic, DataDog, ...)
123 |
124 | ```javascript
125 | // cypress.config.js
126 | const aggregate = require("lighthouse-eco-index-aggregator/src/main");
127 | const path = require("path");
128 |
129 | const lighthouseOutputPathDir = path.join(__dirname, "reports/lighthouse");
130 | const ecoIndexOutputPathDir = path.join(__dirname, "reports/ecoindex");
131 | const globalOutputPathDir = path.join(__dirname, "report_final");
132 |
133 | module.exports = defineConfig({
134 | e2e: {
135 | setupNodeEvents(on, config) {
136 | on("after:run", async () => {
137 | await aggregate({
138 | reports: [
139 | "html",
140 | (options, results) => {
141 | const { Client } = require("@elastic/elasticsearch");
142 | const client = new Client();
143 | return client.index({
144 | index: "lighthouse-ecoindex",
145 | document: {
146 | ...results,
147 | "@timestamp": new Date(),
148 | },
149 | });
150 | },
151 | ],
152 | verbose: true,
153 | srcLighthouse: lighthouseOutputPathDir,
154 | srcEcoIndex: ecoIndexOutputPathDir,
155 | outputPath: globalOutputPathDir,
156 | });
157 | });
158 | },
159 | },
160 | });
161 | ```
162 |
--------------------------------------------------------------------------------
/src/reporters/generatorReports.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const path = require("path");
3 | const folderTranslate = "translate";
4 | const minify = require("html-minifier").minify;
5 | const fse = require("fs-extra");
6 | const defaultLang = "en-GB";
7 | const {
8 | globalNoteTag,
9 | globalPerformanceTag,
10 | globalAccessibilityTag,
11 | globalBestPracticesTag,
12 | globalEcoIndexTag,
13 | performanceBlock,
14 | accessibilityBlock,
15 | bestPracticesBlock,
16 | ecoIndexBlock,
17 | htmlPerPageBlock,
18 | } = require("./globalTag");
19 |
20 | const {
21 | PageSizeTag,
22 | PageSizeRecommendationTag,
23 | PageComplexityTag,
24 | PageComplexityRecommendationTag,
25 | lighthouseReportPathTag,
26 | NumberOfRequestTag,
27 | NumberOfRequestRecommendationTag,
28 | greenItMetricsBlock,
29 | pageMetricsBlock,
30 | IconPerPageTag,
31 | numberPageTag,
32 | pageNameTag,
33 | statusClassPerPageTag,
34 | } = require("./pageTag");
35 | const ejs = require("ejs");
36 | const { readTemplate } = require("./readTemplate");
37 | const computeCssClassForMetrics = require("./utils/computeCssClassForMetrics");
38 | const pageInErrorOrWarning = require("./utils/displayPageErrorIcon");
39 | const { statusGreen } = require("./utils/statusGreen");
40 | const { statusPerPage } = require("./utils/statusPerPage");
41 | const { basename } = require("path");
42 |
43 | const generateReportsSonar = async (options, results) => {
44 | if (!options.sonarFilePath) {
45 | console.error("You should define the sonarFilePath property");
46 | process.exit(1);
47 | }
48 |
49 | const issues = [];
50 |
51 | const addIssues = (value, ruleId, name) => {
52 | if (options.fail > value) {
53 | issues.push({
54 | engineId: "eco-index",
55 | ruleId,
56 | severity: "MAJOR",
57 | type: "BUG",
58 | primaryLocation: {
59 | message: `You ${name} (${value}) is below the configured threshold (${options.fail})`,
60 | filePath: options.sonarFilePath,
61 | },
62 | });
63 | } else {
64 | if (options.fail <= value && value < options.pass) {
65 | issues.push({
66 | engineId: "eco-index",
67 | ruleId,
68 | severity: "MINOR",
69 | type: "BUG",
70 | primaryLocation: {
71 | message: `You ${name} (${value}) is below the configured threshold (${options.pass})`,
72 | filePath: options.sonarFilePath,
73 | },
74 | });
75 | }
76 | }
77 | };
78 |
79 | addIssues(results.ecoIndex, "eco-index-below-threshold", "ecoindex");
80 | addIssues(results.performance, "performance-below-threshold", "performance");
81 | addIssues(
82 | results.accessibility,
83 | "accessibility-below-threshold",
84 | "accessibility"
85 | );
86 | addIssues(
87 | results.bestPractices,
88 | "bestPractices-below-threshold",
89 | "bestPractices"
90 | );
91 |
92 | fs.writeFileSync(
93 | path.join(options.outputPath, "report.json"),
94 | JSON.stringify({ issues })
95 | );
96 | };
97 |
98 | const generateReports = async (options, results) => {
99 | if (!options?.pass) {
100 | options.pass = 90;
101 | }
102 |
103 | if (!options?.fail) {
104 | options.fail = 30;
105 | }
106 |
107 | if (options?.verbose) {
108 | console.log("Generate reports html.");
109 | }
110 | if (!options.lang) {
111 | options.lang = defaultLang;
112 | }
113 |
114 | options.translations = populateTranslation(options);
115 |
116 | if (options.srcLighthouse) {
117 | const finalSrcLighthouse =
118 | options.outputPath + "/" + basename(options.srcLighthouse);
119 | fse.copySync(options.srcLighthouse, finalSrcLighthouse, {
120 | overwrite: true,
121 | });
122 | options.srcLighthouse = finalSrcLighthouse;
123 | }
124 | if (options.srcEcoIndex) {
125 | const finalSrcEcoIndex =
126 | options.outputPath + "/" + basename(options.srcEcoIndex);
127 | fse.copySync(options.srcEcoIndex, finalSrcEcoIndex, { overwrite: true });
128 | options.srcEcoIndex = finalSrcEcoIndex;
129 | }
130 |
131 | const htmlPerPageResult = await populateTemplatePerPage(options, results);
132 | let htmlResult = await populateTemplate(options, results, htmlPerPageResult);
133 |
134 | const minifyOptions = {
135 | includeAutoGeneratedTags: true,
136 | removeAttributeQuotes: true,
137 | removeComments: true,
138 | removeRedundantAttributes: true,
139 | removeScriptTypeAttributes: true,
140 | removeStyleLinkTypeAttributes: true,
141 | sortClassName: true,
142 | useShortDoctype: true,
143 | collapseWhitespace: true,
144 | minifyCSS: true,
145 | };
146 |
147 | if (options?.minify ?? true) {
148 | htmlResult = minify(htmlResult, minifyOptions);
149 | }
150 |
151 | fs.writeFileSync(path.join(options.outputPath, "report.html"), htmlResult);
152 | };
153 |
154 | const populateTemplate = async (options, results, htmlPerPageResult) => {
155 | const template = readTemplate("template.html");
156 |
157 | const performanceBlockTemplate = populateTemplatePerformance(
158 | options,
159 | results.performance,
160 | "global"
161 | );
162 | const accessibilityBlockTemplate = populateTemplateAccessibility(
163 | options,
164 | results.accessibility,
165 | "global"
166 | );
167 | const bestPracticesBlockTemplate = populateTemplateBestPractices(
168 | options,
169 | results.bestPractices,
170 | "global"
171 | );
172 | const ecoIndexBlockTemplate = populateTemplateEcoIndex(
173 | options,
174 | results.ecoIndex,
175 | "global"
176 | );
177 |
178 | const GlobalGreenItMetricsTemplate = populateGreentItMetrics(options, {
179 | greenhouseGases: results.greenhouseGases,
180 | greenhouseGasesKm: results.greenhouseGasesKm,
181 | water: results.water,
182 | waterShower: results.waterShower,
183 | waterNumberOfVisits: results.waterNumberOfVisits,
184 | gasesNumberOfVisits: results.gasesNumberOfVisits,
185 | });
186 |
187 | return ejs.render(template, {
188 | [globalNoteTag]: statusGreen(results.globalNote, options),
189 | [globalPerformanceTag]: performanceBlockTemplate,
190 | [globalAccessibilityTag]: accessibilityBlockTemplate,
191 | [globalEcoIndexTag]: ecoIndexBlockTemplate,
192 | [globalBestPracticesTag]: bestPracticesBlockTemplate,
193 | [htmlPerPageBlock]: htmlPerPageResult,
194 | GlobalGreenItMetrics: GlobalGreenItMetricsTemplate,
195 | Translations: options.translations,
196 | style: readTemplate("./style.css"),
197 | lang: options.lang,
198 | });
199 | };
200 |
201 | const populateMetrics = (options, metric) => {
202 | if (options?.verbose) {
203 | console.log("Populate metrics:", metric);
204 | }
205 | const template = readTemplate("templatePageMetrics.html");
206 | const NumberOfRequestMetric =
207 | metric?.find(m => m.name === "number_requests") ?? {};
208 | const PageSizeMetric = metric?.find(m => m.name === "page_size") ?? {};
209 | const PageComplexityMetric =
210 | metric?.find(m => m.name === "Page_complexity") ?? {};
211 |
212 | return ejs.render(template, {
213 | Translations: options.translations,
214 | [NumberOfRequestTag]: NumberOfRequestMetric.value,
215 | [NumberOfRequestRecommendationTag]: NumberOfRequestMetric.recommandation,
216 | NumberOfRequestCssClass: computeCssClassForMetrics(NumberOfRequestMetric),
217 | [PageSizeTag]: PageSizeMetric.value,
218 | [PageSizeRecommendationTag]: PageSizeMetric.recommandation,
219 | PageSizeCssClass: computeCssClassForMetrics(PageSizeMetric),
220 | [PageComplexityTag]: PageComplexityMetric.value,
221 | [PageComplexityRecommendationTag]: PageComplexityMetric.recommandation,
222 | PageComplexityCssClass: computeCssClassForMetrics(PageComplexityMetric),
223 | });
224 | };
225 |
226 | const populateGreentItMetrics = (
227 | options,
228 | {
229 | greenhouseGases,
230 | greenhouseGasesKm,
231 | water,
232 | waterShower,
233 | gasesNumberOfVisits,
234 | waterNumberOfVisits,
235 | }
236 | ) => {
237 | if (options?.verbose) {
238 | console.log("Populate GreenIt metrics:", {
239 | greenhouseGases,
240 | greenhouseGasesKm,
241 | water,
242 | waterShower,
243 | gasesNumberOfVisits,
244 | waterNumberOfVisits,
245 | });
246 | }
247 |
248 | const template = readTemplate("templateGreenItMetrics.html");
249 | const svgIconCo2 = readTemplate("co2.svg");
250 | const svgIconWater = readTemplate("water.svg");
251 | return ejs.render(template, {
252 | Translations: options.translations,
253 | greenhouseGases,
254 | greenhouseGasesKm,
255 | water,
256 | waterShower,
257 | gasesNumberOfVisits,
258 | waterNumberOfVisits,
259 | Translations: options.translations,
260 | mySvg: { svgIconCo2: svgIconCo2, svgIconWater: svgIconWater },
261 | gasesNumberOfVisits,
262 | waterNumberOfVisits,
263 | Translations: options.translations,
264 | mySvg: { svgIconCo2: svgIconCo2, svgIconWater: svgIconWater },
265 | });
266 | };
267 |
268 | const populateTemplatePerPage = async (options, results) => {
269 | let htmlPerPage = "";
270 | const defaultTemplatePerPage = readTemplate("templatePerPage.html");
271 | let numberPage = 0;
272 | results.perPages.forEach(page => {
273 | numberPage += 1;
274 | if (options?.verbose) {
275 | console.log("Populate reports page:", numberPage);
276 | }
277 |
278 | const performanceBlockTemplate = populateTemplatePerformance(
279 | options,
280 | page.performance,
281 | numberPage
282 | );
283 | const accessibilityBlockTemplate = populateTemplateAccessibility(
284 | options,
285 | page.accessibility,
286 | numberPage
287 | );
288 | const bestPracticesBlockTemplate = populateTemplateBestPractices(
289 | options,
290 | page.bestPractices,
291 | numberPage
292 | );
293 | const ecoIndexBlockTemplate = populateTemplateEcoIndex(
294 | options,
295 | page.ecoIndex,
296 | numberPage
297 | );
298 | const metricsTemplate = populateMetrics(options, page.metrics);
299 | const greenItMetricsTemplate = populateGreentItMetrics(options, {
300 | greenhouseGasesKm: page.greenhouseGasesKm,
301 | waterShower: page.waterShower,
302 | greenhouseGases: page.greenhouseGases,
303 | water: page.water,
304 | gasesNumberOfVisits:
305 | page.estimatation_water?.commentDetails?.numberOfVisit,
306 | waterNumberOfVisits: page.estimatation_co2?.commentDetails?.numberOfVisit,
307 | });
308 |
309 | const templatePerPage = ejs.render(defaultTemplatePerPage, {
310 | Translations: options.translations,
311 | [performanceBlock]: performanceBlockTemplate,
312 | [accessibilityBlock]: accessibilityBlockTemplate,
313 | [bestPracticesBlock]: bestPracticesBlockTemplate,
314 | [ecoIndexBlock]: ecoIndexBlockTemplate,
315 | [pageMetricsBlock]: metricsTemplate,
316 | [greenItMetricsBlock]: greenItMetricsTemplate,
317 | [numberPageTag]: numberPage,
318 | [pageNameTag]: page.pageName,
319 | [lighthouseReportPathTag]: page.lighthouseReport,
320 | [IconPerPageTag]: pageInErrorOrWarning(page, options),
321 | [statusClassPerPageTag]: statusPerPage(page, options),
322 | });
323 | htmlPerPage += templatePerPage;
324 | });
325 | return htmlPerPage;
326 | };
327 |
328 | const populateDoughnut = (value, label, options) => {
329 | const template = readTemplate("templateDoughnut.html");
330 | return ejs.render(template, {
331 | Class: generateCSSClassBasedOnValue(value, options),
332 | Value: value,
333 | Label: label,
334 | });
335 | };
336 |
337 | const populateTemplatePerformance = (options, performance, numberPage) => {
338 | if (options?.verbose) {
339 | console.log(
340 | `populate performance with value:${performance} for page ${numberPage}`
341 | );
342 | }
343 | return populateDoughnut(
344 | performance,
345 | options.translations.LabelPerformance,
346 | options
347 | );
348 | };
349 |
350 | const populateTemplateAccessibility = (options, accessibility, numberPage) => {
351 | if (options?.verbose) {
352 | console.log(
353 | `populate accessibility with value: ${accessibility} for page ${numberPage}`
354 | );
355 | }
356 | return populateDoughnut(
357 | accessibility,
358 | options.translations.LabelAccessibility,
359 | options
360 | );
361 | };
362 |
363 | const populateTemplateBestPractices = (options, bestPractices, numberPage) => {
364 | if (options?.verbose) {
365 | console.log(
366 | `populate bestPractices with value ${bestPractices} for page ${numberPage}`
367 | );
368 | }
369 | return populateDoughnut(
370 | bestPractices,
371 | options.translations.LabelBestPractices,
372 | options
373 | );
374 | };
375 |
376 | const populateTemplateEcoIndex = (options, ecoIndex, numberPage) => {
377 | if (options?.verbose) {
378 | console.log(
379 | `populate ecoIndex with value: ${ecoIndex} for page: ${numberPage}`
380 | );
381 | }
382 | return populateDoughnut(
383 | ecoIndex,
384 | options.translations.LabelEcoIndex,
385 | options
386 | );
387 | };
388 |
389 | const populateTranslation = options => {
390 | const i18nFile = `${options.lang}.json`;
391 |
392 | if (options?.verbose) {
393 | console.log("Translate by files:", i18nFile);
394 | }
395 | const templatePath = path.join(__dirname, folderTranslate, i18nFile);
396 | if (fs.existsSync(templatePath)) {
397 | return require(templatePath);
398 | }
399 |
400 | if (options?.verbose) {
401 | console.log(`The file ${i18nFile} does not exist. We will use the default one.`);
402 | }
403 |
404 | return populateTranslation({ ...options, lang: defaultLang});
405 | };
406 |
407 | const generateCSSClassBasedOnValue = (value, { pass, fail }) => {
408 | const cssPassClass = "lh-gauge__wrapper--pass";
409 | const cssAverageClass = "lh-gauge__wrapper--average";
410 | const cssFailClass = "lh-gauge__wrapper--fail";
411 | const cssNotApplicableClass = "lh-gauge__wrapper--not-applicable";
412 |
413 | if (value >= pass) return cssPassClass;
414 | else if (value < pass && value >= fail) return cssAverageClass;
415 | else if (value < fail) return cssFailClass;
416 | return cssNotApplicableClass;
417 | };
418 |
419 | module.exports = {
420 | generateReports,
421 | generateReportsSonar,
422 | populateTemplatePerformance,
423 | populateTemplateAccessibility,
424 | populateTemplateBestPractices,
425 | populateTemplateEcoIndex,
426 | };
427 |
--------------------------------------------------------------------------------
/src/reporters/utils/__snapshots__/statusGreen.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`statusGreen should one sheet 1`] = `
4 | "
5 |
6 |
7 |
8 |
44 |
45 |
46 |
47 |
48 |
49 |
85 |
86 |
87 |
88 |
89 |
90 |
126 |
127 |
128 |
"
129 | `;
130 |
131 | exports[`statusGreen should three sheet 1`] = `
132 | "
133 |
134 |
135 |
136 |
172 |
173 |
174 |
175 |
176 |
177 |
213 |
214 |
215 |
216 |
217 |
218 |
254 |
255 |
256 |
"
257 | `;
258 |
259 | exports[`statusGreen should two sheet 1`] = `
260 | "
261 |
262 |
263 |
264 |
300 |
301 |
302 |
303 |
304 |
305 |
341 |
342 |
343 |
344 |
345 |
346 |
382 |
383 |
384 |
"
385 | `;
386 |
--------------------------------------------------------------------------------
/src/reporters/templates/style.css:
--------------------------------------------------------------------------------
1 | .lh-vars {
2 | /* Palette using Material Design Colors
3 | * https://www.materialui.co/colors */
4 | --color-amber-50: #fff8e1;
5 | --color-blue-200: #90caf9;
6 | --color-blue-900: #0d47a1;
7 | --color-blue-A700: #2962ff;
8 | --color-blue-primary: #06f;
9 | --color-cyan-500: #00bcd4;
10 | --color-gray-100: #f5f5f5;
11 | --color-gray-300: #cfcfcf;
12 | --color-gray-200: #e0e0e0;
13 | --color-gray-400: #bdbdbd;
14 | --color-gray-50: #fafafa;
15 | --color-gray-500: #9e9e9e;
16 | --color-gray-600: #757575;
17 | --color-gray-700: #616161;
18 | --color-gray-800: #424242;
19 | --color-gray-900: #212121;
20 | --color-gray: #000000;
21 | --color-green-700: #080;
22 | --color-green: #0c6;
23 | --color-lime-400: #d3e156;
24 | --color-orange-50: #fff3e0;
25 | --color-orange-700: #c33300;
26 | --color-orange: #fa3;
27 | --color-red-700: #c00;
28 | --color-red: #f33;
29 | --color-teal-600: #00897b;
30 | --color-white: #ffffff;
31 | /* Context-specific colors */
32 | --color-average-secondary: var(--color-orange-700);
33 | --color-average: var(--color-orange);
34 | --color-fail-secondary: var(--color-red-700);
35 | --color-fail: var(--color-red);
36 | --color-hover: var(--color-gray-50);
37 | --color-informative: var(--color-blue-900);
38 | --color-pass-secondary: var(--color-green-700);
39 | --color-pass: var(--color-green);
40 | --color-not-applicable: var(--color-gray-600);
41 | /* Component variables */
42 | --audit-description-padding-left: calc(var(--score-icon-size) + var(--score-icon-margin-left) + var(--score-icon-margin-right));
43 | --audit-explanation-line-height: 16px;
44 | --audit-group-margin-bottom: calc(var(--default-padding) * 6);
45 | --audit-group-padding-vertical: 8px;
46 | --audit-margin-horizontal: 5px;
47 | --audit-padding-vertical: 8px;
48 | --category-padding: calc(var(--default-padding) * 6) var(--edge-gap-padding) calc(var(--default-padding) * 4);
49 | --chevron-line-stroke: var(--color-gray-600);
50 | --chevron-size: 12px;
51 | --default-padding: 8px;
52 | --edge-gap-padding: calc(var(--default-padding) * 4);
53 | --env-item-background-color: var(--color-gray-100);
54 | --env-item-font-size: 28px;
55 | --env-item-line-height: 36px;
56 | --env-item-padding: 10px 0px;
57 | --env-name-min-width: 220px;
58 | --footer-padding-vertical: 16px;
59 | --gauge-circle-size-big: 96px;
60 | --gauge-circle-size: 96px;
61 | --gauge-circle-size-sm: 32px;
62 | --gauge-label-font-size-big: 18px;
63 | --gauge-label-font-size: var(--report-font-size-secondary);
64 | --gauge-label-line-height-big: 24px;
65 | --gauge-label-line-height: var(--report-line-height-secondary);
66 | --gauge-percentage-font-size-big: 38px;
67 | --gauge-percentage-font-size: var(--report-font-size-secondary);
68 | --gauge-wrapper-width: 120px;
69 | --header-line-height: 24px;
70 | --highlighter-background-color: var(--report-text-color);
71 | --icon-square-size: calc(var(--score-icon-size) * 0.88);
72 | --image-preview-size: 48px;
73 | --link-color: var(--color-blue-primary);
74 | --locale-selector-background-color: var(--color-white);
75 | --metric-toggle-lines-fill: #7f7f7f;
76 | --metric-value-font-size: calc(var(--report-font-size) * 1.8);
77 | --metrics-toggle-background-color: var(--color-gray-200);
78 | --plugin-badge-background-color: var(--color-white);
79 | --plugin-badge-size-big: calc(var(--gauge-circle-size-big) / 2.7);
80 | --plugin-badge-size: calc(var(--gauge-circle-size) / 2.7);
81 | --plugin-icon-size: 65%;
82 | --pwa-icon-margin: 0 var(--default-padding);
83 | --pwa-icon-size: var(--topbar-logo-size);
84 | --report-background-color: #fff;
85 | --report-border-color-secondary: #ebebeb;
86 | --report-font-family-monospace: "Roboto Mono", "Menlo",
87 | "dejavu sans mono", "Consolas", "Lucida Console", monospace;
88 | --report-font-family: Roboto, Helvetica, Arial, sans-serif;
89 | --report-font-size: 14px;
90 | --report-font-size-secondary: 12px;
91 | --report-icon-size: var(--score-icon-background-size);
92 | --report-line-height: 24px;
93 | --report-line-height-secondary: 20px;
94 | --report-monospace-font-size: calc(var(--report-font-size) * 0.85);
95 | --report-text-color-secondary: var(--color-gray-800);
96 | --report-text-color: var(--color-gray-900);
97 | --report-content-max-width: calc(60 * var(--report-font-size));
98 | /* defaults to 840px */
99 | --report-content-min-width: 360px;
100 | --report-content-max-width-minus-edge-gap: calc(var(--report-content-max-width) - var(--edge-gap-padding) * 2);
101 | --score-container-padding: 8px;
102 | --score-icon-background-size: 24px;
103 | --score-icon-margin-left: 6px;
104 | --score-icon-margin-right: 14px;
105 | --score-icon-margin: 0 var(--score-icon-margin-right) 0 var(--score-icon-margin-left);
106 | --score-icon-size: 12px;
107 | --score-icon-size-big: 16px;
108 | --screenshot-overlay-background: rgba(0, 0, 0, 0.3);
109 | --section-padding-vertical: calc(var(--default-padding) * 6);
110 | --snippet-background-color: var(--color-gray-50);
111 | --snippet-color: #0938c2;
112 | --sparkline-height: 5px;
113 | --stackpack-padding-horizontal: 10px;
114 | --sticky-header-background-color: var(--report-background-color);
115 | --table-higlight-background-color: hsla(210, 17%, 77%, 0.1);
116 | --tools-icon-color: var(--color-gray-600);
117 | --topbar-background-color: var(--color-white);
118 | --topbar-height: 32px;
119 | --topbar-logo-size: 24px;
120 | --topbar-padding: 0 8px;
121 | --toplevel-warning-background-color: hsla(30, 100%, 75%, 10%);
122 | --toplevel-warning-message-text-color: var(--color-average-secondary);
123 | --toplevel-warning-padding: 18px;
124 | --toplevel-warning-text-color: var(--report-text-color);
125 | /* SVGs */
126 | --plugin-icon-url-dark: url('data:image/svg+xml;utf8,');
127 | --plugin-icon-url: url('data:image/svg+xml;utf8,');
128 | --pass-icon-url: url('data:image/svg+xml;utf8,');
129 | --average-icon-url: url('data:image/svg+xml;utf8,');
130 | --fail-icon-url: url('data:image/svg+xml;utf8,');
131 | --pwa-installable-gray-url: url('data:image/svg+xml;utf8,');
132 | --pwa-optimized-gray-url: url('data:image/svg+xml;utf8,');
133 | --pwa-installable-gray-url-dark: url('data:image/svg+xml;utf8,');
134 | --pwa-optimized-gray-url-dark: url('data:image/svg+xml;utf8,');
135 | --pwa-installable-color-url: url('data:image/svg+xml;utf8,');
136 | --pwa-optimized-color-url: url('data:image/svg+xml;utf8,');
137 | --swap-locale-icon-url: url('data:image/svg+xml;utf8,');
138 | }
139 |
140 | .lh-footer {
141 | padding: var(--footer-padding-vertical) calc(var(--default-padding) * 2);
142 | max-width: var(--report-content-max-width);
143 | margin: 0 auto;
144 | }
145 |
146 | .lh-footer .lh-generated {
147 | text-align: center;
148 | }
149 |
150 | @media not print {
151 | .lh-dark {
152 | /* Pallete */
153 | --color-gray-200: var(--color-gray-800);
154 | --color-gray-300: #616161;
155 | --color-gray-400: var(--color-gray-600);
156 | --color-gray-700: var(--color-gray-400);
157 | --color-gray-50: #757575;
158 | --color-gray-600: var(--color-gray-500);
159 | --color-green-700: var(--color-green);
160 | --color-orange-700: var(--color-orange);
161 | --color-red-700: var(--color-red);
162 | --color-teal-600: var(--color-cyan-500);
163 | /* Context-specific colors */
164 | --color-hover: rgba(0, 0, 0, 0.2);
165 | --color-informative: var(--color-blue-200);
166 | /* Component variables */
167 | --env-item-background-color: #393535;
168 | --link-color: var(--color-blue-200);
169 | --locale-selector-background-color: var(--color-gray-200);
170 | --plugin-badge-background-color: var(--color-gray-800);
171 | --report-background-color: var(--color-gray-900);
172 | --report-border-color-secondary: var(--color-gray-200);
173 | --report-text-color-secondary: var(--color-gray-400);
174 | --report-text-color: var(--color-gray-100);
175 | --snippet-color: var(--color-cyan-500);
176 | --topbar-background-color: var(--color-gray);
177 | --toplevel-warning-background-color: hsl(33deg 14% 18%);
178 | --toplevel-warning-message-text-color: var(--color-orange-700);
179 | --toplevel-warning-text-color: var(--color-gray-100);
180 | /* SVGs */
181 | --plugin-icon-url: var(--plugin-icon-url-dark);
182 | --pwa-installable-gray-url: var(--pwa-installable-gray-url-dark);
183 | --pwa-optimized-gray-url: var(--pwa-optimized-gray-url-dark);
184 | }
185 | }
186 |
187 | @media only screen and (max-width: 480px) {
188 | .lh-vars {
189 | --audit-group-margin-bottom: 20px;
190 | --edge-gap-padding: var(--default-padding);
191 | --env-name-min-width: 120px;
192 | --gauge-circle-size-big: 96px;
193 | --gauge-circle-size: 72px;
194 | --gauge-label-font-size-big: 22px;
195 | --gauge-label-font-size: 14px;
196 | --gauge-label-line-height-big: 26px;
197 | --gauge-label-line-height: 20px;
198 | --gauge-percentage-font-size-big: 34px;
199 | --gauge-percentage-font-size: 26px;
200 | --gauge-wrapper-width: 112px;
201 | --header-padding: 16px 0 16px 0;
202 | --image-preview-size: 24px;
203 | --plugin-icon-size: 75%;
204 | --pwa-icon-margin: 0 7px 0 -3px;
205 | --report-font-size: 14px;
206 | --report-line-height: 20px;
207 | --score-icon-margin-left: 2px;
208 | --score-icon-size: 10px;
209 | --topbar-height: 28px;
210 | --topbar-logo-size: 20px;
211 | }
212 |
213 | /* Not enough space to adequately show the relative savings bars. */
214 | .lh-sparkline {
215 | display: none;
216 | }
217 | }
218 |
219 | .lh-vars.lh-devtools {
220 | --audit-explanation-line-height: 14px;
221 | --audit-group-margin-bottom: 20px;
222 | --audit-group-padding-vertical: 12px;
223 | --audit-padding-vertical: 4px;
224 | --category-padding: 12px;
225 | --default-padding: 12px;
226 | --env-name-min-width: 120px;
227 | --footer-padding-vertical: 8px;
228 | --gauge-circle-size-big: 72px;
229 | --gauge-circle-size: 64px;
230 | --gauge-label-font-size-big: 22px;
231 | --gauge-label-font-size: 14px;
232 | --gauge-label-line-height-big: 26px;
233 | --gauge-label-line-height: 20px;
234 | --gauge-percentage-font-size-big: 34px;
235 | --gauge-percentage-font-size: 26px;
236 | --gauge-wrapper-width: 97px;
237 | --header-line-height: 20px;
238 | --header-padding: 16px 0 16px 0;
239 | --screenshot-overlay-background: transparent;
240 | --plugin-icon-size: 75%;
241 | --pwa-icon-margin: 0 7px 0 -3px;
242 | --report-font-family-monospace: "Menlo", "dejavu sans mono",
243 | "Consolas", "Lucida Console", monospace;
244 | --report-font-family: ".SFNSDisplay-Regular", "Helvetica Neue",
245 | "Lucida Grande", sans-serif;
246 | --report-font-size: 12px;
247 | --report-line-height: 20px;
248 | --score-icon-margin-left: 2px;
249 | --score-icon-size: 10px;
250 | --section-padding-vertical: 8px;
251 | }
252 |
253 | .lh-devtools.lh-root {
254 | height: 100%;
255 | }
256 |
257 | .lh-devtools.lh-root img {
258 | /* Override devtools default 'min-width: 0' so svg without size in a flexbox isn't collapsed. */
259 | min-width: auto;
260 | }
261 |
262 | .lh-devtools .lh-container {
263 | overflow-y: scroll;
264 | height: calc(100% - var(--topbar-height));
265 | }
266 |
267 | @media print {
268 | .lh-devtools .lh-container {
269 | overflow: unset;
270 | }
271 | }
272 |
273 | .lh-devtools .lh-element-screenshot__overlay {
274 | position: absolute;
275 | }
276 |
277 | @keyframes fadeIn {
278 | 0% {
279 | opacity: 0;
280 | }
281 |
282 | 100% {
283 | opacity: 0.6;
284 | }
285 | }
286 |
287 | .lh-root *,
288 | .lh-root *::before,
289 | .lh-root *::after {
290 | box-sizing: border-box;
291 | }
292 |
293 | .lh-root {
294 | font-family: var(--report-font-family);
295 | font-size: var(--report-font-size);
296 | margin: 0;
297 | line-height: var(--report-line-height);
298 | background: var(--report-background-color);
299 | color: var(--report-text-color);
300 | }
301 |
302 | .lh-root :focus {
303 | outline: -webkit-focus-ring-color auto 3px;
304 | }
305 |
306 | .lh-root summary:focus {
307 | outline: none;
308 | box-shadow: 0 0 0 1px hsl(217, 89%, 61%);
309 | }
310 |
311 | .lh-root [hidden] {
312 | display: none !important;
313 | }
314 |
315 | .lh-root pre {
316 | margin: 0;
317 | }
318 |
319 | .lh-root details>summary {
320 | cursor: pointer;
321 | }
322 |
323 | .lh-hidden {
324 | display: none !important;
325 | }
326 |
327 | .lh-container {
328 | /*
329 | Text wrapping in the report is so much FUN!
330 | We have a `word-break: break-word;` globally here to prevent a few common scenarios, namely
331 | long non-breakable text (usually URLs) found in:
332 | 1. The footer
333 | 2. .lh-node (outerHTML)
334 | 3. .lh-code
335 |
336 | With that sorted, the next challenge is appropriate column sizing and text wrapping inside our
337 | .lh-details tables. Even more fun.
338 | * We don't want table headers ("Potential Savings (ms)") to wrap or their column values, but
339 | we'd be happy for the URL column to wrap if the URLs are particularly long.
340 | * We want the narrow columns to remain narrow, providing the most column width for URL
341 | * We don't want the table to extend past 100% width.
342 | * Long URLs in the URL column can wrap. Util.getURLDisplayName maxes them out at 64 characters,
343 | but they do not get any overflow:ellipsis treatment.
344 | */
345 | word-break: break-word;
346 | }
347 |
348 | .lh-audit-group a,
349 | .lh-category-header__description a,
350 | .lh-audit__description a,
351 | .lh-warnings a,
352 | .lh-footer a,
353 | .lh-table-column--link a {
354 | color: var(--link-color);
355 | }
356 |
357 | .lh-audit__description,
358 | .lh-audit__stackpack {
359 | --inner-audit-padding-right: var(--stackpack-padding-horizontal);
360 | padding-left: var(--audit-description-padding-left);
361 | padding-right: var(--inner-audit-padding-right);
362 | padding-top: 8px;
363 | padding-bottom: 8px;
364 | }
365 |
366 | .lh-details {
367 | margin-top: var(--default-padding);
368 | margin-bottom: var(--default-padding);
369 | margin-left: var(--audit-description-padding-left);
370 | /* whatever the .lh-details side margins are */
371 | width: 100%;
372 | }
373 |
374 | .lh-audit__stackpack {
375 | display: flex;
376 | align-items: center;
377 | }
378 |
379 | .lh-audit__stackpack__img {
380 | max-width: 30px;
381 | margin-right: var(--default-padding);
382 | }
383 |
384 | /* Report header */
385 | .lh-report-icon {
386 | display: flex;
387 | align-items: center;
388 | padding: 10px 12px;
389 | cursor: pointer;
390 | }
391 |
392 | .lh-report-icon[disabled] {
393 | opacity: 0.3;
394 | pointer-events: none;
395 | }
396 |
397 | .lh-report-icon::before {
398 | content: "";
399 | margin: 4px;
400 | background-repeat: no-repeat;
401 | width: var(--report-icon-size);
402 | height: var(--report-icon-size);
403 | opacity: 0.7;
404 | display: inline-block;
405 | vertical-align: middle;
406 | }
407 |
408 | .lh-report-icon:hover::before {
409 | opacity: 1;
410 | }
411 |
412 | .lh-dark .lh-report-icon::before {
413 | filter: invert(1);
414 | }
415 |
416 | .lh-report-icon--print::before {
417 | background-image: url('data:image/svg+xml;utf8,');
418 | }
419 |
420 | .lh-report-icon--copy::before {
421 | background-image: url('data:image/svg+xml;utf8,');
422 | }
423 |
424 | .lh-report-icon--open::before {
425 | background-image: url('data:image/svg+xml;utf8,');
426 | }
427 |
428 | .lh-report-icon--download::before {
429 | background-image: url('data:image/svg+xml;utf8,');
430 | }
431 |
432 | .lh-report-icon--dark::before {
433 | background-image: url('data:image/svg+xml;utf8,');
434 | }
435 |
436 | .lh-report-icon--treemap::before {
437 | background-image: url('data:image/svg+xml;utf8,');
438 | }
439 |
440 | .lh-report-icon--date::before {
441 | background-image: url('data:image/svg+xml;utf8,');
442 | }
443 |
444 | .lh-report-icon--devices::before {
445 | background-image: url('data:image/svg+xml;utf8,');
446 | }
447 |
448 | .lh-report-icon--world::before {
449 | background-image: url('data:image/svg+xml;utf8,');
450 | }
451 |
452 | .lh-report-icon--stopwatch::before {
453 | background-image: url('data:image/svg+xml;utf8,');
454 | }
455 |
456 | .lh-report-icon--networkspeed::before {
457 | background-image: url('data:image/svg+xml;utf8,');
458 | }
459 |
460 | .lh-report-icon--samples-one::before {
461 | background-image: url('data:image/svg+xml;utf8,');
462 | }
463 |
464 | .lh-report-icon--samples-many::before {
465 | background-image: url('data:image/svg+xml;utf8,');
466 | }
467 |
468 | .lh-report-icon--chrome::before {
469 | background-image: url('data:image/svg+xml;utf8,');
470 | }
471 |
472 | .lh-buttons {
473 | display: flex;
474 | flex-wrap: wrap;
475 | margin: var(--default-padding) 0;
476 | }
477 |
478 | .lh-button {
479 | height: 32px;
480 | border: 1px solid var(--report-border-color-secondary);
481 | border-radius: 3px;
482 | color: var(--link-color);
483 | background-color: var(--report-background-color);
484 | margin: 5px;
485 | }
486 |
487 | .lh-button:first-of-type {
488 | margin-left: 0;
489 | }
490 |
491 | /* Node */
492 | .lh-node__snippet {
493 | font-family: var(--report-font-family-monospace);
494 | color: var(--snippet-color);
495 | font-size: var(--report-monospace-font-size);
496 | line-height: 20px;
497 | }
498 |
499 | /* Score */
500 | .lh-audit__score-icon {
501 | width: var(--score-icon-size);
502 | height: var(--score-icon-size);
503 | margin: var(--score-icon-margin);
504 | }
505 |
506 | .lh-audit--pass .lh-audit__display-text {
507 | color: var(--color-pass-secondary);
508 | }
509 |
510 | .lh-audit--pass .lh-audit__score-icon,
511 | .lh-scorescale-range--pass::before {
512 | border-radius: 100%;
513 | background: var(--color-pass);
514 | }
515 |
516 | .lh-audit--average .lh-audit__display-text {
517 | color: var(--color-average-secondary);
518 | }
519 |
520 | .lh-audit--average .lh-audit__score-icon,
521 | .lh-scorescale-range--average::before {
522 | background: var(--color-average);
523 | width: var(--icon-square-size);
524 | height: var(--icon-square-size);
525 | }
526 |
527 | .lh-audit--fail .lh-audit__display-text {
528 | color: var(--color-fail-secondary);
529 | }
530 |
531 | .lh-audit--fail .lh-audit__score-icon,
532 | .lh-audit--error .lh-audit__score-icon,
533 | .lh-scorescale-range--fail::before {
534 | border-left: calc(var(--score-icon-size) / 2) solid transparent;
535 | border-right: calc(var(--score-icon-size) / 2) solid transparent;
536 | border-bottom: var(--score-icon-size) solid var(--color-fail);
537 | }
538 |
539 | .lh-audit--manual .lh-audit__display-text,
540 | .lh-audit--notapplicable .lh-audit__display-text {
541 | color: var(--color-gray-600);
542 | }
543 |
544 | .lh-audit--manual .lh-audit__score-icon,
545 | .lh-audit--notapplicable .lh-audit__score-icon {
546 | border: calc(0.2 * var(--score-icon-size)) solid var(--color-gray-400);
547 | border-radius: 100%;
548 | background: none;
549 | }
550 |
551 | .lh-audit--informative .lh-audit__display-text {
552 | color: var(--color-gray-600);
553 | }
554 |
555 | .lh-audit--informative .lh-audit__score-icon {
556 | border: calc(0.2 * var(--score-icon-size)) solid var(--color-gray-400);
557 | border-radius: 100%;
558 | }
559 |
560 | .lh-audit__description,
561 | .lh-audit__stackpack {
562 | color: var(--report-text-color-secondary);
563 | }
564 |
565 | .lh-audit__adorn {
566 | border: 1px solid slategray;
567 | border-radius: 3px;
568 | margin: 0 3px;
569 | padding: 0 2px;
570 | line-height: 1.1;
571 | display: inline-block;
572 | font-size: 90%;
573 | }
574 |
575 | .lh-category-header__description {
576 | text-align: center;
577 | color: var(--color-gray-700);
578 | margin: 0px auto;
579 | max-width: 400px;
580 | }
581 |
582 | .lh-audit__display-text,
583 | .lh-load-opportunity__sparkline,
584 | .lh-chevron-container {
585 | margin: 0 var(--audit-margin-horizontal);
586 | }
587 |
588 | .lh-chevron-container {
589 | margin-right: 0;
590 | }
591 |
592 | .lh-audit__title-and-text {
593 | flex: 1;
594 | }
595 |
596 | .lh-audit__title-and-text code {
597 | color: var(--snippet-color);
598 | font-size: var(--report-monospace-font-size);
599 | }
600 |
601 | /* Prepend display text with em dash separator. But not in Opportunities. */
602 | .lh-audit__display-text:not(:empty):before {
603 | content: "—";
604 | margin-right: var(--audit-margin-horizontal);
605 | }
606 |
607 | .lh-audit-group.lh-audit-group--load-opportunities .lh-audit__display-text:not(:empty):before {
608 | display: none;
609 | }
610 |
611 | /* Expandable Details (Audit Groups, Audits) */
612 | .lh-audit__header {
613 | display: flex;
614 | align-items: center;
615 | padding: var(--default-padding);
616 | }
617 |
618 | .lh-audit__title {
619 | display: flex;
620 | flex-direction: column;
621 | align-items: start;
622 | padding: var(--default-padding);
623 | }
624 |
625 | .lh-audit__title span {
626 | margin-top: auto;
627 | margin-bottom: auto;
628 |
629 | }
630 |
631 | .lh-audit--load-opportunity .lh-audit__header {
632 | display: block;
633 | }
634 |
635 | .lh-metricfilter {
636 | display: grid;
637 | justify-content: end;
638 | align-items: center;
639 | grid-auto-flow: column;
640 | gap: 4px;
641 | color: var(--color-gray-700);
642 | }
643 |
644 | .lh-metricfilter__radio {
645 | position: absolute;
646 | left: -9999px;
647 | }
648 |
649 | .lh-metricfilter input[type="radio"]:focus-visible+label {
650 | outline: -webkit-focus-ring-color auto 1px;
651 | }
652 |
653 | .lh-metricfilter__label {
654 | display: inline-flex;
655 | padding: 0 4px;
656 | height: 16px;
657 | text-decoration: underline;
658 | align-items: center;
659 | cursor: pointer;
660 | font-size: 90%;
661 | }
662 |
663 | .lh-metricfilter__label--active {
664 | background: var(--color-blue-primary);
665 | color: var(--color-white);
666 | border-radius: 3px;
667 | text-decoration: none;
668 | }
669 |
670 | /* Give the 'All' choice a more muted display */
671 | .lh-metricfilter__label--active[for="metric-All"] {
672 | background-color: var(--color-blue-200) !important;
673 | color: black !important;
674 | }
675 |
676 | .lh-metricfilter__text {
677 | margin-right: 8px;
678 | }
679 |
680 | /* If audits are filtered, hide the itemcount for Passed Audits… */
681 | .lh-category--filtered .lh-audit-group .lh-audit-group__itemcount {
682 | display: none;
683 | }
684 |
685 | .lh-audit__header:hover {
686 | background-color: var(--color-hover);
687 | }
688 |
689 | /* We want to hide the browser's default arrow marker on summary elements. Admittedly, it's complicated. */
690 | .lh-root details>summary {
691 | /* Blink 89+ and Firefox will hide the arrow when display is changed from (new) default of `list-item` to block. https://chromestatus.com/feature/6730096436051968*/
692 | display: block;
693 | }
694 |
695 | /* Safari and Blink <=88 require using the -webkit-details-marker selector */
696 | .lh-root details>summary::-webkit-details-marker {
697 | display: none;
698 | }
699 |
700 | /* Perf Metric */
701 | .lh-metrics-container {
702 | display: grid;
703 | grid-auto-rows: 1fr;
704 | grid-template-columns: 1fr 1fr;
705 | grid-column-gap: var(--report-line-height);
706 | margin-bottom: var(--default-padding);
707 | }
708 |
709 | .lh-metric {
710 | border-top: 1px solid var(--report-border-color-secondary);
711 | }
712 |
713 | .lh-metric:nth-last-child(-n + 2) {
714 | border-bottom: 1px solid var(--report-border-color-secondary);
715 | }
716 |
717 | .lh-metric__innerwrap {
718 | display: grid;
719 | /**
720 | * Icon -- Metric Name
721 | * -- Metric Value
722 | */
723 | grid-template-columns:
724 | calc(var(--score-icon-size) + var(--score-icon-margin-left) + var(--score-icon-margin-right)) 1fr;
725 | align-items: center;
726 | padding: var(--default-padding);
727 | }
728 |
729 | .lh-metric__details {
730 | order: -1;
731 | }
732 |
733 | .lh-metric__title {
734 | flex: 1;
735 | }
736 |
737 | .lh-calclink {
738 | padding-left: calc(1ex / 3);
739 | }
740 |
741 | .lh-metric__description {
742 | display: none;
743 | grid-column-start: 2;
744 | grid-column-end: 4;
745 | color: var(--report-text-color-secondary);
746 | }
747 |
748 | .lh-metric__value {
749 | font-size: var(--metric-value-font-size);
750 | margin: calc(var(--default-padding) / 2) 0;
751 | white-space: nowrap;
752 | /* No wrapping between metric value and the icon */
753 | grid-column-start: 2;
754 | }
755 |
756 | @media screen and (max-width: 535px) {
757 | .lh-metrics-container {
758 | display: block;
759 | }
760 |
761 | .lh-metric {
762 | border-bottom: none !important;
763 | }
764 |
765 | .lh-metric:nth-last-child(1) {
766 | border-bottom: 1px solid var(--report-border-color-secondary) !important;
767 | }
768 |
769 | /* Change the grid to 3 columns for narrow viewport. */
770 | .lh-metric__innerwrap {
771 | /**
772 | * Icon -- Metric Name -- Metric Value
773 | */
774 | grid-template-columns:
775 | calc(var(--score-icon-size) + var(--score-icon-margin-left) + var(--score-icon-margin-right)) 2fr 1fr;
776 | }
777 |
778 | .lh-metric__value {
779 | justify-self: end;
780 | grid-column-start: unset;
781 | }
782 | }
783 |
784 | /* No-JS toggle switch */
785 | /* Keep this selector sync'd w/ `magicSelector` in report-ui-features-test.js */
786 | .lh-metrics-toggle__input:checked~.lh-metrics-container .lh-metric__description {
787 | display: block;
788 | }
789 |
790 | /* TODO get rid of the SVGS and clean up these some more */
791 | .lh-metrics-toggle__input {
792 | opacity: 0;
793 | position: absolute;
794 | right: 0;
795 | top: 0px;
796 | }
797 |
798 | .lh-metrics-toggle__input+div>label>.lh-metrics-toggle__labeltext--hide,
799 | .lh-metrics-toggle__input:checked+div>label>.lh-metrics-toggle__labeltext--show {
800 | display: none;
801 | }
802 |
803 | .lh-metrics-toggle__input:checked+div>label>.lh-metrics-toggle__labeltext--hide {
804 | display: inline;
805 | }
806 |
807 | .lh-metrics-toggle__input:focus+div>label {
808 | outline: -webkit-focus-ring-color auto 3px;
809 | }
810 |
811 | .lh-metrics-toggle__label {
812 | cursor: pointer;
813 | font-size: var(--report-font-size-secondary);
814 | line-height: var(--report-line-height-secondary);
815 | color: var(--color-gray-700);
816 | }
817 |
818 | /* Pushes the metric description toggle button to the right. */
819 | .lh-audit-group--metrics .lh-audit-group__header {
820 | display: flex;
821 | justify-content: space-between;
822 | }
823 |
824 | .lh-metric__icon,
825 | .lh-scorescale-range::before {
826 | content: "";
827 | width: var(--score-icon-size);
828 | height: var(--score-icon-size);
829 | display: inline-block;
830 | margin: var(--score-icon-margin);
831 | }
832 |
833 | .lh-metric--pass .lh-metric__value {
834 | color: var(--color-pass-secondary);
835 | }
836 |
837 | .lh-metric--pass .lh-metric__icon {
838 | border-radius: 100%;
839 | background: var(--color-pass);
840 | }
841 |
842 | .lh-metric--average .lh-metric__value {
843 | color: var(--color-average-secondary);
844 | }
845 |
846 | .lh-metric--average .lh-metric__icon {
847 | background: var(--color-average);
848 | width: var(--icon-square-size);
849 | height: var(--icon-square-size);
850 | }
851 |
852 | .lh-metric--fail .lh-metric__value {
853 | color: var(--color-fail-secondary);
854 | }
855 |
856 | .lh-metric--fail .lh-metric__icon,
857 | .lh-metric--error .lh-metric__icon {
858 | border-left: calc(var(--score-icon-size) / 2) solid transparent;
859 | border-right: calc(var(--score-icon-size) / 2) solid transparent;
860 | border-bottom: var(--score-icon-size) solid var(--color-fail);
861 | }
862 |
863 | .lh-metric--error .lh-metric__value,
864 | .lh-metric--error .lh-metric__description {
865 | color: var(--color-fail-secondary);
866 | }
867 |
868 | /* Perf load opportunity */
869 | .lh-load-opportunity__cols {
870 | display: flex;
871 | align-items: flex-start;
872 | }
873 |
874 | .lh-load-opportunity__header .lh-load-opportunity__col {
875 | color: var(--color-gray-600);
876 | display: unset;
877 | line-height: calc(2.3 * var(--report-font-size));
878 | }
879 |
880 | .lh-load-opportunity__col {
881 | display: flex;
882 | }
883 |
884 | .lh-load-opportunity__col--one {
885 | flex: 5;
886 | align-items: center;
887 | margin-right: 2px;
888 | }
889 |
890 | .lh-load-opportunity__col--two {
891 | flex: 4;
892 | text-align: right;
893 | }
894 |
895 | .lh-audit--load-opportunity .lh-audit__display-text {
896 | text-align: right;
897 | flex: 0 0 calc(3 * var(--report-font-size));
898 | }
899 |
900 | /* Sparkline */
901 | .lh-load-opportunity__sparkline {
902 | flex: 1;
903 | margin-top: calc((var(--report-line-height) - var(--sparkline-height)) / 2);
904 | }
905 |
906 | .lh-sparkline {
907 | height: var(--sparkline-height);
908 | width: 100%;
909 | }
910 |
911 | .lh-sparkline__bar {
912 | height: 100%;
913 | float: right;
914 | }
915 |
916 | .lh-audit--pass .lh-sparkline__bar {
917 | background: var(--color-pass);
918 | }
919 |
920 | .lh-audit--average .lh-sparkline__bar {
921 | background: var(--color-average);
922 | }
923 |
924 | .lh-audit--fail .lh-sparkline__bar {
925 | background: var(--color-fail);
926 | }
927 |
928 | /* Filmstrip */
929 | .lh-filmstrip-container {
930 | /* smaller gap between metrics and filmstrip */
931 | margin: -8px auto 0 auto;
932 | }
933 |
934 | .lh-filmstrip {
935 | display: grid;
936 | justify-content: space-between;
937 | padding-bottom: var(--default-padding);
938 | width: 100%;
939 | grid-template-columns: repeat(auto-fit, 60px);
940 | }
941 |
942 | .lh-filmstrip__frame {
943 | text-align: right;
944 | position: relative;
945 | }
946 |
947 | .lh-filmstrip__thumbnail {
948 | border: 1px solid var(--report-border-color-secondary);
949 | max-height: 100px;
950 | max-width: 60px;
951 | }
952 |
953 | /* Audit */
954 | .lh-audit {
955 | border-bottom: 1px solid var(--report-border-color-secondary);
956 | }
957 |
958 | /* Apply border-top to just the first audit. */
959 | .lh-audit {
960 | border-top: 1px solid var(--report-border-color-secondary);
961 | }
962 |
963 | .lh-audit~.lh-audit {
964 | border-top: none;
965 | }
966 |
967 | .lh-audit--error .lh-audit__display-text {
968 | color: var(--color-fail-secondary);
969 | }
970 |
971 | /* Audit Group */
972 | .lh-audit-group {
973 | margin-bottom: var(--audit-group-margin-bottom);
974 | position: relative;
975 | }
976 |
977 | .lh-audit-group--metrics {
978 | margin-bottom: calc(var(--audit-group-margin-bottom) / 2);
979 | }
980 |
981 | .lh-audit-group__header::before {
982 | /* By default, groups don't get an icon */
983 | content: none;
984 | width: var(--pwa-icon-size);
985 | height: var(--pwa-icon-size);
986 | margin: var(--pwa-icon-margin);
987 | display: inline-block;
988 | vertical-align: middle;
989 | }
990 |
991 | /* Style the "over budget" columns red. */
992 | .lh-audit-group--budgets #performance-budget tbody tr td:nth-child(4),
993 | .lh-audit-group--budgets #performance-budget tbody tr td:nth-child(5),
994 | .lh-audit-group--budgets #timing-budget tbody tr td:nth-child(3) {
995 | color: var(--color-red-700);
996 | }
997 |
998 | /* Align the "over budget request count" text to be close to the "over budget bytes" column. */
999 | .lh-audit-group--budgets .lh-table tbody tr td:nth-child(4) {
1000 | text-align: right;
1001 | }
1002 |
1003 | .lh-audit-group--budgets .lh-details--budget {
1004 | width: 100%;
1005 | margin: 0 0 var(--default-padding);
1006 | }
1007 |
1008 | .lh-audit-group--pwa-installable .lh-audit-group__header::before {
1009 | content: "";
1010 | background-image: var(--pwa-installable-gray-url);
1011 | }
1012 |
1013 | .lh-audit-group--pwa-optimized .lh-audit-group__header::before {
1014 | content: "";
1015 | background-image: var(--pwa-optimized-gray-url);
1016 | }
1017 |
1018 | .lh-audit-group--pwa-installable.lh-badged .lh-audit-group__header::before {
1019 | background-image: var(--pwa-installable-color-url);
1020 | }
1021 |
1022 | .lh-audit-group--pwa-optimized.lh-badged .lh-audit-group__header::before {
1023 | background-image: var(--pwa-optimized-color-url);
1024 | }
1025 |
1026 | .lh-audit-group--metrics .lh-audit-group__summary {
1027 | margin-top: 0;
1028 | margin-bottom: 0;
1029 | }
1030 |
1031 | .lh-audit-group__summary {
1032 | display: flex;
1033 | justify-content: space-between;
1034 | align-items: center;
1035 | }
1036 |
1037 | .lh-audit-group__header .lh-chevron {
1038 | margin-top: calc((var(--report-line-height) - 5px) / 2);
1039 | }
1040 |
1041 | .lh-audit-group__header {
1042 | letter-spacing: 0.8px;
1043 | padding: var(--default-padding);
1044 | padding-left: 0;
1045 | }
1046 |
1047 | .lh-audit-group__header,
1048 | .lh-audit-group__summary {
1049 | font-size: var(--report-font-size-secondary);
1050 | line-height: var(--report-line-height-secondary);
1051 | color: var(--color-gray-700);
1052 | }
1053 |
1054 | .lh-audit-group__title {
1055 | text-transform: uppercase;
1056 | font-weight: 500;
1057 | }
1058 |
1059 | .lh-audit-group__itemcount {
1060 | color: var(--color-gray-600);
1061 | }
1062 |
1063 | .lh-audit-group__footer {
1064 | color: var(--color-gray-600);
1065 | display: block;
1066 | margin-top: var(--default-padding);
1067 | }
1068 |
1069 | .lh-details,
1070 | .lh-category-header__description,
1071 | .lh-load-opportunity__header,
1072 | .lh-audit-group__footer {
1073 | font-size: var(--report-font-size-secondary);
1074 | line-height: var(--report-line-height-secondary);
1075 | }
1076 |
1077 | .lh-audit-explanation {
1078 | margin: var(--audit-padding-vertical) 0 calc(var(--audit-padding-vertical) / 2) var(--audit-margin-horizontal);
1079 | line-height: var(--audit-explanation-line-height);
1080 | display: inline-block;
1081 | }
1082 |
1083 | .lh-audit--fail .lh-audit-explanation {
1084 | color: var(--color-fail-secondary);
1085 | }
1086 |
1087 | /* Report */
1088 | .lh-list> :not(:last-child) {
1089 | margin-bottom: calc(var(--default-padding) * 2);
1090 | }
1091 |
1092 | .lh-header-container {
1093 | display: block;
1094 | margin: 0 auto;
1095 | position: relative;
1096 | word-wrap: break-word;
1097 | max-width: var(--report-content-max-width);
1098 | }
1099 |
1100 | .lh-header-container .lh-scores-wrapper {
1101 | border-bottom: 1px solid var(--color-gray-200);
1102 | }
1103 |
1104 | .lh-report {
1105 | min-width: var(--report-content-min-width);
1106 | }
1107 |
1108 | .lh-exception {
1109 | font-size: large;
1110 | }
1111 |
1112 | .lh-code {
1113 | white-space: normal;
1114 | margin-top: 0;
1115 | font-size: var(--report-monospace-font-size);
1116 | }
1117 |
1118 | .lh-warnings {
1119 | --item-margin: calc(var(--report-line-height) / 6);
1120 | color: var(--color-average-secondary);
1121 | margin: var(--audit-padding-vertical) 0;
1122 | padding: var(--default-padding) var(--default-padding) var(--default-padding) calc(var(--audit-description-padding-left));
1123 | background-color: var(--toplevel-warning-background-color);
1124 | }
1125 |
1126 | .lh-warnings span {
1127 | font-weight: bold;
1128 | }
1129 |
1130 | .lh-warnings--toplevel {
1131 | --item-margin: calc(var(--header-line-height) / 4);
1132 | color: var(--toplevel-warning-text-color);
1133 | margin-left: auto;
1134 | margin-right: auto;
1135 | max-width: var(--report-content-max-width-minus-edge-gap);
1136 | padding: var(--toplevel-warning-padding);
1137 | border-radius: 8px;
1138 | }
1139 |
1140 | .lh-warnings__msg {
1141 | color: var(--toplevel-warning-message-text-color);
1142 | margin: 0;
1143 | }
1144 |
1145 | .lh-warnings ul {
1146 | margin: 0;
1147 | }
1148 |
1149 | .lh-warnings li {
1150 | margin: var(--item-margin) 0;
1151 | }
1152 |
1153 | .lh-warnings li:last-of-type {
1154 | margin-bottom: 0;
1155 | }
1156 |
1157 | .lh-scores-header {
1158 | display: flex;
1159 | flex-wrap: wrap;
1160 | justify-content: center;
1161 | }
1162 |
1163 | .lh-scores-header__solo {
1164 | padding: 0;
1165 | border: 0;
1166 | }
1167 |
1168 | /* Gauge */
1169 | .lh-gauge__wrapper--pass {
1170 | color: var(--color-pass-secondary);
1171 | fill: var(--color-pass);
1172 | stroke: var(--color-pass);
1173 | }
1174 |
1175 | .lh-gauge__wrapper--average {
1176 | color: var(--color-average-secondary);
1177 | fill: var(--color-average);
1178 | stroke: var(--color-average);
1179 | }
1180 |
1181 | .lh-gauge__wrapper--fail {
1182 | color: var(--color-fail-secondary);
1183 | fill: var(--color-fail);
1184 | stroke: var(--color-fail);
1185 | }
1186 |
1187 | .lh-gauge__wrapper--not-applicable {
1188 | color: var(--color-not-applicable);
1189 | fill: var(--color-not-applicable);
1190 | stroke: var(--color-not-applicable);
1191 | }
1192 |
1193 | .lh-fraction__wrapper .lh-fraction__content::before {
1194 | content: "";
1195 | height: var(--score-icon-size);
1196 | width: var(--score-icon-size);
1197 | margin: var(--score-icon-margin);
1198 | display: inline-block;
1199 | }
1200 |
1201 | .lh-fraction__wrapper--pass .lh-fraction__content {
1202 | color: var(--color-pass-secondary);
1203 | }
1204 |
1205 | .lh-fraction__wrapper--pass .lh-fraction__background {
1206 | background-color: var(--color-pass);
1207 | }
1208 |
1209 | .lh-fraction__wrapper--pass .lh-fraction__content::before {
1210 | background-color: var(--color-pass);
1211 | border-radius: 50%;
1212 | }
1213 |
1214 | .lh-fraction__wrapper--average .lh-fraction__content {
1215 | color: var(--color-average-secondary);
1216 | }
1217 |
1218 | .lh-fraction__wrapper--average .lh-fraction__background,
1219 | .lh-fraction__wrapper--average .lh-fraction__content::before {
1220 | background-color: var(--color-average);
1221 | }
1222 |
1223 | .lh-fraction__wrapper--fail .lh-fraction__content {
1224 | color: var(--color-fail);
1225 | }
1226 |
1227 | .lh-fraction__wrapper--fail .lh-fraction__background {
1228 | background-color: var(--color-fail);
1229 | }
1230 |
1231 | .lh-fraction__wrapper--fail .lh-fraction__content::before {
1232 | border-left: calc(var(--score-icon-size) / 2) solid transparent;
1233 | border-right: calc(var(--score-icon-size) / 2) solid transparent;
1234 | border-bottom: var(--score-icon-size) solid var(--color-fail);
1235 | }
1236 |
1237 | .lh-fraction__wrapper--null .lh-fraction__content {
1238 | color: var(--color-gray-700);
1239 | }
1240 |
1241 | .lh-fraction__wrapper--null .lh-fraction__background {
1242 | background-color: var(--color-gray-700);
1243 | }
1244 |
1245 | .lh-fraction__wrapper--null .lh-fraction__content::before {
1246 | border-radius: 50%;
1247 | border: calc(0.2 * var(--score-icon-size)) solid var(--color-gray-700);
1248 | }
1249 |
1250 | .lh-fraction__background {
1251 | position: absolute;
1252 | height: 100%;
1253 | width: 100%;
1254 | border-radius: calc(var(--gauge-circle-size) / 2);
1255 | opacity: 0.1;
1256 | z-index: -1;
1257 | }
1258 |
1259 | .lh-fraction__content-wrapper {
1260 | height: var(--gauge-circle-size);
1261 | display: flex;
1262 | align-items: center;
1263 | }
1264 |
1265 | .lh-fraction__content {
1266 | display: flex;
1267 | position: relative;
1268 | align-items: center;
1269 | justify-content: center;
1270 | font-size: calc(0.3 * var(--gauge-circle-size));
1271 | line-height: calc(0.4 * var(--gauge-circle-size));
1272 | width: max-content;
1273 | min-width: calc(1.5 * var(--gauge-circle-size));
1274 | padding: calc(0.1 * var(--gauge-circle-size)) calc(0.2 * var(--gauge-circle-size));
1275 | --score-icon-size: calc(0.21 * var(--gauge-circle-size));
1276 | --score-icon-margin: 0 calc(0.15 * var(--gauge-circle-size)) 0 0;
1277 | }
1278 |
1279 | .lh-gauge {
1280 | stroke-linecap: round;
1281 | width: var(--gauge-circle-size);
1282 | height: var(--gauge-circle-size);
1283 | }
1284 |
1285 | .lh-category .lh-gauge {
1286 | --gauge-circle-size: var(--gauge-circle-size-big);
1287 | }
1288 |
1289 | .lh-gauge-base {
1290 | opacity: 0.1;
1291 | }
1292 |
1293 | .lh-gauge-arc {
1294 | fill: none;
1295 | transform-origin: 50% 50%;
1296 | animation: load-gauge var(--transition-length) ease forwards;
1297 | animation-delay: 250ms;
1298 | }
1299 |
1300 | .lh-gauge__svg-wrapper {
1301 | position: relative;
1302 | height: var(--gauge-circle-size);
1303 | }
1304 |
1305 | .lh-category .lh-gauge__svg-wrapper,
1306 | .lh-category .lh-fraction__wrapper {
1307 | --gauge-circle-size: var(--gauge-circle-size-big);
1308 | }
1309 |
1310 | /* The plugin badge overlay */
1311 | .lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {
1312 | width: var(--plugin-badge-size);
1313 | height: var(--plugin-badge-size);
1314 | background-color: var(--plugin-badge-background-color);
1315 | background-image: var(--plugin-icon-url);
1316 | background-repeat: no-repeat;
1317 | background-size: var(--plugin-icon-size);
1318 | background-position: 58% 50%;
1319 | content: "";
1320 | position: absolute;
1321 | right: -6px;
1322 | bottom: 0px;
1323 | display: block;
1324 | z-index: 100;
1325 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
1326 | border-radius: 25%;
1327 | }
1328 |
1329 | .lh-category .lh-gauge__wrapper--plugin .lh-gauge__svg-wrapper::before {
1330 | width: var(--plugin-badge-size-big);
1331 | height: var(--plugin-badge-size-big);
1332 | }
1333 |
1334 | @keyframes load-gauge {
1335 | from {
1336 | stroke-dasharray: 0 352;
1337 | }
1338 | }
1339 |
1340 | .lh-gauge__percentage {
1341 | width: 100%;
1342 | height: var(--gauge-circle-size);
1343 | position: absolute;
1344 | font-family: var(--report-font-family-monospace);
1345 | font-size: calc(var(--gauge-circle-size) * 0.34 + 1.3px);
1346 | line-height: 0;
1347 | text-align: center;
1348 | top: calc(var(--score-container-padding) + var(--gauge-circle-size) / 2);
1349 | }
1350 |
1351 | .lh-category .lh-gauge__percentage {
1352 | --gauge-circle-size: var(--gauge-circle-size-big);
1353 | --gauge-percentage-font-size: var(--gauge-percentage-font-size-big);
1354 | }
1355 |
1356 | .lh-gauge__wrapper,
1357 | .lh-fraction__wrapper {
1358 | position: relative;
1359 | display: flex;
1360 | align-items: center;
1361 | flex-direction: column;
1362 | text-decoration: none;
1363 | padding: var(--score-container-padding);
1364 | --transition-length: 1s;
1365 | /* Contain the layout style paint & layers during animation*/
1366 | contain: content;
1367 | will-change: opacity;
1368 | /* Only using for layer promotion */
1369 | }
1370 |
1371 | .lh-gauge__label,
1372 | .lh-fraction__label {
1373 | font-size: var(--gauge-label-font-size);
1374 | font-weight: 500;
1375 | line-height: var(--gauge-label-line-height);
1376 | margin-top: 10px;
1377 | text-align: center;
1378 | color: var(--report-text-color);
1379 | word-break: keep-all;
1380 | }
1381 |
1382 | /* TODO(#8185) use more BEM (.lh-gauge__label--big) instead of relying on descendant selector */
1383 | .lh-category .lh-gauge__label,
1384 | .lh-category .lh-fraction__label {
1385 | --gauge-label-font-size: var(--gauge-label-font-size-big);
1386 | --gauge-label-line-height: var(--gauge-label-line-height-big);
1387 | margin-top: 14px;
1388 | }
1389 |
1390 | .lh-scores-header .lh-gauge__wrapper,
1391 | .lh-scores-header .lh-fraction__wrapper,
1392 | .lh-scores-header .lh-gauge--pwa__wrapper,
1393 | .lh-scorescale {
1394 | display: inline-flex;
1395 | gap: calc(var(--default-padding) * 4);
1396 | margin: 16px auto 0 auto;
1397 | font-size: var(--report-font-size-secondary);
1398 | color: var(--color-gray-700);
1399 | }
1400 |
1401 | .lh-scorescale-range {
1402 | display: flex;
1403 | align-items: center;
1404 | font-family: var(--report-font-family-monospace);
1405 | white-space: nowrap;
1406 | }
1407 |
1408 | .lh-category-header__finalscreenshot .lh-scorescale {
1409 | border: 0;
1410 | display: flex;
1411 | justify-content: center;
1412 | }
1413 |
1414 | .lh-category-header__finalscreenshot .lh-scorescale-range {
1415 | font-family: unset;
1416 | font-size: 12px;
1417 | }
1418 |
1419 | .lh-scorescale-wrap {
1420 | display: contents;
1421 | }
1422 |
1423 | /* Hide category score gauages if it's a single category report */
1424 | .lh-header--solo-category .lh-scores-wrapper {
1425 | display: none;
1426 | }
1427 |
1428 | .lh-categories {
1429 | width: 100%;
1430 | overflow: hidden;
1431 | }
1432 |
1433 | .lh-category {
1434 | padding: var(--category-padding);
1435 | max-width: var(--report-content-max-width);
1436 | margin: 0 auto;
1437 | --sticky-header-height: calc(var(--gauge-circle-size-sm) + var(--score-container-padding) * 2);
1438 | --topbar-plus-sticky-header: calc(var(--topbar-height) + var(--sticky-header-height));
1439 | scroll-margin-top: var(--topbar-plus-sticky-header);
1440 | /* Faster recalc style & layout of the report. https://web.dev/content-visibility/ */
1441 | content-visibility: auto;
1442 | contain-intrinsic-size: 1000px;
1443 | }
1444 |
1445 | .lh-category-wrapper {
1446 | border-bottom: 1px solid var(--color-gray-200);
1447 | }
1448 |
1449 | .lh-category-header {
1450 | margin-bottom: var(--section-padding-vertical);
1451 | }
1452 |
1453 | .lh-category-header .lh-score__gauge {
1454 | max-width: 400px;
1455 | width: auto;
1456 | margin: 0px auto;
1457 | }
1458 |
1459 | .lh-category-header__finalscreenshot {
1460 | display: grid;
1461 | grid-template: none / 1fr 1px 1fr;
1462 | justify-items: center;
1463 | align-items: center;
1464 | gap: var(--report-line-height);
1465 | min-height: 288px;
1466 | margin-bottom: var(--default-padding);
1467 | }
1468 |
1469 | .lh-final-ss-image {
1470 | /* constrain the size of the image to not be too large */
1471 | max-height: calc(var(--gauge-circle-size-big) * 2.8);
1472 | max-width: calc(var(--gauge-circle-size-big) * 3.5);
1473 | border: 1px solid var(--color-gray-200);
1474 | padding: 4px;
1475 | border-radius: 3px;
1476 | display: block;
1477 | }
1478 |
1479 | .lh-category-headercol--separator {
1480 | background: var(--color-gray-200);
1481 | width: 1px;
1482 | height: var(--gauge-circle-size-big);
1483 | }
1484 |
1485 | @media screen and (max-width: 780px) {
1486 | .lh-category-header__finalscreenshot {
1487 | grid-template: 1fr 1fr / none;
1488 | }
1489 |
1490 | .lh-category-headercol--separator {
1491 | display: none;
1492 | }
1493 | }
1494 |
1495 | /* 964 fits the min-width of the filmstrip */
1496 | @media screen and (max-width: 964px) {
1497 | .lh-report {
1498 | margin-left: 0;
1499 | width: 100%;
1500 | }
1501 | }
1502 |
1503 | @media print {
1504 | body {
1505 | -webkit-print-color-adjust: exact;
1506 | /* print background colors */
1507 | }
1508 |
1509 | .lh-container {
1510 | display: block;
1511 | }
1512 |
1513 | .lh-report {
1514 | margin-left: 0;
1515 | padding-top: 0;
1516 | }
1517 |
1518 | .lh-categories {
1519 | margin-top: 0;
1520 | }
1521 | }
1522 |
1523 | .lh-table {
1524 | border-collapse: collapse;
1525 | /* Can't assign padding to table, so shorten the width instead. */
1526 | width: calc(100% - var(--audit-description-padding-left) - var(--stackpack-padding-horizontal));
1527 | border: 1px solid var(--report-border-color-secondary);
1528 | }
1529 |
1530 | .lh-table thead th {
1531 | font-weight: normal;
1532 | color: var(--color-gray-600);
1533 | /* See text-wrapping comment on .lh-container. */
1534 | word-break: normal;
1535 | }
1536 |
1537 | .lh-row--even {
1538 | background-color: var(--table-higlight-background-color);
1539 | }
1540 |
1541 | .lh-row--hidden {
1542 | display: none;
1543 | }
1544 |
1545 | .lh-table th,
1546 | .lh-table td {
1547 | padding: var(--default-padding);
1548 | }
1549 |
1550 | .lh-table tr {
1551 | vertical-align: middle;
1552 | }
1553 |
1554 | /* Looks unnecessary, but mostly for keeping the s left-aligned */
1555 | .lh-table-column--text,
1556 | .lh-table-column--source-location,
1557 | .lh-table-column--url,
1558 | /* .lh-table-column--thumbnail, */
1559 | /* .lh-table-column--empty,*/
1560 | .lh-table-column--code,
1561 | .lh-table-column--node {
1562 | text-align: left;
1563 | }
1564 |
1565 | .lh-table-column--code {
1566 | min-width: 100px;
1567 | }
1568 |
1569 | .lh-table-column--bytes,
1570 | .lh-table-column--timespanMs,
1571 | .lh-table-column--ms,
1572 | .lh-table-column--numeric {
1573 | text-align: right;
1574 | word-break: normal;
1575 | }
1576 |
1577 | .lh-table .lh-table-column--thumbnail {
1578 | width: var(--image-preview-size);
1579 | }
1580 |
1581 | .lh-table-column--url {
1582 | min-width: 250px;
1583 | }
1584 |
1585 | .lh-table-column--text {
1586 | min-width: 80px;
1587 | }
1588 |
1589 | /* Keep columns narrow if they follow the URL column */
1590 | /* 12% was determined to be a decent narrow width, but wide enough for column headings */
1591 | .lh-table-column--url+th.lh-table-column--bytes,
1592 | .lh-table-column--url+.lh-table-column--bytes+th.lh-table-column--bytes,
1593 | .lh-table-column--url+.lh-table-column--ms,
1594 | .lh-table-column--url+.lh-table-column--ms+th.lh-table-column--bytes,
1595 | .lh-table-column--url+.lh-table-column--bytes+th.lh-table-column--timespanMs {
1596 | width: 12%;
1597 | }
1598 | .lh-text svg{
1599 | width: 20px;
1600 | }
1601 |
1602 | .lh-text__url-host {
1603 | display: inline;
1604 | }
1605 |
1606 | .lh-text__url-host {
1607 | margin-left: calc(var(--report-font-size) / 2);
1608 | opacity: 0.6;
1609 | font-size: 90%;
1610 | }
1611 |
1612 | .lh-thumbnail {
1613 | object-fit: cover;
1614 | width: var(--image-preview-size);
1615 | height: var(--image-preview-size);
1616 | display: block;
1617 | }
1618 |
1619 | .lh-unknown pre {
1620 | overflow: scroll;
1621 | border: solid 1px var(--color-gray-200);
1622 | }
1623 |
1624 | .lh-text__url>a {
1625 | color: inherit;
1626 | text-decoration: none;
1627 | }
1628 |
1629 | .lh-text__url>a:hover {
1630 | text-decoration: underline dotted #999;
1631 | }
1632 |
1633 | .lh-sub-item-row {
1634 | margin-left: 20px;
1635 | margin-bottom: 0;
1636 | color: var(--color-gray-700);
1637 | }
1638 |
1639 | .lh-sub-item-row td {
1640 | padding-top: 4px;
1641 | padding-bottom: 4px;
1642 | padding-left: 20px;
1643 | }
1644 |
1645 | .lh-table td:first-child .lh-text {
1646 | display: flex;
1647 | gap: 10px;
1648 | }
1649 |
1650 | /* Chevron
1651 | https://codepen.io/paulirish/pen/LmzEmK
1652 | */
1653 | .lh-chevron {
1654 | --chevron-angle: 42deg;
1655 | /* Edge doesn't support transform: rotate(calc(...)), so we define it here */
1656 | --chevron-angle-right: -42deg;
1657 | width: var(--chevron-size);
1658 | height: var(--chevron-size);
1659 | margin-top: calc((var(--report-line-height) - 12px) / 2);
1660 | }
1661 |
1662 | .lh-chevron__lines {
1663 | transition: transform 0.4s;
1664 | transform: translateY(var(--report-line-height));
1665 | }
1666 |
1667 | .lh-chevron__line {
1668 | stroke: var(--chevron-line-stroke);
1669 | stroke-width: var(--chevron-size);
1670 | stroke-linecap: square;
1671 | transform-origin: 50%;
1672 | transform: rotate(var(--chevron-angle));
1673 | transition: transform 300ms, stroke 300ms;
1674 | }
1675 |
1676 | .lh-expandable-details .lh-chevron__line-right,
1677 | .lh-expandable-details[open] .lh-chevron__line-left {
1678 | transform: rotate(var(--chevron-angle-right));
1679 | }
1680 |
1681 | .lh-expandable-details[open] .lh-chevron__line-right {
1682 | transform: rotate(var(--chevron-angle));
1683 | }
1684 |
1685 | .lh-expandable-details[open] .lh-chevron__lines {
1686 | transform: translateY(calc(var(--chevron-size) * -1));
1687 | }
1688 |
1689 | .lh-expandable-details[open] {
1690 | animation: 300ms openDetails forwards;
1691 | padding-bottom: var(--default-padding);
1692 | }
1693 |
1694 | @keyframes openDetails {
1695 | from {
1696 | outline: 1px solid var(--report-background-color);
1697 | }
1698 |
1699 | to {
1700 | outline: 1px solid;
1701 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.24);
1702 | }
1703 | }
1704 |
1705 | @media screen and (max-width: 780px) {
1706 |
1707 | /* no black outline if we're not confident the entire table can be displayed within bounds */
1708 | .lh-expandable-details[open] {
1709 | animation: none;
1710 | }
1711 | }
1712 |
1713 | .lh-expandable-details[open] summary,
1714 | details.lh-clump>summary {
1715 | border-bottom: 1px solid var(--report-border-color-secondary);
1716 | }
1717 |
1718 | details.lh-clump[open]>summary {
1719 | border-bottom-width: 0;
1720 | }
1721 |
1722 | details .lh-clump-toggletext--hide,
1723 | details[open] .lh-clump-toggletext--show {
1724 | display: none;
1725 | }
1726 |
1727 | details[open] .lh-clump-toggletext--hide {
1728 | display: block;
1729 | }
1730 |
1731 | /* Tooltip */
1732 | .lh-tooltip-boundary {
1733 | position: relative;
1734 | }
1735 |
1736 | .lh-tooltip {
1737 | position: absolute;
1738 | display: none;
1739 | /* Don't retain these layers when not needed */
1740 | opacity: 0;
1741 | background: #ffffff;
1742 | white-space: pre-line;
1743 | /* Render newlines in the text */
1744 | min-width: 246px;
1745 | max-width: 275px;
1746 | padding: 15px;
1747 | border-radius: 5px;
1748 | text-align: initial;
1749 | line-height: 1.4;
1750 | }
1751 |
1752 | /* shrink tooltips to not be cutoff on left edge of narrow viewports
1753 | 45vw is chosen to be ~= width of the left column of metrics
1754 | */
1755 | @media screen and (max-width: 535px) {
1756 | .lh-tooltip {
1757 | min-width: 45vw;
1758 | padding: 3vw;
1759 | }
1760 | }
1761 |
1762 | .lh-tooltip-boundary:hover .lh-tooltip {
1763 | display: block;
1764 | animation: fadeInTooltip 250ms;
1765 | animation-fill-mode: forwards;
1766 | animation-delay: 850ms;
1767 | bottom: 100%;
1768 | z-index: 1;
1769 | will-change: opacity;
1770 | right: 0;
1771 | pointer-events: none;
1772 | }
1773 |
1774 | .lh-tooltip::before {
1775 | content: "";
1776 | border: solid transparent;
1777 | border-bottom-color: #fff;
1778 | border-width: 10px;
1779 | position: absolute;
1780 | bottom: -20px;
1781 | right: 6px;
1782 | transform: rotate(180deg);
1783 | pointer-events: none;
1784 | }
1785 |
1786 | @keyframes fadeInTooltip {
1787 | 0% {
1788 | opacity: 0;
1789 | }
1790 |
1791 | 75% {
1792 | opacity: 1;
1793 | }
1794 |
1795 | 100% {
1796 | opacity: 1;
1797 | filter: drop-shadow(1px 0px 1px #aaa) drop-shadow(0px 2px 4px hsla(206, 6%, 25%, 0.15));
1798 | pointer-events: auto;
1799 | }
1800 | }
1801 |
1802 | /* Element screenshot */
1803 | .lh-element-screenshot {
1804 | position: relative;
1805 | overflow: hidden;
1806 | float: left;
1807 | margin-right: 20px;
1808 | }
1809 |
1810 | .lh-element-screenshot__content {
1811 | overflow: hidden;
1812 | }
1813 |
1814 | .lh-element-screenshot__image {
1815 | /* Set by ElementScreenshotRenderer.installFullPageScreenshotCssVariable */
1816 | background-image: var(--element-screenshot-url);
1817 | outline: 2px solid #777;
1818 | background-color: white;
1819 | background-repeat: no-repeat;
1820 | }
1821 |
1822 | .lh-element-screenshot__mask {
1823 | position: absolute;
1824 | background: #555;
1825 | opacity: 0.8;
1826 | }
1827 |
1828 | .lh-element-screenshot__element-marker {
1829 | position: absolute;
1830 | outline: 2px solid var(--color-lime-400);
1831 | }
1832 |
1833 | .lh-element-screenshot__overlay {
1834 | position: fixed;
1835 | top: 0;
1836 | left: 0;
1837 | right: 0;
1838 | bottom: 0;
1839 | z-index: 2000;
1840 | /* .lh-topbar is 1000 */
1841 | background: var(--screenshot-overlay-background);
1842 | display: flex;
1843 | align-items: center;
1844 | justify-content: center;
1845 | cursor: zoom-out;
1846 | }
1847 |
1848 | .lh-element-screenshot__overlay .lh-element-screenshot {
1849 | margin-right: 0;
1850 | /* clearing margin used in thumbnail case */
1851 | outline: 1px solid var(--color-gray-700);
1852 | }
1853 |
1854 | .lh-screenshot-overlay--enabled .lh-element-screenshot {
1855 | cursor: zoom-out;
1856 | }
1857 |
1858 | .lh-screenshot-overlay--enabled .lh-node .lh-element-screenshot {
1859 | cursor: zoom-in;
1860 | }
1861 |
1862 | .lh-meta__items {
1863 | --meta-icon-size: calc(var(--report-icon-size) * 0.667);
1864 | padding: var(--default-padding);
1865 | display: grid;
1866 | grid-template-columns: 1fr 1fr 1fr;
1867 | background-color: var(--env-item-background-color);
1868 | border-radius: 3px;
1869 | margin: 0 0 var(--default-padding) 0;
1870 | font-size: 12px;
1871 | column-gap: var(--default-padding);
1872 | color: var(--color-gray-700);
1873 | }
1874 |
1875 | .lh-meta__item {
1876 | display: block;
1877 | list-style-type: none;
1878 | position: relative;
1879 | padding: 0 0 0 calc(var(--meta-icon-size) + var(--default-padding) * 2);
1880 | cursor: unset;
1881 | /* disable pointer cursor from report-icon */
1882 | }
1883 |
1884 | .lh-meta__item.lh-tooltip-boundary {
1885 | text-decoration: dotted underline var(--color-gray-500);
1886 | cursor: help;
1887 | }
1888 |
1889 | .lh-meta__item.lh-report-icon::before {
1890 | position: absolute;
1891 | left: var(--default-padding);
1892 | width: var(--meta-icon-size);
1893 | height: var(--meta-icon-size);
1894 | }
1895 |
1896 | .lh-meta__item.lh-report-icon:hover::before {
1897 | opacity: 0.7;
1898 | }
1899 |
1900 | .lh-meta__item .lh-tooltip {
1901 | color: var(--color-gray-800);
1902 | }
1903 |
1904 | .lh-meta__item .lh-tooltip::before {
1905 | right: auto;
1906 | /* Set the tooltip arrow to the leftside */
1907 | left: 6px;
1908 | }
1909 |
1910 | /* Change the grid for narrow viewport. */
1911 | @media screen and (max-width: 640px) {
1912 | .lh-meta__items {
1913 | grid-template-columns: 1fr 1fr;
1914 | }
1915 | }
1916 |
1917 | @media screen and (max-width: 535px) {
1918 | .lh-meta__items {
1919 | display: block;
1920 | }
1921 | }
1922 |
1923 | .lh-score100 .lh-lighthouse stop:first-child {
1924 | stop-color: hsla(200, 12%, 95%, 0);
1925 | }
1926 |
1927 | .lh-score100 .lh-lighthouse stop:last-child {
1928 | stop-color: hsla(65, 81%, 76%, 1);
1929 | }
1930 |
1931 |
1932 | @keyframes bang {
1933 | to {
1934 | box-shadow: -70px -115.67px #47ebbc, -28px -99.67px #eb47a4,
1935 | 58px -31.67px #7eeb47, 13px -141.67px #eb47c5,
1936 | -19px 6.33px #7347eb, -2px -74.67px #ebd247,
1937 | 24px -151.67px #eb47e0, 57px -138.67px #b4eb47,
1938 | -51px -104.67px #479eeb, 62px 8.33px #ebcf47, -93px 0.33px #d547eb,
1939 | -16px -118.67px #47bfeb, 53px -84.67px #47eb83,
1940 | 66px -57.67px #eb47bf, -93px -65.67px #91eb47,
1941 | 30px -13.67px #86eb47, -2px -59.67px #83eb47, -44px 1.33px #eb47eb,
1942 | 61px -58.67px #47eb73, 5px -22.67px #47e8eb,
1943 | -66px -28.67px #ebe247, 42px -123.67px #eb5547,
1944 | -75px 26.33px #7beb47, 15px -52.67px #a147eb,
1945 | 36px -51.67px #eb8347, -38px -12.67px #eb5547,
1946 | -46px -59.67px #47eb81, 78px -114.67px #eb47ba,
1947 | 15px -156.67px #eb47bf, -36px 1.33px #eb4783,
1948 | -72px -86.67px #eba147, 31px -46.67px #ebe247,
1949 | -68px 29.33px #47e2eb, -55px 19.33px #ebe047,
1950 | -56px 27.33px #4776eb, -13px -91.67px #eb5547,
1951 | -47px -138.67px #47ebc7, -18px -96.67px #eb47ac,
1952 | 11px -88.67px #4783eb, -67px -28.67px #47baeb,
1953 | 53px 10.33px #ba47eb, 11px 19.33px #5247eb, -5px -11.67px #eb4791,
1954 | -68px -4.67px #47eba7, 95px -37.67px #eb478b,
1955 | -67px -162.67px #eb5d47, -54px -120.67px #eb6847,
1956 | 49px -12.67px #ebe047, 88px 8.33px #47ebda, 97px 33.33px #eb8147,
1957 | 6px -71.67px #ebbc47;
1958 | }
1959 | }
1960 |
1961 | @keyframes gravity {
1962 | to {
1963 | transform: translateY(80px);
1964 | opacity: 0;
1965 | }
1966 | }
1967 |
1968 | @keyframes position {
1969 |
1970 | 0%,
1971 | 19.9% {
1972 | margin-top: 4%;
1973 | margin-left: 47%;
1974 | }
1975 |
1976 | 20%,
1977 | 39.9% {
1978 | margin-top: 7%;
1979 | margin-left: 30%;
1980 | }
1981 |
1982 | 40%,
1983 | 59.9% {
1984 | margin-top: 6%;
1985 | margin-left: 70%;
1986 | }
1987 |
1988 | 60%,
1989 | 79.9% {
1990 | margin-top: 3%;
1991 | margin-left: 20%;
1992 | }
1993 |
1994 | 80%,
1995 | 99.9% {
1996 | margin-top: 3%;
1997 | margin-left: 80%;
1998 | }
1999 | }
2000 |
2001 | .lh-scores-container {
2002 | display: flex;
2003 | flex-direction: column;
2004 | padding: var(--default-padding) 0;
2005 | position: relative;
2006 | width: 100%;
2007 | }
2008 |
2009 | .lh-highlighter {
2010 | width: var(--gauge-wrapper-width);
2011 | height: 1px;
2012 | background-color: var(--highlighter-background-color);
2013 | /* Position at bottom of first gauge in sticky header. */
2014 | position: absolute;
2015 | grid-column: 1;
2016 | bottom: -1px;
2017 | }
2018 |
2019 | .lh-gauge__wrapper:first-of-type {
2020 | contain: none;
2021 | }
2022 |
2023 | details .lh-scores-wrapper {
2024 | width: 100%;
2025 | }
2026 |
2027 | #performance>.lh-audit__description {
2028 | padding: 0;
2029 | }
2030 |
2031 | #performance>table {
2032 | margin: 0;
2033 | margin-bottom: 2rem;
2034 | width: 100%;
2035 | }
2036 |
2037 | .sheetScoreContainer{
2038 | display:flex;
2039 | flex-direction: row;
2040 | }
2041 |
2042 | .sheetScore {
2043 | width: 50px;
2044 | margin: auto;
2045 | }
2046 |
2047 | .green-audit--fail {
2048 | fill: #fff;
2049 | stroke: #000;
2050 | stroke-width: 2px;
2051 | }
2052 |
2053 | .green-audit--pass {
2054 | fill: #4CAF4B
2055 |
2056 | }
2057 |
2058 | h1.lh-audit__title{
2059 | flex-direction: row;
2060 | }
2061 | .st0{
2062 | fill:#009549;
2063 | }
2064 |
--------------------------------------------------------------------------------
|