├── .eslintrc.js ├── .github └── workflows │ ├── auto-assign.yml │ ├── build.yml │ └── npm-publish.yml ├── .gitignore ├── .husky └── pre-push ├── .vscode └── launch.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── cypress-demo ├── config.js ├── cypress.config.js ├── cypress │ ├── e2e │ │ └── demo.cy.js │ ├── fixtures │ │ └── example.json │ └── support │ │ ├── commands.js │ │ └── e2e.js ├── package-lock.json └── package.json ├── example ├── ecoindex │ └── example1.json └── lighthouse │ ├── example1.html │ └── example1.json ├── package-lock.json ├── package.json ├── playwright-demo ├── .gitignore ├── README.md ├── Tests │ └── welcomeAxaPage.spec.ts ├── extensions │ ├── aggregator │ │ ├── aggregator-args-builder.ts │ │ ├── aggregator-config.ts │ │ └── aggregatorExtension.ts │ ├── ecoIndex │ │ ├── ecoIndexConfig.ts │ │ └── ecoIndexExtension.ts │ ├── extensionWrapper.ts │ └── lighthouse │ │ ├── lighthouseConfig.ts │ │ ├── lighthouseExtension.ts │ │ └── lighthouseReport.ts ├── package-lock.json ├── package.json ├── playwright.config.ts └── tsconfig.json ├── readme.md └── src ├── cli.js ├── config.json ├── ecoIndex ├── aggregatorService.js └── aggregatorService.spec.js ├── globlalAggregation ├── aggregatorService.js └── aggregatorService.spec.js ├── lighthouse ├── aggregatorService.js └── aggregatorService.spec.js ├── main.js └── reporters ├── __snapshots__ └── generatorReports.spec.js.snap ├── generatorReports.js ├── generatorReports.spec.js ├── globalTag.js ├── pageTag.js ├── readTemplate.js ├── templates ├── co2.svg ├── sheet.svg ├── style.css ├── template.html ├── templateDoughnut.html ├── templateGreenItMetrics.html ├── templatePageMetrics.html ├── templatePerPage.html ├── templateStatusGreen.html └── water.svg ├── test └── globalReportsTest.html ├── translate ├── Fr-fr.json └── en-GB.json └── utils ├── __snapshots__ └── statusGreen.spec.js.snap ├── computeCssClassForMetrics.js ├── displayPageErrorIcon.js ├── statusGreen.js ├── statusGreen.spec.js ├── statusPerPage.js └── statusPerPage.spec.js /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint && npm run test 5 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | corrige le nan dans ecoindex -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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: -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## A picture tells a thousand words 2 | 3 | ## Before this PR 4 | 5 | ## After this PR -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /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') -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /playwright-demo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | /reports/ -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /playwright-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "resolveJsonModule": true 5 | } 6 | } -------------------------------------------------------------------------------- /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 | ![image](https://user-images.githubusercontent.com/6480596/213727763-d8cdf611-2b35-4c60-aa94-bd85d5de006c.png) 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 [![npm version](https://badge.fury.io/js/lighthouse-eco-index-aggregator.svg)](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/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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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.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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/reporters/templates/co2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | Svg Vector Icons : http://www.onlinewebfonts.com/icon 7 | 8 | 9 | 11 | 13 | 15 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/reporters/templates/sheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 20 | 29 | 38 | 39 | -------------------------------------------------------------------------------- /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,check'); 129 | --average-icon-url: url('data:image/svg+xml;utf8,info'); 130 | --fail-icon-url: url('data:image/svg+xml;utf8,warn'); 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 | -------------------------------------------------------------------------------- /src/reporters/templates/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 |
17 |
18 |
19 |

20 | <%- Translations.LabelGlobalTitle %> 21 | <%- GlobalNote %> 22 |

23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | <%- GlobalPerformance %> 31 | <%- GlobalAccessibility %> 32 | <%- GlobalBestPractices %> 33 | <%- GlobalEcoIndex %> 34 |
35 |
36 |
37 | 38 | 39 |
40 |
41 |
42 |
43 |
44 | <%- Translations.LabelDescriptionAudit %> 45 |
46 | <%- GlobalGreenItMetrics %> 47 |
48 |
49 | <%= Translations.LabelResultByPage %> 50 |
51 |
52 |
53 | <%= Translations.LabelPage %> 54 |
55 |
56 | <%- PerPages %> 57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | 66 |
-------------------------------------------------------------------------------- /src/reporters/templates/templateDoughnut.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 7 | 8 |
9 |
<%= Value %>
10 |
<%= Label %>
11 |
12 | -------------------------------------------------------------------------------- /src/reporters/templates/templateGreenItMetrics.html: -------------------------------------------------------------------------------- 1 |
2 | <%- Translations.DescriptionMetrics %> 3 |
4 | 5 | 6 | 7 | 12 | 17 | 22 | 23 | 24 | 25 | 26 | 37 | 42 | 47 | 48 | 49 | 50 | 63 | 68 | 74 | 75 | 76 |
8 |
9 | <%= Translations.LabelMetrics %> 10 |
11 |
13 |
14 | <%= Translations.LabelValue %> 15 |
16 |
18 |
19 | <%= Translations.LabelConvertedValue %> 20 |
21 |
27 |
28 | <%- mySvg.svgIconCo2 %> 29 | 30 | <%= Translations.LabelGases %> 31 | 32 | 33 | (<%- Translations.LabelWordFor %> <%= gasesNumberOfVisits %> <%- Translations.LabelWordVisits %>) 34 | 35 |
36 |
38 |
39 | <%= greenhouseGases %> 40 |
41 |
43 |
44 | <%= greenhouseGasesKm %> km 45 |
46 |
51 |
52 | 53 | <%- mySvg.svgIconWater %> 54 | 55 | 56 | <%= Translations.LabelWaterConsummation %> 57 | 58 | 59 | (<%- Translations.LabelWordFor %> <%= waterNumberOfVisits %> <%- Translations.LabelWordVisits %>) 60 | 61 |
62 |
64 |
65 | <%= water %> 66 |
67 |
69 |
70 | <%= waterShower %> 71 | <%= Translations.LabelShowers %> 72 |
73 |
-------------------------------------------------------------------------------- /src/reporters/templates/templatePageMetrics.html: -------------------------------------------------------------------------------- 1 |
2 | <%= Translations.DescriptionEcoIndex %> 3 |
4 | 5 | 6 | 7 | 12 | 17 | 22 | 23 | 24 | 25 | 26 | 31 | 34 | 37 | 38 | 39 | 40 | 43 | 47 | 50 | 51 | 52 | 53 | 56 | 59 | 62 | 63 | 64 |
8 |
9 | <%= Translations.LabelMetrics %> 10 |
11 |
13 |
14 | <%= Translations.LabelValue %> 15 |
16 |
18 |
19 | <%= Translations.LabelMaxRecommandedValue %> 20 |
21 |
27 |
28 | <%= Translations.LabelNumberRequest %> 29 |
30 |
32 |
<%= NumberOfRequest %>
33 |
35 |
<%= NumberOfRequestRecommendation %>
36 |
41 |
<%= Translations.LabelWeightPage %>
42 |
44 | 45 |
<%= PageSize %>
46 |
48 |
<%= PageSizeRecommendation %>
49 |
54 |
<%= Translations.LabelComplexityDom %>
55 |
57 |
<%= PageComplexity %>
58 |
60 |
<%= PageComplexityRecommendation %>
61 |
65 | -------------------------------------------------------------------------------- /src/reporters/templates/templatePerPage.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |
7 | <%- IconPerPageTag %> 8 |
9 |
10 | <%= Translations.LabelStep %> 11 | <%= numberPageTag %>
12 | 13 | <%= PageName %> 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 | <%= Translations.SeeReport %> 22 | 23 | <%= Translations.LabelTheReport %> 24 | 25 | 26 |
27 |
28 |
29 |
30 | <%- PerformanceBlock %> 31 | <%- AccessibilityBlock %> 32 | <%- BestPracticesBlock %> 33 | <%- EcoIndexBlock %> 34 |
35 |
36 |
37 | <%- PageMetricsBlock %> 38 | <%- GreenItMetricsBlock %> 39 |
40 |
-------------------------------------------------------------------------------- /src/reporters/templates/templateStatusGreen.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%- mySvg.firtSheet %> 4 |
5 |
6 | <%- mySvg.secondSheet %> 7 |
8 |
9 | <%- mySvg.ThreeSheet %> 10 |
11 |
-------------------------------------------------------------------------------- /src/reporters/templates/water.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Svg Vector Icons : http://www.onlinewebfonts.com/icon 6 | 7 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | 10 | 25 | 34 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 51 | 66 | 75 | 84 | 85 | 86 |
87 |
88 | 89 | 90 | 92 | 107 | 116 | 125 | 126 | 127 |
128 |
" 129 | `; 130 | 131 | exports[`statusGreen should three sheet 1`] = ` 132 | "
133 |
134 | 135 | 136 | 138 | 153 | 162 | 171 | 172 | 173 |
174 |
175 | 176 | 177 | 179 | 194 | 203 | 212 | 213 | 214 |
215 |
216 | 217 | 218 | 220 | 235 | 244 | 253 | 254 | 255 |
256 |
" 257 | `; 258 | 259 | exports[`statusGreen should two sheet 1`] = ` 260 | "
261 |
262 | 263 | 264 | 266 | 281 | 290 | 299 | 300 | 301 |
302 |
303 | 304 | 305 | 307 | 322 | 331 | 340 | 341 | 342 |
343 |
344 | 345 | 346 | 348 | 363 | 372 | 381 | 382 | 383 |
384 |
" 385 | `; 386 | -------------------------------------------------------------------------------- /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/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; -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------