Skip to content

Commit

Permalink
Merge pull request #2878 from alphagov/reduce-percy-usage
Browse files Browse the repository at this point in the history
Reduce GitHub workflow runs (including Percy screenshot uploads)
  • Loading branch information
colinrotherham committed Sep 30, 2022
2 parents 0cbe83f + 83736b7 commit b2f95c4
Show file tree
Hide file tree
Showing 17 changed files with 387 additions and 214 deletions.
15 changes: 14 additions & 1 deletion .github/workflows/sass.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
name: Sass

on: [push, pull_request]
on:
pull_request:

push:
branches:
- main
- 'feature/**'
- 'support/**'

workflow_dispatch:

concurrency:
group: sass-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
dart-sass:
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
name: Tests

on: [push, pull_request]
on:
pull_request:

push:
branches:
- main
- 'feature/**'
- 'support/**'

workflow_dispatch:

concurrency:
group: tests-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
run-tests:
Expand Down
10 changes: 10 additions & 0 deletions config/jest/browser/open.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import { setup } from 'jest-environment-puppeteer'
* @param {import('jest').Config} jestConfig
*/
export default async function browserOpen (jestConfig) {
const { maxWorkers } = jestConfig

/**
* Increase Node.js max listeners warning threshold by Jest --maxWorkers
* Allows jest-puppeteer.config.js `browserPerWorker: true` to open multiple browsers
*/
if (Number.isFinite(maxWorkers)) {
process.setMaxListeners(1 + maxWorkers)
}

await serverStart() // Wait for web server
await setup(jestConfig) // Open browser
}
14 changes: 7 additions & 7 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = {
'jest-serializer-html'
],
testMatch: [
'**/template.test.js'
'**/(*.)?template.test.js'
]
},
{
Expand All @@ -39,13 +39,13 @@ module.exports = {
testMatch: [
'**/*.test.js',

// Exclude unit/snapshot tests
'!**/template.test.js',
// Exclude macro/unit tests
'!**/(*.)?template.test.js',
'!**/*.unit.test.{js,mjs}',

// Exclude other tests
'!**/all.test.js',
'!**/components/**',
'!**/components/*/**',
'!**/gulp/**'
],

Expand All @@ -59,10 +59,10 @@ module.exports = {
setupFilesAfterEnv: [require.resolve('expect-puppeteer')],
testMatch: [
'**/all.test.js',
'**/components/**/*.test.js',
'**/components/*/*.test.js',

// Exclude unit/snapshot tests
'!**/template.test.js',
// Exclude macro/unit tests
'!**/(*.)?template.test.js',
'!**/*.unit.test.{js,mjs}'
],

Expand Down
2 changes: 1 addition & 1 deletion lib/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
overrides: [
{
files: ['jest-*'],
files: ['puppeteer-*'],
globals: {
page: true,
browser: true
Expand Down
12 changes: 0 additions & 12 deletions lib/axe-helper.js

This file was deleted.

72 changes: 55 additions & 17 deletions lib/file-helper.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,83 @@
const fs = require('fs')
const { readdirSync, readFileSync, statSync } = require('fs')
const path = require('path')
const yaml = require('js-yaml')
const fm = require('front-matter')

const configPaths = require('../config/paths.js')

const childDirectories = dir => {
return fs.readdirSync(dir)
.filter(file => fs.statSync(path.join(dir, file)).isDirectory())
/**
* Directory listing for path
*
* @param {string} directoryPath
* @returns {{ basename: string; stats: import('fs').Stats }[]} entries
*/
const getListing = (directoryPath) => {
const listing = readdirSync(directoryPath)

// Loop through listing entries
return listing.map(basename => ({
basename, stats: statSync(path.join(directoryPath, basename))
}))
}

// Generate component list from source directory, excluding anything that's not
// a directory (for example, .DS_Store files)
exports.allComponents = childDirectories(configPaths.components)
/**
* Directory listing (directories only)
*
* @param {string} directoryPath
* @returns {string[]} directories
*/
const getDirectories = (directoryPath) => {
const entries = getListing(directoryPath)

// Read the contents of a file from a given path
const readFileContents = filePath => {
return fs.readFileSync(filePath, 'utf8')
return entries
.filter(({ stats }) => stats.isDirectory())
.map(({ basename: directory }) => directory)
}

exports.readFileContents = readFileContents
/**
* Directory listing (files only)
*
* @param {string} directoryPath
* @returns {string[]} directories
*/
const getFiles = (directoryPath) => {
const entries = getListing(directoryPath)

return entries
.filter(({ stats }) => stats.isFile())
.map(({ basename: file }) => file)
}

// Generate component list from source directory, excluding anything that's not
// a directory (for example, .DS_Store files)
const allComponents = getDirectories(configPaths.components)

const getComponentData = componentName => {
try {
const yamlPath = path.join(configPaths.components, componentName, `${componentName}.yaml`)
return yaml.load(
fs.readFileSync(yamlPath, 'utf8'), { json: true }
readFileSync(yamlPath, 'utf8'), { json: true }
)
} catch (error) {
throw new Error(error)
}
}

exports.getComponentData = getComponentData

exports.fullPageExamples = () => {
return childDirectories(path.resolve(configPaths.fullPageExamples))
const fullPageExamples = () => {
return getDirectories(path.resolve(configPaths.fullPageExamples))
.map(folderName => ({
name: folderName,
path: folderName,
...fm(readFileContents(path.join(configPaths.fullPageExamples, folderName, 'index.njk'))).attributes
...fm(readFileSync(path.join(configPaths.fullPageExamples, folderName, 'index.njk'), 'utf8')).attributes
}))
.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1)
}

module.exports = {
allComponents,
fullPageExamples,
getComponentData,
getDirectories,
getFiles,
getListing
}
63 changes: 11 additions & 52 deletions lib/jest-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const sass = require('node-sass')
const sassRender = util.promisify(sass.render)

const configPaths = require('../config/paths.js')
const { componentNameToMacroName, componentNameToJavaScriptClassName } = require('./helper-functions.js')
const { componentNameToMacroName } = require('./helper-functions.js')

const nunjucksEnv = nunjucks.configure([configPaths.src, configPaths.components], {
trimBlocks: true,
Expand Down Expand Up @@ -82,56 +82,6 @@ function renderTemplate (params = {}, blocks = {}) {
return cheerio.load(output)
}

/**
* Render and initialise a component within test boilerplate HTML
*
* Renders a component's Nunjucks macro with the given params, injects it into
* the test boilerplate page, then either:
*
* - instantiates the component class, passing the provided JavaScript
* configuration, and calls the init function
* - runs the passed initialiser function inside the browser
* (which lets you instantiate it a different way, like using `initAll`,
* or run arbitrary code)
*
* @param {String} componentName - The kebab-cased name of the component
* @param {Object} options
* @param {String} options.baseUrl - The base URL of the test server
* @param {Object} options.nunjucksParams - Params passed to the Nunjucks macro
* @param {Object} [options.javascriptConfig] - The configuration hash passed to
* the component's class for initialisation
* @param {Function} [options.initialiser] - A function that'll run in the
* browser to execute arbitrary initialisation. Receives an object with the
* passed configuration as `config` and the PascalCased component name as
* `componentClassName`
* @returns {Promise<import('puppeteer').Page>}
*/
async function renderAndInitialise (componentName, options = {}) {
await page.goto(`${options.baseUrl}/tests/boilerplate`, { waitUntil: 'load' })

const html = renderHtml(componentName, options.nunjucksParams)

// Inject rendered HTML into the page
await page.$eval('#slot', (slot, htmlForSlot) => {
slot.innerHTML = htmlForSlot
}, html)

const initialiser = options.initialiser || function ({ config, componentClassName }) {
const $component = document.querySelector('[data-module]')
new window.GOVUKFrontend[componentClassName]($component, config).init()
}

if (initialiser) {
// Run a script to init the JavaScript component
await page.evaluate(initialiser, {
config: options.javascriptConfig,
componentClassName: componentNameToJavaScriptClassName(componentName)
})
}

return page
}

/**
* Get examples from a component's metadata file
* @param {string} componentName
Expand Down Expand Up @@ -196,4 +146,13 @@ const axe = configureAxe({
}
})

module.exports = { axe, render, renderHtml, renderAndInitialise, getExamples, htmlWithClassName, renderSass, renderTemplate }
module.exports = {
axe,
getExamples,
htmlWithClassName,
nunjucksEnv,
render,
renderHtml,
renderSass,
renderTemplate
}
56 changes: 56 additions & 0 deletions lib/puppeteer-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const { componentNameToJavaScriptClassName } = require('./helper-functions.js')
const { renderHtml } = require('./jest-helpers.js')

/**
* Render and initialise a component within test boilerplate HTML
*
* Renders a component's Nunjucks macro with the given params, injects it into
* the test boilerplate page, then either:
*
* - instantiates the component class, passing the provided JavaScript
* configuration, and calls the init function
* - runs the passed initialiser function inside the browser
* (which lets you instantiate it a different way, like using `initAll`,
* or run arbitrary code)
*
* @param {String} componentName - The kebab-cased name of the component
* @param {Object} options
* @param {String} options.baseUrl - The base URL of the test server
* @param {Object} options.nunjucksParams - Params passed to the Nunjucks macro
* @param {Object} [options.javascriptConfig] - The configuration hash passed to
* the component's class for initialisation
* @param {Function} [options.initialiser] - A function that'll run in the
* browser to execute arbitrary initialisation. Receives an object with the
* passed configuration as `config` and the PascalCased component name as
* `componentClassName`
* @returns {Promise<import('puppeteer').Page>}
*/
async function renderAndInitialise (componentName, options = {}) {
await page.goto(`${options.baseUrl}/tests/boilerplate`, { waitUntil: 'load' })

const html = renderHtml(componentName, options.nunjucksParams)

// Inject rendered HTML into the page
await page.$eval('#slot', (slot, htmlForSlot) => {
slot.innerHTML = htmlForSlot
}, html)

const initialiser = options.initialiser || function ({ config, componentClassName }) {
const $component = document.querySelector('[data-module]')
new window.GOVUKFrontend[componentClassName]($component, config).init()
}

if (initialiser) {
// Run a script to init the JavaScript component
await page.evaluate(initialiser, {
config: options.javascriptConfig,
componentClassName: componentNameToJavaScriptClassName(componentName)
})
}

return page
}

module.exports = {
renderAndInitialise
}

0 comments on commit b2f95c4

Please sign in to comment.