# Utilisation avec Lighthouse CI

# Objectifs

Utiliser le plugin lighthouse-ecoindex avec Lighthouse CI dans vos outils de CI/CD ou vos repositories CI/Cd.

Vous pourrez ainsi :

  • Publier les résultats des audits Lighthouse et EcoIndex® dans votre CI/CD ;
  • Publier les résultats des audits Lighthouse et EcoIndex® dans un serveur Lighthouse.

# Installation

# Ajout à un projet existant
npm install lighthouse lighthouse-plugin-ecoindex-core puppeteer --save-dev

# Utilisation

Vous devez utiliser le fichiers configuration de Lighthouse pour pouvoir utiliser le plugin lighthouse-ecoindex.

const path = require('path')

/**
 * Get the path to the custom Lighthouse config file.
 * @returns {string} The path to the custom Lighthouse config file.
 */
const getLighthouseConfig = () => {
  return path.join(
    require.resolve('lighthouse-plugin-ecoindex-core'),
    '../helpers/lhci/custom-config.js',
  )
}

module.exports = {
  ci: {
    collect: {
      url: [
        'https://www.ecoindex.fr/',
        'https://novagaia.fr/',
        // 'https://www.relocalisons.bzh/',
        // 'https://www.neuro-mav-france.org/',
        // 'https://www.alodokter.com/',
      ],
      numberOfRuns: 1,
      settings: {
        configPath: getLighthouseConfig(),
      },
      puppeteerLaunchOptions: {
        headless: 'new',
        args: [
          '--disable-gpu',
          '--disable-dev-shm-usage',
          '--disable-setuid-sandbox',
          '--no-sandbox',
        ],
      },
      puppeteerScript: './.puppeteerrc.cjs',
    },
    assert: {
      preset: 'lighthouse:default',
    },
  },
}

Tip Placer le fichier .lighthouserc.js à la racine de votre projet.

// https://pptr.dev/guides/configuration
// https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/configuration.md#puppeteerscript
/**
 * @param {puppeteer.Browser} browser
 * @param {{url: string, options: LHCI.CollectCommand.Options}} context
 */
module.exports = async (browser, context) => {
  // launch browser for LHCI
  var page = await browser.newPage(context.options)
  // To be set by env vars
  const authenticate = {
    loginPage: `https://greenit.eco/wp-login.php/`,
    loginField: '#user_login',
    loginValue: process.env.LOGIN_VALUE || '********',
    passField: '#user_pass',
    passValue: process.env.PASS_VALUE || '********',
  }

  // Test if current page is the login URL page
  if (context.url === authenticate.loginPage) {
    console.log(`Authenticate on`, authenticate.loginPage)
    connect(page, browser, authenticate)
  } else {
    const session = await page.target().createCDPSession()
    try {
      await page.goto(context.url, {
        waitUntil: 'networkidle0',
        timeout: 10000, // change timeout to 10s for crashing tests faster.
      })
    } catch (error) {
      console.error('Error getting page:', error.message)
      console.error('Retry...')
      await page.goto(context.url)
    }
    await startEcoindexPageMesure(page, session)
    await endEcoindexPageMesure()
    // close session for next run
    await page.close()
  }
}
async function connect(page, browser, authenticate) {
  page = await browser.newPage()
  await page.goto(authenticate.loginPage)
  await page.type(authenticate.loginField, authenticate.loginValue)
  await page.type(authenticate.passField, authenticate.passValue)
  await page.click('[type="submit"]')
  try {
    await page.waitForNavigation()
    // close session for next run
    await page.close()
    console.log(`Authenticated!`)
  } catch (error) {
    throw new Error(`Connection failed!`)
  }
}

async function startEcoindexPageMesure(page, session) {
  page.setViewport({
    width: 1920,
    height: 1080,
  })
  await new Promise(r => setTimeout(r, 3 * 1000))
  const dimensions = await page.evaluate(() => {
    var body = document.body,
      html = document.documentElement

    var height = Math.max(
      body.scrollHeight,
      body.offsetHeight,
      html.clientHeight,
      html.scrollHeight,
      html.offsetHeight,
    )
    return {
      width: document.documentElement.clientWidth,
      height: height,
      deviceScaleFactor: window.devicePixelRatio,
    }
  })
  // console.log('dimensions', dimensions)
  // We need the ability to scroll like a user. There's not a direct puppeteer function for this, but we can use the DevTools Protocol and issue a Input.synthesizeScrollGesture event, which has convenient parameters like repetitions and delay to somewhat simulate a more natural scrolling gesture.
  // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-synthesizeScrollGesture
  await session.send('Input.synthesizeScrollGesture', {
    x: 100,
    y: 600,
    yDistance: -dimensions.height,
    speed: 1000,
  })
}

/**
 * End Ecoindex flow. Wait 3s.
 */
async function endEcoindexPageMesure() {
  await new Promise(r => setTimeout(r, 3 * 1000))
}

# Exemple

Projet example pour lighthouse-ci
https://github.com/cnumr/lighthouse-plugin-ecoindex/blob/main/test/test-ecoindex-lh-plugin-ts

# Exemples à adapter suivant votre CI/CD

Informations

.github/workflows/ci.yml

name: CI
on: [push]
jobs:
  lighthouseci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16
      - run: npm install && npm install -g @lhci/cli@0.12.x
      - run: npm run build
      - run: lhci autorun

.gitlab-ci.yml

default:
  cache: # Cache modules using lock file
    key:
      files:
        - package-lock.json
    paths:
      - .npm/
  before_script:
    - npm ci --cache .npm --prefer-offline --loglevel=error

lighthouse:
  image: registry.gitlab.com/gitlab-ci-utils/lighthouse:latest
  stage: test
  when: manual
  script:
    - npm run lhci:collect || echo "LHCI:Collect failed!"
    - npm run lhci:upload || echo "LHCI:Upload failed!"
  artifacts:
    # Always save artifacts. This is needed if lighthouse is run configured
    # to fail on certain criteria, and will ensure the report is saved.
    when: always
    # Save the lighthouse report, which by default is named for the site
    # analyzed and the current time.
    paths:
      - .lighthouseci/*

package.json

{
  "name": "poc-mesure-automatisee-ecoindex-lighthouse",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "lhci": "npm run lhci:collect && npm run lhci:upload",
    "lhci:collect": "lhci collect",
    "lhci:upload": "lhci upload --upload.basicAuth.username=******** --upload.basicAuth.********",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@lhci/cli": "^0.14.0",
    "lighthouse-plugin-ecoindex-core": "^5.0.0",
    "puppeteer": "^23.3.0",
    "puppeteer-core": "^23.3.0"
  }
}

.lighthouserc.js

const path = require('path')

/**
 * Get the path to the custom Lighthouse config file.
 * @returns {string} The path to the custom Lighthouse config file.
 */
const getLighthouseConfig = () => {
  return path.join(
    require.resolve('lighthouse-plugin-ecoindex-core'),
    '../helpers/lhci/custom-config.js',
  )
}

module.exports = {
  ci: {
    collect: {
      url: [
        'https://www.ecoindex.fr/',
        'https://novagaia.fr/',
        // 'https://www.relocalisons.bzh/',
        // 'https://www.neuro-mav-france.org/',
        // 'https://www.alodokter.com/',
      ],
      numberOfRuns: 1,
      settings: {
        configPath: getLighthouseConfig(),
      },
      puppeteerLaunchOptions: {
        headless: 'new',
        args: [
          '--disable-gpu',
          '--disable-dev-shm-usage',
          '--disable-setuid-sandbox',
          '--no-sandbox',
        ],
      },
      puppeteerScript: './.puppeteerrc.cjs',
    },
    assert: {
      preset: 'lighthouse:default',
    },
  },
}

.puppeteerrc.cjs

// https://pptr.dev/guides/configuration
// https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/configuration.md#puppeteerscript
/**
 * @param {puppeteer.Browser} browser
 * @param {{url: string, options: LHCI.CollectCommand.Options}} context
 */
module.exports = async (browser, context) => {
  // launch browser for LHCI
  var page = await browser.newPage(context.options)
  // To be set by env vars
  const authenticate = {
    loginPage: `https://greenit.eco/wp-login.php/`,
    loginField: '#user_login',
    loginValue: process.env.LOGIN_VALUE || '********',
    passField: '#user_pass',
    passValue: process.env.PASS_VALUE || '********',
  }

  // Test if current page is the login URL page
  if (context.url === authenticate.loginPage) {
    console.log(`Authenticate on`, authenticate.loginPage)
    connect(page, browser, authenticate)
  } else {
    const session = await page.target().createCDPSession()
    try {
      await page.goto(context.url, {
        waitUntil: 'networkidle0',
        timeout: 10000, // change timeout to 10s for crashing tests faster.
      })
    } catch (error) {
      console.error('Error getting page:', error.message)
      console.error('Retry...')
      await page.goto(context.url)
    }
    await startEcoindexPageMesure(page, session)
    await endEcoindexPageMesure()
    // close session for next run
    await page.close()
  }
}
async function connect(page, browser, authenticate) {
  page = await browser.newPage()
  await page.goto(authenticate.loginPage)
  await page.type(authenticate.loginField, authenticate.loginValue)
  await page.type(authenticate.passField, authenticate.passValue)
  await page.click('[type="submit"]')
  try {
    await page.waitForNavigation()
    // close session for next run
    await page.close()
    console.log(`Authenticated!`)
  } catch (error) {
    throw new Error(`Connection failed!`)
  }
}

async function startEcoindexPageMesure(page, session) {
  page.setViewport({
    width: 1920,
    height: 1080,
  })
  await new Promise(r => setTimeout(r, 3 * 1000))
  const dimensions = await page.evaluate(() => {
    var body = document.body,
      html = document.documentElement

    var height = Math.max(
      body.scrollHeight,
      body.offsetHeight,
      html.clientHeight,
      html.scrollHeight,
      html.offsetHeight,
    )
    return {
      width: document.documentElement.clientWidth,
      height: height,
      deviceScaleFactor: window.devicePixelRatio,
    }
  })
  // console.log('dimensions', dimensions)
  // We need the ability to scroll like a user. There's not a direct puppeteer function for this, but we can use the DevTools Protocol and issue a Input.synthesizeScrollGesture event, which has convenient parameters like repetitions and delay to somewhat simulate a more natural scrolling gesture.
  // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-synthesizeScrollGesture
  await session.send('Input.synthesizeScrollGesture', {
    x: 100,
    y: 600,
    yDistance: -dimensions.height,
    speed: 1000,
  })
}

/**
 * End Ecoindex flow. Wait 3s.
 */
async function endEcoindexPageMesure() {
  await new Promise(r => setTimeout(r, 3 * 1000))
}

lighthouse-metrics.js

const fs = require('fs')

const scoreScalingFactor = 100
const categories = ['accessibility', 'lighthouse-plugin-ecoindex-core']

// get all the report files
const reportFileNames = fs.readdirSync('./').filter(fn => fn.endsWith('.json'))
// read the report files
reportFileNames.forEach(report => {
  // read the report file
  const results = JSON.parse(fs.readFileSync(report, 'utf8'))
  // get the URL
  const url = results.finalUrl
  console.log(`Metrics for ${url}`)
  // must slugify the URL to avoid special characters in the file name
  const slug = url.replace(/[^a-z0-9]/gi, '-').toLowerCase()
  // generate the file name
  const metricsFileName = `lighthouse-metrics-${slug}.txt`
  // generate the metrics
  const metrics = categories
    .map(category => {
      if (category === 'lighthouse-plugin-ecoindex-core') {
        return `lighthouse{category="ecoindex"} ${
          results?.categories['lighthouse-plugin-ecoindex-core']?.score *
          scoreScalingFactor
        }`
      } else {
        return `lighthouse{category="${category}"} ${
          results?.categories[category]?.score * scoreScalingFactor
        }`
      }
    })
    .join('\n')
  // write the metrics to the file
  fs.writeFileSync(metricsFileName, metrics)
})

.circleci/config.yml

version: 2.1
orbs:
  browser-tools: circleci/browser-tools@1.2.3
jobs:
  build:
    docker:
      - image: cimg/node:16.13-browsers
    working_directory: ~/your-project
    steps:
      - checkout
      - browser-tools/install-chrome
      - run: npm install
      - run: npm run build
      - run: sudo npm install -g @lhci/cli@0.12.x
      - run: lhci autorun

machine-setup.sh

#!/bin/bash

set -euxo pipefail

# Add Chrome's apt-key
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google.list
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -

# Add Node's apt-key
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -

# Install NodeJS and Google Chrome
sudo apt-get update
sudo apt-get install -y nodejs google-chrome-stable

job.sh


#!/bin/bash

set -euxo pipefail

npm install
npm run build

export CHROME_PATH=$(which google-chrome-stable)
export LHCI_BUILD_CONTEXT__EXTERNAL_BUILD_URL="$BUILD_URL"

npm install -g @lhci/cli@0.12.x
lhci autorun

# Documentation externe des dépendances

Lighthouse CI
https://github.com/GoogleChrome/lighthouse-ci#readme

Puppeteer
https://pptr.dev/