diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js index ad8ce8ec36..a88e00d007 100644 --- a/jest-puppeteer.config.js +++ b/jest-puppeteer.config.js @@ -2,12 +2,20 @@ module.exports = { browserContext: 'incognito', browserPerWorker: true, launch: { - // we use --no-sandbox --disable-setuid-sandbox as a workaround for the - // 'No usable sandbox! Update your kernel' error - // see more https://github.com/Googlechrome/puppeteer/issues/290 args: [ + /** + * Workaround for 'No usable sandbox! Update your kernel' error + * see more https://github.com/Googlechrome/puppeteer/issues/290 + */ '--no-sandbox', - '--disable-setuid-sandbox' - ] + '--disable-setuid-sandbox', + + /** + * Prevent empty Chromium startup window + * Tests use their own `browser.newPage()` instead + */ + '--no-startup-window' + ], + waitForInitialPage: false } } diff --git a/jest.config.js b/jest.config.js index f5feb8c22d..6b2a38b1fc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -66,6 +66,9 @@ module.exports = { '!**/*.unit.test.{js,mjs}' ], + // Browser test increased timeout (5s to 15s) + testTimeout: 15000, + // Web server and browser required globalSetup: './config/jest/browser/open.mjs', globalTeardown: './config/jest/browser/close.mjs' diff --git a/lib/puppeteer-helpers.js b/lib/puppeteer-helpers.js index b99bfab2f8..7e04fb32b7 100644 --- a/lib/puppeteer-helpers.js +++ b/lib/puppeteer-helpers.js @@ -1,6 +1,9 @@ const { componentNameToJavaScriptClassName } = require('./helper-functions.js') const { renderHtml } = require('./jest-helpers.js') +const configPaths = require('../config/paths.js') +const PORT = configPaths.ports.test + /** * Render and initialise a component within test boilerplate HTML * @@ -13,9 +16,9 @@ const { renderHtml } = require('./jest-helpers.js') * (which lets you instantiate it a different way, like using `initAll`, * or run arbitrary code) * + * @param {import('puppeteer').Page} page - Puppeteer page object * @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 @@ -23,10 +26,10 @@ const { renderHtml } = require('./jest-helpers.js') * browser to execute arbitrary initialisation. Receives an object with the * passed configuration as `config` and the PascalCased component name as * `componentClassName` - * @returns {Promise} + * @returns {Promise} Puppeteer page object */ -async function renderAndInitialise (componentName, options = {}) { - await page.goto(`${options.baseUrl}/tests/boilerplate`, { waitUntil: 'load' }) +async function renderAndInitialise (page, componentName, options = {}) { + await goTo(page, '/tests/boilerplate') const html = renderHtml(componentName, options.nunjucksParams) @@ -51,6 +54,54 @@ async function renderAndInitialise (componentName, options = {}) { return page } +/** + * Navigate to path + * + * @param {import('puppeteer').Page} page - Puppeteer page object + * @param {URL['pathname']} path - URL path + * @returns {Promise} Puppeteer page object + */ +async function goTo (page, path) { + const { href } = new URL(path, `http://localhost:${PORT}`) + + await page.goto(href) + await page.bringToFront() + + return page +} + +/** + * Navigate to example + * + * @param {import('puppeteer').Page} page - Puppeteer page object + * @param {string} exampleName - Example name + * @param {import('puppeteer').WaitForOptions} [options] Navigation options (optional) + * @returns {Promise} Puppeteer page object + */ +function goToExample (page, exampleName, options) { + return goTo(page, `/examples/${exampleName}`, options) +} + +/** + * Navigate to component preview page + * + * @param {import('puppeteer').Page} page - Puppeteer page object + * @param {string} componentName - Component name + * @param {object} [options] - Component options + * @param {string} options.exampleName - Example name + * @returns {Promise} Puppeteer page object + */ +function goToComponent (page, componentName, { exampleName } = {}) { + const componentPath = exampleName + ? `/components/${componentName}/${exampleName}/preview` + : `/components/${componentName}/preview` + + return goTo(page, componentPath) +} + module.exports = { - renderAndInitialise + renderAndInitialise, + goTo, + goToComponent, + goToExample } diff --git a/src/govuk/all.test.js b/src/govuk/all.test.js index dc9ce9530a..d0a72e1c9e 100644 --- a/src/govuk/all.test.js +++ b/src/govuk/all.test.js @@ -5,11 +5,9 @@ const sassdoc = require('sassdoc') const configPaths = require('../../config/paths.js') -const PORT = configPaths.ports.test const { renderSass } = require('../../lib/jest-helpers') - -const baseUrl = 'http://localhost:' + PORT +const { goTo, goToExample } = require('../../lib/puppeteer-helpers') beforeAll(() => { // Capture JavaScript errors. @@ -24,21 +22,21 @@ beforeAll(() => { describe('GOV.UK Frontend', () => { describe('javascript', () => { it('can be accessed via `GOVUKFrontend`', async () => { - await page.goto(baseUrl + '/', { waitUntil: 'load' }) + await goTo(page, '/') const GOVUKFrontendGlobal = await page.evaluate(() => window.GOVUKFrontend) expect(typeof GOVUKFrontendGlobal).toBe('object') }) it('exports `initAll` function', async () => { - await page.goto(baseUrl + '/', { waitUntil: 'load' }) + await goTo(page, '/') const typeofInitAll = await page.evaluate(() => typeof window.GOVUKFrontend.initAll) expect(typeofInitAll).toEqual('function') }) it('exports Components', async () => { - await page.goto(baseUrl + '/', { waitUntil: 'load' }) + await goTo(page, '/') const GOVUKFrontendGlobal = await page.evaluate(() => window.GOVUKFrontend) @@ -60,7 +58,7 @@ describe('GOV.UK Frontend', () => { ]) }) it('exported Components have an init function', async () => { - await page.goto(baseUrl + '/', { waitUntil: 'load' }) + await goTo(page, '/') var componentsWithoutInitFunctions = await page.evaluate(() => { var components = Object.keys(window.GOVUKFrontend) @@ -75,7 +73,7 @@ describe('GOV.UK Frontend', () => { expect(componentsWithoutInitFunctions).toEqual([]) }) it('can be initialised scoped to certain sections of the page', async () => { - await page.goto(baseUrl + '/examples/scoped-initialisation', { waitUntil: 'load' }) + await goToExample(page, 'scoped-initialisation') // To test that certain parts of the page are scoped we have two similar components // that we can interact with to check if they're interactive. diff --git a/src/govuk/components/accordion/accordion.test.js b/src/govuk/components/accordion/accordion.test.js index 29ff7533de..9a8e34d3e1 100644 --- a/src/govuk/components/accordion/accordion.test.js +++ b/src/govuk/components/accordion/accordion.test.js @@ -2,10 +2,7 @@ * @jest-environment puppeteer */ -const configPaths = require('../../../../config/paths.js') -const PORT = configPaths.ports.test - -const baseUrl = 'http://localhost:' + PORT +const { goToComponent, goToExample } = require('../../../../lib/puppeteer-helpers') describe('/components/accordion', () => { describe('/components/accordion/preview', () => { @@ -19,7 +16,7 @@ describe('/components/accordion', () => { }) it('falls back to making all accordion sections visible', async () => { - await page.goto(baseUrl + '/components/accordion/preview', { waitUntil: 'load' }) + await goToComponent(page, 'accordion') const numberOfExampleSections = 2 @@ -32,7 +29,7 @@ describe('/components/accordion', () => { }) it('does not display "↓/↑" in the section headings', async () => { - await page.goto(baseUrl + '/components/accordion/preview', { waitUntil: 'load' }) + await goToComponent(page, 'accordion') const numberOfIcons = await page.evaluate(() => document.body.querySelectorAll('.govuk-accordion .govuk-accordion__section .govuk-accordion-nav__chevron').length) expect(numberOfIcons).toEqual(0) @@ -46,7 +43,7 @@ describe('/components/accordion', () => { }) it('should indicate that the sections are not expanded', async () => { - await page.goto(baseUrl + '/components/accordion/preview', { waitUntil: 'load' }) + await goToComponent(page, 'accordion') const numberOfExampleSections = 2 @@ -60,7 +57,7 @@ describe('/components/accordion', () => { }) it('should change the Show all sections button to Hide all sections when both sections are opened', async () => { - await page.goto(baseUrl + '/components/accordion/preview', { waitUntil: 'load' }) + await goToComponent(page, 'accordion') await page.click('.govuk-accordion .govuk-accordion__section:nth-of-type(2) .govuk-accordion__section-header') await page.click('.govuk-accordion .govuk-accordion__section:nth-of-type(3) .govuk-accordion__section-header') @@ -72,7 +69,7 @@ describe('/components/accordion', () => { }) it('should open both sections when the Show all sections button is clicked', async () => { - await page.goto(baseUrl + '/components/accordion/preview', { waitUntil: 'load' }) + await goToComponent(page, 'accordion') await page.click('.govuk-accordion__show-all') @@ -86,7 +83,9 @@ describe('/components/accordion', () => { }) it('should already have all sections open if they have the expanded class', async () => { - await page.goto(baseUrl + '/components/accordion/with-all-sections-already-open/preview', { waitUntil: 'load' }) + await goToComponent(page, 'accordion', { + exampleName: 'with-all-sections-already-open' + }) const numberOfExampleSections = 2 @@ -106,7 +105,7 @@ describe('/components/accordion', () => { it('should maintain the expanded state after a page refresh', async () => { const sectionHeaderButton = '.govuk-accordion .govuk-accordion__section:nth-of-type(2) .govuk-accordion__section-button' - await page.goto(baseUrl + '/components/accordion/preview', { waitUntil: 'load' }) + await goToComponent(page, 'accordion') await page.click(sectionHeaderButton) const expandedState = await page.evaluate((sectionHeaderButton) => { @@ -125,7 +124,7 @@ describe('/components/accordion', () => { }) it('should transform the button span to