Skip to content

Commit

Permalink
Update Puppeteer render() to bypass Review app
Browse files Browse the repository at this point in the history
  • Loading branch information
colinrotherham committed Oct 18, 2023
1 parent 78bcdcb commit 510437b
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 61 deletions.
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"@types/jest": "^29.5.5",
"@types/jest-axe": "^3.5.6",
"@types/js-yaml": "^4.0.7",
"@types/mime-types": "^2.1.3",
"@types/node": "^20.8.6",
"@types/nunjucks": "^3.2.4",
"@types/slug": "^5.0.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
const { goTo, goToExample } = require('@govuk-frontend/helpers/puppeteer')

describe('GOV.UK Frontend', () => {
describe('javascript', () => {
describe('JavaScript', () => {
let exported

beforeEach(async () => {
await goTo(page, '/tests/boilerplate')
await goTo(page, '/')

// Exports available via browser dynamic import
exported = await page.evaluate(async () =>
Object.keys(await import('govuk-frontend'))
exported = await page.evaluate(
async (importPath) => Object.keys(await import(importPath)),
'/javascripts/govuk-frontend.min.js'
)
})

it('exports `initAll` function', async () => {
await goTo(page, '/tests/boilerplate')

const typeofInitAll = await page.evaluate(async (utility) => {
const namespace = await import('govuk-frontend')
return typeof namespace[utility]
}, 'initAll')
await goTo(page, '/')

const typeofInitAll = await page.evaluate(
async (importPath, exportName) => {
const namespace = await import(importPath)
return typeof namespace[exportName]
},
'/javascripts/govuk-frontend.min.js',
'initAll'
)

expect(typeofInitAll).toEqual('function')
})
Expand Down
1 change: 1 addition & 0 deletions shared/helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"jest-environment-jsdom": "^29.7.0",
"jest-environment-node-single-context": "^29.1.0",
"jest-environment-puppeteer": "^9.0.0",
"mime-types": "^2.1.35",
"outdent": "^0.8.0",
"puppeteer": "^21.3.8",
"sass-embedded": "^1.69.2",
Expand Down
159 changes: 108 additions & 51 deletions shared/helpers/puppeteer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
const { readFile } = require('fs/promises')
const { join } = require('path')

const { AxePuppeteer } = require('@axe-core/puppeteer')
const { ports } = require('@govuk-frontend/config')
const { ports, paths } = require('@govuk-frontend/config')
const { renderPreview } = require('@govuk-frontend/lib/components')
const { componentNameToClassName } = require('@govuk-frontend/lib/names')
const mime = require('mime-types')
const slug = require('slug')

/**
Expand Down Expand Up @@ -76,72 +80,125 @@ async function axe(page, overrides = {}) {
* @returns {Promise<import('puppeteer').Page>} Puppeteer page object
*/
async function render(page, componentName, renderOptions, browserOptions) {
page.setRequestInterception(true)
page.on('request', renderMiddleware)

const exampleName = renderOptions.fixture?.name ?? 'default'
const exportName = componentNameToClassName(componentName)
const selector = `[data-module="govuk-${componentName}"]`

await goToComponent(page, componentName, { exampleName })
await goToComponent(page, componentName, {
baseUrl: new URL('file://'),
exampleName
})

// Inject rendered HTML into the page
await page.setContent(renderPreview(componentName, renderOptions))

// Call `beforeInitialisation` in a separate `$eval` call
// as running it inside the body of the next `evaluate`
// didn't provide a reliable execution
if (browserOptions?.beforeInitialisation) {
await page.$eval(
try {
// Call `beforeInitialisation` in a separate `$eval` call
// as running it inside the body of the next `evaluate`
// didn't provide a reliable execution
if (browserOptions?.beforeInitialisation) {
await page.$eval(
selector,
browserOptions.beforeInitialisation,
browserOptions.context
)
}

// Run a script to init the JavaScript component
//
// Use `evaluate` to ensure we run `document.querySelector` inside the
// browser, like users would, rather than rely on Puppeteer looking for the
// element which would cause an error in Jest-land rather than within the
// browser if the element is missingß
//
// Puppeteer returns very little information on errors thrown during
// `evaluate`, only a `name` that maps to the error class (and not its `name`
// property, which means we get a mangled value). As a workaround, we can
// gather and `return` the values we need from inside the browser
const error = await page.evaluate(
async (selector, exportName, config) => {
const namespace = await import('govuk-frontend')

// Find all matching modules
const $modules = document.querySelectorAll(selector)

try {
// Loop and initialise all $modules or use default
// selector `null` return value when none found
;($modules.length ? $modules : [null]).forEach(
($module) => new namespace[exportName]($module, config)
)
} catch ({ name, message }) {
return { name, message }
}
},
selector,
browserOptions.beforeInitialisation,
browserOptions.context
exportName,
browserOptions?.config
)
}

// Run a script to init the JavaScript component
//
// Use `evaluate` to ensure we run `document.querySelector` inside the
// browser, like users would, rather than rely on Puppeteer looking for the
// element which would cause an error in Jest-land rather than within the
// browser if the element is missingß
//
// Puppeteer returns very little information on errors thrown during
// `evaluate`, only a `name` that maps to the error class (and not its `name`
// property, which means we get a mangled value). As a workaround, we can
// gather and `return` the values we need from inside the browser
const error = await page.evaluate(
async (selector, exportName, config) => {
const namespace = await import('govuk-frontend')

// Find all matching modules
const $modules = document.querySelectorAll(selector)

try {
// Loop and initialise all $modules or use default
// selector `null` return value when none found
;($modules.length ? $modules : [null]).forEach(
($module) => new namespace[exportName]($module, config)
)
} catch ({ name, message }) {
return { name, message }
}
},
selector,
exportName,
browserOptions?.config
)

// Throw Puppeteer errors back to Jest unless component example,
// but skip errors when component example is known to throw already
if (error && !renderOptions.fixture?.errors) {
throw new Error(
`Initialising \`new ${exportName}()\` with example '${exampleName}' threw:` +
`\n\t${error.name}: ${error.message}`,
{ cause: error }
)
// Throw Puppeteer errors back to Jest unless component example,
// but skip errors when component example is known to throw already
if (error && !renderOptions.fixture?.errors) {
throw new Error(
`Initialising \`new ${exportName}()\` with example '${exampleName}' threw:` +
`\n\t${error.name}: ${error.message}`,
{ cause: error }
)
}
} finally {
page.setRequestInterception(false)
page.off('request', renderMiddleware)
}

return page
}

/**
* Component preview HTTP request middleware
*
* @param {import('puppeteer').HTTPRequest} request - Puppeteer HTTP request
* @returns {Promise<void>}
*/
async function renderMiddleware(request) {
if (request.isInterceptResolutionHandled()) {
return
}

const { href, pathname, protocol } = new URL(request.url())

// Only mock file:// URLs when not cached
if (protocol === 'file:' && !renderMiddlewareAssets.has(href)) {
if (pathname.startsWith('/components/')) {
renderMiddlewareAssets.set(href, {
body: '<!doctype html>',
contentType: 'text/html'
})
}

if (pathname.startsWith('/assets/')) {
renderMiddlewareAssets.set(href, {
body: await readFile(join(paths.package, `dist/govuk/${pathname}`)),
contentType: mime.lookup(pathname) || 'text/plain'
})
}
}

return renderMiddlewareAssets.has(href)
? request.respond(renderMiddlewareAssets.get(href))
: request.continue()
}

/**
* Cache request middleware assets
*
* @type {Map<string, Partial<import('puppeteer').ResponseForRequest>>}
*/
const renderMiddlewareAssets = new Map()

/**
* Navigate to path
*
Expand Down

0 comments on commit 510437b

Please sign in to comment.