From 9540bef921e57b5ef75347935760c0725d3184f4 Mon Sep 17 00:00:00 2001 From: Colin Rotherham Date: Mon, 14 Aug 2023 11:33:22 +0100 Subject: [PATCH] Run `prettier --write` --- .eslintrc.js | 6 +- __tests__/accessibility-audit.test.js | 2 +- __tests__/back-to-top.test.js | 7 +- __tests__/component-options.test.js | 55 ++++-- __tests__/cookie-banner.test.js | 26 ++- __tests__/cookie-functions.test.mjs | 83 ++++++--- __tests__/cookies-page.test.js | 2 +- __tests__/navigation.test.js | 28 ++- __tests__/search.test.js | 109 ++++++++---- __tests__/tabs.test.js | 34 ++-- babel.config.js | 13 +- jest-puppeteer.config.js | 4 +- lib/colours.js | 19 +- lib/extract-page-headings/index.js | 15 +- lib/extract-page-headings/index.test.js | 42 ++++- lib/file-helper.js | 13 +- lib/fingerprints/index.js | 16 +- lib/fingerprints/index.test.js | 54 +++--- lib/generate-sitemap.js | 55 +++--- lib/get-macro-options/__mocks__/fs.js | 2 +- lib/get-macro-options/__tests__/index.js | 44 +++-- lib/get-macro-options/index.js | 98 +++++----- lib/highlighter.js | 5 +- lib/marked-renderer.js | 13 +- lib/metalsmith-lunr-index/index.js | 83 ++++----- lib/metalsmith-lunr-index/index.test.js | 35 ++-- lib/metalsmith-title-checker.js | 58 +++--- lib/metalsmith.js | 167 ++++++++++-------- lib/navigation.js | 16 +- lib/puppeteer-helpers.js | 10 +- lib/rollup/index.js | 5 +- lib/rollup/index.test.js | 6 +- rollup.config.js | 28 ++- src/javascripts/application.mjs | 9 +- src/javascripts/components/analytics.mjs | 5 +- src/javascripts/components/back-to-top.mjs | 64 +++---- src/javascripts/components/cookie-banner.mjs | 35 +++- .../components/cookie-functions.mjs | 43 +++-- src/javascripts/components/cookies-page.mjs | 51 ++++-- src/javascripts/components/copy.mjs | 2 +- src/javascripts/components/example.mjs | 2 +- src/javascripts/components/helpers.mjs | 2 +- src/javascripts/components/navigation.mjs | 40 +++-- src/javascripts/components/options-table.mjs | 16 +- src/javascripts/components/search.mjs | 24 ++- .../components/search.tracking.mjs | 49 ++--- src/javascripts/components/tabs.mjs | 61 ++++--- src/javascripts/example.mjs | 2 +- tasks/build.js | 4 +- 49 files changed, 948 insertions(+), 614 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 44ba9bc006..7026349274 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -21,11 +21,7 @@ module.exports = { parserOptions: { ecmaVersion: 'latest' }, - plugins: [ - 'import', - 'n', - 'promise' - ], + plugins: ['import', 'n', 'promise'], rules: { // Check import or require statements are A-Z ordered 'import/order': [ diff --git a/__tests__/accessibility-audit.test.js b/__tests__/accessibility-audit.test.js index 5061cd4868..f196634b9b 100644 --- a/__tests__/accessibility-audit.test.js +++ b/__tests__/accessibility-audit.test.js @@ -2,7 +2,7 @@ const { AxePuppeteer } = require('@axe-core/puppeteer') const { goTo } = require('../lib/puppeteer-helpers.js') -async function analyze (page, path) { +async function analyze(page, path) { await goTo(page, path) const axe = new AxePuppeteer(page) diff --git a/__tests__/back-to-top.test.js b/__tests__/back-to-top.test.js index 1e821fe766..faaa413416 100644 --- a/__tests__/back-to-top.test.js +++ b/__tests__/back-to-top.test.js @@ -5,15 +5,16 @@ describe('Back to top', () => { let $backToTopLink let pageHeight - async function setup (page) { + async function setup(page) { $module = await page.$('[data-module="app-back-to-top"]') $backToTopLink = await $module.$('a') // Scrollable height of body - pageHeight = await page.$eval('body', ($element) => $element.scrollHeight) ?? 0 + pageHeight = + (await page.$eval('body', ($element) => $element.scrollHeight)) ?? 0 } - function scrollTo (page, scrollY) { + function scrollTo(page, scrollY) { return page.evaluate((y) => window.scroll(0, y), scrollY) } diff --git a/__tests__/component-options.test.js b/__tests__/component-options.test.js index 08b92a9d1f..473e5e8e4b 100644 --- a/__tests__/component-options.test.js +++ b/__tests__/component-options.test.js @@ -4,8 +4,10 @@ describe('Component page', () => { it('should contain a "Nunjucks" tab heading', async () => { await goTo(page, '/components/back-link/') - const nunjucksTabHeadings = await page.evaluate(() => Array.from(document.querySelectorAll('.js-tabs__item a')) - .filter(element => element.textContent === 'Nunjucks') + const nunjucksTabHeadings = await page.evaluate(() => + Array.from(document.querySelectorAll('.js-tabs__item a')).filter( + (element) => element.textContent === 'Nunjucks' + ) ) expect(nunjucksTabHeadings[0]).toBeTruthy() @@ -15,9 +17,10 @@ describe('Component page', () => { await goTo(page, '/components/back-link/') // Get "aria-controls" attributes from "Nunjucks" tab headings - const nunjucksTabHeadingControls = await page.evaluateHandle(() => Array.from(document.querySelectorAll('.js-tabs__item a')) - .filter(element => element.textContent === 'Nunjucks') - .map(element => element.getAttribute('aria-controls')) + const nunjucksTabHeadingControls = await page.evaluateHandle(() => + Array.from(document.querySelectorAll('.js-tabs__item a')) + .filter((element) => element.textContent === 'Nunjucks') + .map((element) => element.getAttribute('aria-controls')) ) const tabContentIds = await nunjucksTabHeadingControls.jsonValue() // Returns Puppeteer JSONHandle @@ -25,8 +28,15 @@ describe('Component page', () => { const id = tabContentIds[0] // Get summary text of details element in "Nunjucks" tab - const nunjucksTabHeadings = await page.evaluate(id => Array.from(document.getElementById(id).querySelectorAll('.govuk-details__summary-text')) - .map(element => element.textContent.trim()), id) + const nunjucksTabHeadings = await page.evaluate( + (id) => + Array.from( + document + .getElementById(id) + .querySelectorAll('.govuk-details__summary-text') + ).map((element) => element.textContent.trim()), + id + ) expect(nunjucksTabHeadings).toContain('Nunjucks macro options') }) @@ -35,18 +45,32 @@ describe('Component page', () => { await goTo(page, '/components/back-link/') // Get "aria-controls" attributes from "Nunjucks" tab headings - const nunjucksTabHeadingControls = await page.evaluateHandle(() => Array.from(document.querySelectorAll('.js-tabs__item a')) - .filter(element => element.textContent === 'Nunjucks') - .map(element => element.getAttribute('aria-controls'))) + const nunjucksTabHeadingControls = await page.evaluateHandle(() => + Array.from(document.querySelectorAll('.js-tabs__item a')) + .filter((element) => element.textContent === 'Nunjucks') + .map((element) => element.getAttribute('aria-controls')) + ) const tabContentIds = await nunjucksTabHeadingControls.jsonValue() // Returns Puppeteer JSONHandle const id = tabContentIds[0] // Get table headings of table inside details element in "Nunjucks" tab - const nunjucksTableHeadings = await page.evaluate(id => Array.from(document.getElementById(id).querySelector('.govuk-details__text .govuk-table .govuk-table__head').querySelectorAll('.govuk-table__header')) - .map(element => element.textContent.trim()), id) + const nunjucksTableHeadings = await page.evaluate( + (id) => + Array.from( + document + .getElementById(id) + .querySelector( + '.govuk-details__text .govuk-table .govuk-table__head' + ) + .querySelectorAll('.govuk-table__header') + ).map((element) => element.textContent.trim()), + id + ) - expect(nunjucksTableHeadings.sort()).toEqual(['Name', 'Type', 'Description'].sort()) + expect(nunjucksTableHeadings.sort()).toEqual( + ['Name', 'Type', 'Description'].sort() + ) }) it('macro options should be opened and in view when linked to', async () => { @@ -61,7 +85,10 @@ describe('Component page', () => { }) it('macro options subtable should be opened and in view when linked to', async () => { - await goTo(page, '/components/text-input/#options-text-input-example--label') + await goTo( + page, + '/components/text-input/#options-text-input-example--label' + ) // Check if example's macro options details element is open await page.waitForSelector('#options-text-input-example-details[open=open]') diff --git a/__tests__/cookie-banner.test.js b/__tests__/cookie-banner.test.js index c1523bc045..305f70384e 100644 --- a/__tests__/cookie-banner.test.js +++ b/__tests__/cookie-banner.test.js @@ -18,7 +18,7 @@ describe('Cookie banner', () => { url: `http://localhost:${ports.preview}` } - async function setup (page) { + async function setup(page) { $module = await page.$('[data-module="govuk-cookie-banner"]') $message = await $module.$('.js-cookie-banner-message') @@ -27,8 +27,12 @@ describe('Cookie banner', () => { $buttonReject = await $module.$('.js-cookie-banner-reject') // Accept or reject confirmation messages - $confirmationAccept = await $module.$('.js-cookie-banner-confirmation-accept') - $confirmationReject = await $module.$('.js-cookie-banner-confirmation-reject') + $confirmationAccept = await $module.$( + '.js-cookie-banner-confirmation-accept' + ) + $confirmationReject = await $module.$( + '.js-cookie-banner-confirmation-reject' + ) } beforeEach(async () => { @@ -117,7 +121,9 @@ describe('Cookie banner', () => { it('moves user focus to the confirmation message', async () => { await $buttonAccept.click() - await expect(getAttribute($confirmationAccept, 'tabindex')).resolves.toEqual('-1') + await expect( + getAttribute($confirmationAccept, 'tabindex') + ).resolves.toEqual('-1') }) }) @@ -148,13 +154,17 @@ describe('Cookie banner', () => { it('moves user focus to the confirmation message', async () => { await $buttonReject.click() - await expect(getAttribute($confirmationReject, 'tabindex')).resolves.toEqual('-1') + await expect( + getAttribute($confirmationReject, 'tabindex') + ).resolves.toEqual('-1') }) }) describe('hide button', () => { it('hides the accept confirmation message', async () => { - const $buttonAcceptHide = await $module.$('.js-cookie-banner-hide--accept') + const $buttonAcceptHide = await $module.$( + '.js-cookie-banner-hide--accept' + ) // Accept cookies await $buttonAccept.click() @@ -172,7 +182,9 @@ describe('Cookie banner', () => { }) it('hides the reject confirmation message', async () => { - const $buttonRejectHide = await $module.$('.js-cookie-banner-hide--reject') + const $buttonRejectHide = await $module.$( + '.js-cookie-banner-hide--reject' + ) // Reject cookies await $buttonReject.click() diff --git a/__tests__/cookie-functions.test.mjs b/__tests__/cookie-functions.test.mjs index 88dcecbbdd..f6cf6e424c 100644 --- a/__tests__/cookie-functions.test.mjs +++ b/__tests__/cookie-functions.test.mjs @@ -18,7 +18,11 @@ describe('Cookie settings', () => { cookies.forEach(function (cookie) { const name = cookie.split('=')[0] document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' - document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;domain=' + window.location.hostname + ';path=/' + document.cookie = + name + + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;domain=' + + window.location.hostname + + ';path=/' }) }) @@ -42,7 +46,8 @@ describe('Cookie settings', () => { afterEach(() => { // Delete test cookies document.cookie = 'myCookie=;expires=Thu, 01 Jan 1970 00:00:00 UTC' - document.cookie = 'design_system_cookies_policy=;expires=Thu, 01 Jan 1970 00:00:00 UTC' + document.cookie = + 'design_system_cookies_policy=;expires=Thu, 01 Jan 1970 00:00:00 UTC' }) it('doesnt set a cookie with a value if not a recognised name', async () => { @@ -61,23 +66,35 @@ describe('Cookie settings', () => { }) it('sets allowed cookie with no options', async () => { - CookieHelpers.Cookie('design_system_cookies_policy', '{"analytics":false}') - - expect(document.cookie).toEqual('design_system_cookies_policy={"analytics":false}') + CookieHelpers.Cookie( + 'design_system_cookies_policy', + '{"analytics":false}' + ) + + expect(document.cookie).toEqual( + 'design_system_cookies_policy={"analytics":false}' + ) }) it('sets allowed cookie with options', async () => { - CookieHelpers.Cookie('design_system_cookies_policy', '{"analytics":false}', { days: 100 }) + CookieHelpers.Cookie( + 'design_system_cookies_policy', + '{"analytics":false}', + { days: 100 } + ) // Annoyingly JS can't retrieve expiry date directly from document.cookie, this is all we can assert - expect(document.cookie).toEqual('design_system_cookies_policy={"analytics":false}') + expect(document.cookie).toEqual( + 'design_system_cookies_policy={"analytics":false}' + ) }) }) describe('getConsentCookie', () => { afterEach(() => { // Delete consent cookie - document.cookie = 'design_system_cookies_policy=;expires=Thu, 01 Jan 1970 00:00:00 UTC' + document.cookie = + 'design_system_cookies_policy=;expires=Thu, 01 Jan 1970 00:00:00 UTC' }) it('returns null if consent cookie not present', async () => { @@ -85,9 +102,13 @@ describe('Cookie settings', () => { }) it('returns consent cookie object if present', async () => { - document.cookie = 'design_system_cookies_policy={"analytics":false,"version":1}' + document.cookie = + 'design_system_cookies_policy={"analytics":false,"version":1}' - expect(CookieHelpers.getConsentCookie()).toEqual({ analytics: false, version: 1 }) + expect(CookieHelpers.getConsentCookie()).toEqual({ + analytics: false, + version: 1 + }) }) }) @@ -99,7 +120,8 @@ describe('Cookie settings', () => { afterEach(() => { // Delete consent cookie - document.cookie = 'design_system_cookies_policy=;expires=Thu, 01 Jan 1970 00:00:00 UTC' + document.cookie = + 'design_system_cookies_policy=;expires=Thu, 01 Jan 1970 00:00:00 UTC' }) describe('to false', () => { @@ -108,7 +130,9 @@ describe('Cookie settings', () => { CookieHelpers.setConsentCookie({ analytics: false }) - expect(document.cookie).toEqual('design_system_cookies_policy={"analytics":false,"version":1}') + expect(document.cookie).toEqual( + 'design_system_cookies_policy={"analytics":false,"version":1}' + ) }) it('does not load the analytics script', async () => { @@ -122,7 +146,9 @@ describe('Cookie settings', () => { CookieHelpers.setConsentCookie({ analytics: false }) - expect(document.cookie).toEqual('design_system_cookies_policy={"analytics":false,"version":1}') + expect(document.cookie).toEqual( + 'design_system_cookies_policy={"analytics":false,"version":1}' + ) // Make sure those analytics cookies are definitely gone expect(CookieHelpers.Cookie('_ga')).toEqual(null) expect(CookieHelpers.Cookie('_gid')).toEqual(null) @@ -136,7 +162,9 @@ describe('Cookie settings', () => { CookieHelpers.setConsentCookie({ analytics: true }) - expect(document.cookie).toEqual('design_system_cookies_policy={"analytics":true,"version":1}') + expect(document.cookie).toEqual( + 'design_system_cookies_policy={"analytics":true,"version":1}' + ) }) it('loads analytics script if consenting to analytics cookies', async () => { @@ -150,7 +178,9 @@ describe('Cookie settings', () => { it('sets consent cookie to default if no options are provided', async () => { CookieHelpers.setConsentCookie() - expect(document.cookie).toEqual('design_system_cookies_policy={"analytics":false,"version":1}') + expect(document.cookie).toEqual( + 'design_system_cookies_policy={"analytics":false,"version":1}' + ) }) }) }) @@ -159,11 +189,14 @@ describe('Cookie settings', () => { it('deletes cookies the user has not consented to', async () => { document.cookie = '_ga=test' document.cookie = '_gid=test' - document.cookie = 'design_system_cookies_policy={"analytics":false,"version":1}' + document.cookie = + 'design_system_cookies_policy={"analytics":false,"version":1}' CookieHelpers.resetCookies() - expect(document.cookie).toEqual('design_system_cookies_policy={"analytics":false,"version":1}') + expect(document.cookie).toEqual( + 'design_system_cookies_policy={"analytics":false,"version":1}' + ) }) it('deletes cookies if the consent cookie is not present', async () => { @@ -176,7 +209,8 @@ describe('Cookie settings', () => { }) it('loads analytics script if user consented to analytics', async () => { - document.cookie = 'design_system_cookies_policy={"analytics":true,"version":1}' + document.cookie = + 'design_system_cookies_policy={"analytics":true,"version":1}' CookieHelpers.resetCookies() @@ -194,7 +228,8 @@ describe('Cookie settings', () => { }) it('re-enables analytics by setting a window property', async () => { - document.cookie = 'design_system_cookies_policy={"analytics":true,"version":1}' + document.cookie = + 'design_system_cookies_policy={"analytics":true,"version":1}' CookieHelpers.resetCookies() @@ -211,7 +246,8 @@ describe('Cookie settings', () => { }) it('Cookie will not set cookies if consent cookie is old version', async () => { - document.cookie = 'design_system_cookies_policy={"analytics":true,"version":0}' + document.cookie = + 'design_system_cookies_policy={"analytics":true,"version":0}' CookieHelpers.Cookie('_ga', 'foo') expect(CookieHelpers.Cookie('_ga')).toEqual(null) @@ -220,11 +256,14 @@ describe('Cookie settings', () => { it('resetCookies deletes cookies if consent cookie is old version', async () => { document.cookie = '_ga=test' document.cookie = '_gid=test' - document.cookie = 'design_system_cookies_policy={"analytics":false,"version":0}' + document.cookie = + 'design_system_cookies_policy={"analytics":false,"version":0}' CookieHelpers.resetCookies() - expect(document.cookie).toEqual('design_system_cookies_policy={"analytics":false,"version":0}') + expect(document.cookie).toEqual( + 'design_system_cookies_policy={"analytics":false,"version":0}' + ) }) }) diff --git a/__tests__/cookies-page.test.js b/__tests__/cookies-page.test.js index 5c493f62fa..3078ed22df 100644 --- a/__tests__/cookies-page.test.js +++ b/__tests__/cookies-page.test.js @@ -8,7 +8,7 @@ describe('Cookies page', () => { let $radioNo let $buttonSave - async function setup (page) { + async function setup(page) { $module = await page.$('[data-module="app-cookies-page"]') $radioYes = await $module.$('input[name="analytics"][value="yes"]') $radioNo = await $module.$('input[name="analytics"][value="no"]') diff --git a/__tests__/navigation.test.js b/__tests__/navigation.test.js index 5956859932..f3dc864435 100644 --- a/__tests__/navigation.test.js +++ b/__tests__/navigation.test.js @@ -6,7 +6,7 @@ describe('Homepage', () => { let $navigation let $navigationToggler - async function setup (page) { + async function setup(page) { $navigation = await page.$('.js-app-navigation') $navigationToggler = await page.$('.js-app-navigation__toggler') } @@ -38,32 +38,42 @@ describe('Homepage', () => { describe('when JavaScript is available', () => { describe('when menu button is pressed', () => { it('should apply the corresponding open state class to the menu button', async () => { - await expect(getAttribute($navigationToggler, 'class')).resolves - .not.toContain('govuk-header__menu-button--open') + await expect( + getAttribute($navigationToggler, 'class') + ).resolves.not.toContain('govuk-header__menu-button--open') await $navigationToggler.click() // Menu button open state - await expect(getAttribute($navigationToggler, 'class')).resolves - .toContain('govuk-header__menu-button--open') + await expect( + getAttribute($navigationToggler, 'class') + ).resolves.toContain('govuk-header__menu-button--open') }) it('should indicate the expanded state of the toggle button using aria-expanded', async () => { - await expect(getAttribute($navigationToggler, 'aria-expanded')).resolves.toBe('false') + await expect( + getAttribute($navigationToggler, 'aria-expanded') + ).resolves.toBe('false') await $navigationToggler.click() // Menu button control expanded - await expect(getAttribute($navigationToggler, 'aria-expanded')).resolves.toBe('true') + await expect( + getAttribute($navigationToggler, 'aria-expanded') + ).resolves.toBe('true') }) it('should indicate the open state of the navigation', async () => { - await expect(getAttribute($navigation, 'class')).resolves.not.toContain('app-navigation--active') + await expect(getAttribute($navigation, 'class')).resolves.not.toContain( + 'app-navigation--active' + ) await $navigationToggler.click() // Menu open state - await expect(getAttribute($navigation, 'class')).resolves.toContain('app-navigation--active') + await expect(getAttribute($navigation, 'class')).resolves.toContain( + 'app-navigation--active' + ) }) it('should indicate the visible state of the navigation using the hidden attribute', async () => { diff --git a/__tests__/search.test.js b/__tests__/search.test.js index 824ef8b815..c1a3e77760 100644 --- a/__tests__/search.test.js +++ b/__tests__/search.test.js @@ -9,7 +9,7 @@ describe('Site search', () => { let $wrapper let $searchInput - async function setup (page) { + async function setup(page) { $module = await page.$('[data-module="app-search"]') $wrapper = await $module.$('.app-site-search__wrapper') @@ -29,22 +29,29 @@ describe('Site search', () => { afterEach(async () => { // Reset 'onbeforeunload' to continue navigation - await page.evaluate(() => { window.onbeforeunload = null }) + await page.evaluate(() => { + window.onbeforeunload = null + }) }) it('does not return any results when searching for something that does not exist', async () => { await $searchInput.type('lorem ipsum') const $searchOptions = await $module.$$('.app-site-search__option') - await expect(getProperty($searchOptions[0], 'textContent')).resolves.toBe('No results found') + await expect(getProperty($searchOptions[0], 'textContent')).resolves.toBe( + 'No results found' + ) }) it('returns results where a word in the title begins with the letter "d"', async () => { await $searchInput.type('d') // ignore any results where a match was found in the alias - const resultsArray = await page.$$eval('.app-site-search__option:not(:has(.app-site-search__aliases))', results => - results.map(result => result.firstChild.textContent.toLowerCase())) + const resultsArray = await page.$$eval( + '.app-site-search__option:not(:has(.app-site-search__aliases))', + (results) => + results.map((result) => result.firstChild.textContent.toLowerCase()) + ) // regex with word boundary, in our case words that begin with 'd' for (const result of resultsArray) { @@ -58,8 +65,14 @@ describe('Site search', () => { await $searchInput.type('d') // only get results where a match was found in the alias - const resultsArray = await page.$$eval('.app-site-search__option:has(.app-site-search__aliases)', results => - results.map(result => result.querySelector('.app-site-search__aliases').textContent)) + const resultsArray = await page.$$eval( + '.app-site-search__option:has(.app-site-search__aliases)', + (results) => + results.map( + (result) => + result.querySelector('.app-site-search__aliases').textContent + ) + ) // regex with word boundary, in our case words that begin with 'd' for (const result of resultsArray) { @@ -80,10 +93,7 @@ describe('Site search', () => { await $searchInput.click() await $searchInput.type('details') - await Promise.all([ - page.waitForNavigation(), - page.keyboard.press('Enter') - ]) + await Promise.all([page.waitForNavigation(), page.keyboard.press('Enter')]) const url = new URL(await page.url()) expect(url.pathname).toBe('/components/details/') @@ -93,10 +103,8 @@ describe('Site search', () => { await page.setRequestInterception(true) // Abort requests to search index - page.on('request', request => - isSearchIndex.test(request.url()) - ? request.abort() - : request.continue() + page.on('request', (request) => + isSearchIndex.test(request.url()) ? request.abort() : request.continue() ) // Reload page again @@ -107,14 +115,19 @@ describe('Site search', () => { await $searchInput.type('lorem') const $searchOptions = await $module.$$('.app-site-search__option') - await expect(getProperty($searchOptions[0], 'textContent')).resolves.toBe('Failed to load the search index') + await expect(getProperty($searchOptions[0], 'textContent')).resolves.toBe( + 'Failed to load the search index' + ) }) it('shows user a message that the search index is loading', async () => { await page.setRequestInterception(true) // Intentionally make the search-index request hang - page.on('request', request => isSearchIndex.test(request.url()) || request.continue()) + page.on( + 'request', + (request) => isSearchIndex.test(request.url()) || request.continue() + ) // Reload page again await page.reload() @@ -124,32 +137,41 @@ describe('Site search', () => { await $searchInput.type('d') const $searchOptions = await $module.$$('.app-site-search__option') - await expect(getProperty($searchOptions[0], 'textContent')).resolves.toBe('Loading search index') + await expect(getProperty($searchOptions[0], 'textContent')).resolves.toBe( + 'Loading search index' + ) }) it('should focus the input when clicking the button', async () => { // The button is actually a background on the left most part of the wrapper element. const { top, left } = await $wrapper.evaluate(($element) => - $element.getBoundingClientRect().toJSON()) + $element.getBoundingClientRect().toJSON() + ) // Click the top left side of the element. await page.mouse.click(left, top) // Get the active focused element to compare with the actual expected input. const $activeElement = await page.evaluate(() => document.activeElement) - const $searchInput = await page.evaluate(() => document.querySelector('.app-site-search__input')) + const $searchInput = await page.evaluate(() => + document.querySelector('.app-site-search__input') + ) expect($activeElement).toEqual($searchInput) }) describe('tracking', () => { it('should track if there are no results', async () => { - await page.evaluate(() => { window.__SITE_SEARCH_TRACKING_TIMEOUT = 0 }) + await page.evaluate(() => { + window.__SITE_SEARCH_TRACKING_TIMEOUT = 0 + }) await $searchInput.focus() await $searchInput.type('lorem ipsum') - const GoogleTagManagerDataLayer = await page.evaluate(() => window.dataLayer) + const GoogleTagManagerDataLayer = await page.evaluate( + () => window.dataLayer + ) expect(GoogleTagManagerDataLayer).toEqual( expect.arrayContaining([ @@ -169,19 +191,22 @@ describe('Site search', () => { }) it('should track if there are results', async () => { - await page.evaluate(() => { window.__SITE_SEARCH_TRACKING_TIMEOUT = 0 }) + await page.evaluate(() => { + window.__SITE_SEARCH_TRACKING_TIMEOUT = 0 + }) await $searchInput.focus() await $searchInput.type('g') const $searchOptions = await $module.$$('.app-site-search__option') - const GoogleTagManagerDataLayer = await page.evaluate(() => window.dataLayer) + const GoogleTagManagerDataLayer = await page.evaluate( + () => window.dataLayer + ) // Find layer that has the impressions to test. - const impressions = - GoogleTagManagerDataLayer - .filter(layer => layer.ecommerce) - .map(layer => layer.ecommerce.impressions)[0] + const impressions = GoogleTagManagerDataLayer.filter( + (layer) => layer.ecommerce + ).map((layer) => layer.ecommerce.impressions)[0] expect(impressions.length).toEqual($searchOptions.length) expect(GoogleTagManagerDataLayer).toEqual( @@ -212,8 +237,12 @@ describe('Site search', () => { // Prevent page from unloading so we can check what was tracked. // By setting onbeforeunload it forces a dialog to appear that allows a user // to cancel leaving the page, so we detect the dialog opening and dismiss it to stop the navigation. - await page.evaluate(() => { window.onbeforeunload = () => true }) - page.on('dialog', async dialog => { await dialog.dismiss() }) + await page.evaluate(() => { + window.onbeforeunload = () => true + }) + page.on('dialog', async (dialog) => { + await dialog.dismiss() + }) await $searchInput.focus() await $searchInput.type('g') @@ -221,7 +250,9 @@ describe('Site search', () => { await page.keyboard.press('ArrowDown') await page.keyboard.press('Enter') - const GoogleTagManagerDataLayer = await page.evaluate(() => window.dataLayer) + const GoogleTagManagerDataLayer = await page.evaluate( + () => window.dataLayer + ) expect(GoogleTagManagerDataLayer).toEqual( expect.arrayContaining([ @@ -253,12 +284,16 @@ describe('Site search', () => { }) it('should block personally identifable information emails', async () => { - await page.evaluate(() => { window.__SITE_SEARCH_TRACKING_TIMEOUT = 0 }) + await page.evaluate(() => { + window.__SITE_SEARCH_TRACKING_TIMEOUT = 0 + }) await $searchInput.focus() await $searchInput.type('user@example.com') - const GoogleTagManagerDataLayer = await page.evaluate(() => window.dataLayer) + const GoogleTagManagerDataLayer = await page.evaluate( + () => window.dataLayer + ) expect(GoogleTagManagerDataLayer).toEqual( expect.arrayContaining([ @@ -272,12 +307,16 @@ describe('Site search', () => { }) it('should block personally identifable information numbers', async () => { - await page.evaluate(() => { window.__SITE_SEARCH_TRACKING_TIMEOUT = 0 }) + await page.evaluate(() => { + window.__SITE_SEARCH_TRACKING_TIMEOUT = 0 + }) await $searchInput.focus() await $searchInput.type('079460999') - const GoogleTagManagerDataLayer = await page.evaluate(() => window.dataLayer) + const GoogleTagManagerDataLayer = await page.evaluate( + () => window.dataLayer + ) expect(GoogleTagManagerDataLayer).toEqual( expect.arrayContaining([ diff --git a/__tests__/tabs.test.js b/__tests__/tabs.test.js index 40a0ba9a8c..5c5180eb44 100644 --- a/__tests__/tabs.test.js +++ b/__tests__/tabs.test.js @@ -7,7 +7,7 @@ describe('Component page', () => { let $tabsLinks let $tabsContainers - async function setup (page) { + async function setup(page) { $module = await page.$('[data-module="app-tabs"]') $tabsItems = await $module.$$('.app-tabs__item') @@ -42,16 +42,18 @@ describe('Component page', () => { await $tabsLinks[0].click() // Tab item marked current - await expect(getAttribute($tabsItems[0], 'class')).resolves - .toContain('app-tabs__item--current') + await expect(getAttribute($tabsItems[0], 'class')).resolves.toContain( + 'app-tabs__item--current' + ) }) it('should indicate the selected state of the tab using aria-expanded', async () => { await $tabsLinks[0].click() // Tab link control expanded - await expect(getAttribute($tabsLinks[0], 'aria-expanded')).resolves - .toBe('true') + await expect( + getAttribute($tabsLinks[0], 'aria-expanded') + ).resolves.toBe('true') }) }) @@ -60,16 +62,18 @@ describe('Component page', () => { await $tabsLinks[0].click({ count: 2 }) // Tab item not marked current - await expect(getAttribute($tabsItems[0], 'class')).resolves - .not.toContain('app-tabs__item--current') + await expect( + getAttribute($tabsItems[0], 'class') + ).resolves.not.toContain('app-tabs__item--current') }) it('should indicate the closed state by setting aria-expanded attribute to false', async () => { await $tabsLinks[0].click({ count: 2 }) // Tab link control collapsed - await expect(getAttribute($tabsLinks[0], 'aria-expanded')).resolves - .toBe('false') + await expect( + getAttribute($tabsLinks[0], 'aria-expanded') + ).resolves.toBe('false') }) }) }) @@ -83,12 +87,16 @@ describe('Patterns page', () => { describe('when JavaScript is available', () => { describe('when "hideTab" parameter is set to true', () => { it('the tab list is not rendered', async () => { - const $expandedTabContentWithNoTab = await page.$('#section-headings-question-pages-example-open .app-tabs') + const $expandedTabContentWithNoTab = await page.$( + '#section-headings-question-pages-example-open .app-tabs' + ) expect($expandedTabContentWithNoTab).toBeNull() }) it('close button is not shown on the code block', async () => { - const $expandedTabContentWithNoTabCloseButton = await page.$('.js-tabs__container--no-tabs .js-tabs__close') + const $expandedTabContentWithNoTabCloseButton = await page.$( + '.js-tabs__container--no-tabs .js-tabs__close' + ) expect($expandedTabContentWithNoTabCloseButton).toBeNull() }) }) @@ -107,7 +115,9 @@ describe('Styles -> Images page', () => { }) it('the tab heading items are not rendered', async () => { - const $tabHeadingItems = await page.$('#example-default .app-tabs__heading') + const $tabHeadingItems = await page.$( + '#example-default .app-tabs__heading' + ) expect($tabHeadingItems).toBeNull() }) }) diff --git a/babel.config.js b/babel.config.js index 4881f1c0e7..504473e73b 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,14 +4,15 @@ * @type {import('@babel/core').ConfigFunction} */ module.exports = function (api) { - const browserslistEnv = !api.env('test') - ? 'production' - : 'node' + const browserslistEnv = !api.env('test') ? 'production' : 'node' const presets = [ - ['@babel/preset-env', { - browserslistEnv - }] + [ + '@babel/preset-env', + { + browserslistEnv + } + ] ] return { diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js index 9768f21a24..6cba6a874a 100644 --- a/jest-puppeteer.config.js +++ b/jest-puppeteer.config.js @@ -16,9 +16,7 @@ module.exports = { * * {@link https://developer.chrome.com/articles/new-headless/} */ - headless: process.env.HEADLESS !== 'false' - ? 'new' - : false + headless: process.env.HEADLESS !== 'false' ? 'new' : false }, /** diff --git a/lib/colours.js b/lib/colours.js index 523e4ee114..50140a4736 100644 --- a/lib/colours.js +++ b/lib/colours.js @@ -2,7 +2,7 @@ const { dirname, join } = require('path') const exporter = require('sass-export').exporter -function parseSCSS (filename) { +function parseSCSS(filename) { const packagePath = dirname(require.resolve('govuk-frontend')) return exporter({ @@ -11,8 +11,9 @@ function parseSCSS (filename) { }).getStructured() } -function getColourFromSass (sass, variableName) { - return sass.variables.find(variable => variable.name === variableName).compiledValue +function getColourFromSass(sass, variableName) { + return sass.variables.find((variable) => variable.name === variableName) + .compiledValue } /** @@ -32,12 +33,16 @@ const palette = function () { // For variables with 'if()' conditional find the original map value such as // `$govuk-colours` to `$_govuk-colour-palette-modern` in govuk-frontend v4 const paletteMap = !('paletteMap' in palette) - ? variables.find(({ compiledValue, mapValue }) => compiledValue === palette.compiledValue && mapValue).mapValue + ? variables.find( + ({ compiledValue, mapValue }) => + compiledValue === palette.compiledValue && mapValue + ).mapValue : palette.mapValue // Convert Sass map entries into a single object - return Object.fromEntries(paletteMap - .map(({ name, compiledValue }) => [name, compiledValue])) + return Object.fromEntries( + paletteMap.map(({ name, compiledValue }) => [name, compiledValue]) + ) } /** @@ -66,7 +71,7 @@ const applied = function () { const sass = parseSCSS('_colours-applied.scss') for (const group in data) { - data[group] = data[group].map(colour => { + data[group] = data[group].map((colour) => { colour.colour = getColourFromSass(sass, colour.name) return colour diff --git a/lib/extract-page-headings/index.js b/lib/extract-page-headings/index.js index a0cbef6a7d..7870521703 100644 --- a/lib/extract-page-headings/index.js +++ b/lib/extract-page-headings/index.js @@ -11,18 +11,21 @@ const plugin = () => { const lexer = new marked.Lexer() const tokens = lexer.lex(contents) const headingsArray = [] - tokens.forEach(token => { + tokens.forEach((token) => { if (token.type !== 'heading') { return } let aliases = null if (data.headingAliases) { - aliases = - Object.entries(data.headingAliases) - .filter(([text]) => { return text === token.text }) - .map(([text, alias]) => { return alias }) - .join() + aliases = Object.entries(data.headingAliases) + .filter(([text]) => { + return text === token.text + }) + .map(([text, alias]) => { + return alias + }) + .join() } const heading = { diff --git a/lib/extract-page-headings/index.test.js b/lib/extract-page-headings/index.test.js index 546b59a1d0..7f6c649019 100644 --- a/lib/extract-page-headings/index.test.js +++ b/lib/extract-page-headings/index.test.js @@ -19,18 +19,48 @@ describe('extract-page-headings plugin', () => { it('generated heading metadata matches expected', () => { const metadataHeadings = pages['example.md'].headings const expectedHeadings = [ - { aliases: null, depth: 1, text: 'Heading level 1', url: 'heading-level-1' }, - { aliases: null, depth: 2, text: 'Heading level 2', url: 'heading-level-2' }, - { aliases: null, depth: 3, text: 'Heading level 3', url: 'heading-level-3' } + { + aliases: null, + depth: 1, + text: 'Heading level 1', + url: 'heading-level-1' + }, + { + aliases: null, + depth: 2, + text: 'Heading level 2', + url: 'heading-level-2' + }, + { + aliases: null, + depth: 3, + text: 'Heading level 3', + url: 'heading-level-3' + } ] expect(metadataHeadings).toEqual(expectedHeadings) }) it('generates headings with aliases', () => { const metadataHeadings = pages['example-with-aliases.md'].headings const expectedHeadings = [ - { aliases: 'one', depth: 1, text: 'Heading level 1', url: 'heading-level-1' }, - { aliases: 'two', depth: 2, text: 'Heading level 2', url: 'heading-level-2' }, - { aliases: 'three', depth: 3, text: 'Heading level 3', url: 'heading-level-3' } + { + aliases: 'one', + depth: 1, + text: 'Heading level 1', + url: 'heading-level-1' + }, + { + aliases: 'two', + depth: 2, + text: 'Heading level 2', + url: 'heading-level-2' + }, + { + aliases: 'three', + depth: 3, + text: 'Heading level 3', + url: 'heading-level-3' + } ] expect(metadataHeadings).toEqual(expectedHeadings) }) diff --git a/lib/file-helper.js b/lib/file-helper.js index 1667da5520..210faa9ff2 100644 --- a/lib/file-helper.js +++ b/lib/file-helper.js @@ -12,7 +12,7 @@ nunjucks.configure(join(paths.views, 'layouts')) // This helper function takes a path of a file and // returns the contents as string -exports.getFileContents = path => { +exports.getFileContents = (path) => { let fileContents try { fileContents = fs.readFileSync(path) @@ -28,7 +28,7 @@ exports.getFileContents = path => { // This helper function takes a path of a *.md.njk file and // returns the Nunjucks syntax inside that file without markdown data and imports -exports.getNunjucksCode = path => { +exports.getNunjucksCode = (path) => { const fileContents = this.getFileContents(path) const parsedFile = matter(fileContents) @@ -36,15 +36,12 @@ exports.getNunjucksCode = path => { // Omit any `{% extends "foo.njk" %}` nunjucks code, because we extend // templates that only exist within the Design System – it's not useful to // include this in the code we expect others to copy. - return parsedFile.content.replace( - /{%\s*extends\s*\S*\s*%}\s+/, - '' - ) + return parsedFile.content.replace(/{%\s*extends\s*\S*\s*%}\s+/, '') } // This helper function takes a path of a *.md.njk file and // returns the frontmatter as an object -exports.getFrontmatter = path => { +exports.getFrontmatter = (path) => { const fileContents = this.getFileContents(path) const parsedFile = matter(fileContents) @@ -87,7 +84,7 @@ exports.getFingerprint = function (file) { // This helper function takes a path of a *.md.njk file and // returns the HTML rendered by Nunjucks without markdown data -exports.getHTMLCode = path => { +exports.getHTMLCode = (path) => { const fileContents = this.getFileContents(path) const parsedFile = matter(fileContents) diff --git a/lib/fingerprints/index.js b/lib/fingerprints/index.js index 34a4d0c6b4..d70d73c0cb 100644 --- a/lib/fingerprints/index.js +++ b/lib/fingerprints/index.js @@ -38,7 +38,8 @@ const hashAssets = (config) => (files, metalsmith, done) => { // Replace source map URL with hashed filename files[pathAsset].contents = Buffer.from( - files[pathAsset].contents.toString() + files[pathAsset].contents + .toString() .replace(basename(pathMap), basename(fingerprints[pathMap].path)) ) @@ -94,7 +95,7 @@ const hashAsset = (files, metalsmith) => { * @param {import('crypto').BinaryLike} contents - File contents * @returns {{ path: string; hash: string }} Fingerprint object */ -function hashPath (pathAsset, contents) { +function hashPath(pathAsset, contents) { let { dir, name, ext } = parse(pathAsset) // Gather multiple extensions @@ -108,9 +109,7 @@ function hashPath (pathAsset, contents) { } // Generate hash from file name + contents - const hash = createHmac('md5', name) - .update(contents) - .digest('hex') + const hash = createHmac('md5', name).update(contents).digest('hex') // File path hashed const path = join(dir, `${name}-${hash}${ext}`) @@ -125,9 +124,10 @@ function hashPath (pathAsset, contents) { * @param {import('crypto').BinaryLike} contents - File contents * @returns {string | undefined} Source map URL */ -function getSourceMapPath (pathAsset, contents) { - const sourceMappingURL = /# sourceMappingURL=(?.*\.map)/g - .exec(contents.toString())?.groups?.path +function getSourceMapPath(pathAsset, contents) { + const sourceMappingURL = /# sourceMappingURL=(?.*\.map)/g.exec( + contents.toString() + )?.groups?.path if (sourceMappingURL) { return join(dirname(pathAsset), sourceMappingURL) diff --git a/lib/fingerprints/index.test.js b/lib/fingerprints/index.test.js index b392675b3b..95f3b77934 100644 --- a/lib/fingerprints/index.test.js +++ b/lib/fingerprints/index.test.js @@ -11,17 +11,16 @@ describe('Hash fingerprints plugin', () => { beforeAll((done) => { Metalsmith(__dirname) - // Use test fixtures .source(source) .destination(destination) // Hash fingerprints example - .use(hashAssets({ - pattern: [ - '**/*.{css,map,jpg,js,json}' - ] - })) + .use( + hashAssets({ + pattern: ['**/*.{css,map,jpg,js,json}'] + }) + ) // Check metadata .use((files, metalsmith, done) => { @@ -58,19 +57,21 @@ describe('Hash fingerprints plugin', () => { 'example.json': fingerprint }) - expect(Object.entries(metadata.fingerprints)) - .toHaveLength(9) + expect(Object.entries(metadata.fingerprints)).toHaveLength(9) }) it('syncs fingerprints for source map metadata', () => { - expect(metadata.fingerprints['example-source-map.css'].hash) - .toEqual(metadata.fingerprints['example-source-map.css.map'].hash) + expect(metadata.fingerprints['example-source-map.css'].hash).toEqual( + metadata.fingerprints['example-source-map.css.map'].hash + ) - expect(metadata.fingerprints['example-source-map.js'].hash) - .toEqual(metadata.fingerprints['example-source-map.js.map'].hash) + expect(metadata.fingerprints['example-source-map.js'].hash).toEqual( + metadata.fingerprints['example-source-map.js.map'].hash + ) - expect(metadata.fingerprints['example-xxx-123.js'].hash) - .toEqual(metadata.fingerprints['example-xxx-456.map'].hash) + expect(metadata.fingerprints['example-xxx-123.js'].hash).toEqual( + metadata.fingerprints['example-xxx-456.map'].hash + ) }) it('renames assets with hash fingerprints', () => { @@ -102,14 +103,23 @@ describe('Hash fingerprints plugin', () => { }) it('updates asset source map URLs with hash fingerprints', () => { - expect(output[metadata.fingerprints['example-source-map.css'].path].contents.toString()) - .toContain(metadata.fingerprints['example-source-map.css.map'].path) - - expect(output[metadata.fingerprints['example-source-map.js'].path].contents.toString()) - .toContain(metadata.fingerprints['example-source-map.js.map'].path) - - expect(output[metadata.fingerprints['example-xxx-123.js'].path].contents.toString()) - .toContain(metadata.fingerprints['example-xxx-456.map'].path) + expect( + output[ + metadata.fingerprints['example-source-map.css'].path + ].contents.toString() + ).toContain(metadata.fingerprints['example-source-map.css.map'].path) + + expect( + output[ + metadata.fingerprints['example-source-map.js'].path + ].contents.toString() + ).toContain(metadata.fingerprints['example-source-map.js.map'].path) + + expect( + output[ + metadata.fingerprints['example-xxx-123.js'].path + ].contents.toString() + ).toContain(metadata.fingerprints['example-xxx-456.map'].path) }) it('deletes old assets', () => { diff --git a/lib/generate-sitemap.js b/lib/generate-sitemap.js index 7cc5864a4d..e85dfd14a1 100644 --- a/lib/generate-sitemap.js +++ b/lib/generate-sitemap.js @@ -64,7 +64,10 @@ const plugin = (opts) => { } // Exclude examples from the sitemap - if (frontmatter.layout && frontmatter.layout.startsWith('layout-example')) { + if ( + frontmatter.layout && + frontmatter.layout.startsWith('layout-example') + ) { return false } @@ -75,29 +78,33 @@ const plugin = (opts) => { return true } - Object.keys(files).sort().forEach(function (file) { - // Get the current file's frontmatter - const frontmatter = files[file] - - // Only process files that pass the check - if (!checkPattern(file, frontmatter)) { - return - } - - // Use canonical in frontmatter instead of the file's canonical data - // set by metalsmith-canonical plugin - const url = frontmatter.canonical ? frontmatter.canonical : files[file].canonical - - const sitemapEntry = { - url, - changefreq: frontmatter.changefreq || changefreq, - lastmod: frontmatter.lastmod || lastmod, - priority: frontmatter.priority || priority - } - - // Add the files to the sitemap and remove empty keys - sitemap.write(removeEmptyKeys(sitemapEntry)) - }) + Object.keys(files) + .sort() + .forEach(function (file) { + // Get the current file's frontmatter + const frontmatter = files[file] + + // Only process files that pass the check + if (!checkPattern(file, frontmatter)) { + return + } + + // Use canonical in frontmatter instead of the file's canonical data + // set by metalsmith-canonical plugin + const url = frontmatter.canonical + ? frontmatter.canonical + : files[file].canonical + + const sitemapEntry = { + url, + changefreq: frontmatter.changefreq || changefreq, + lastmod: frontmatter.lastmod || lastmod, + priority: frontmatter.priority || priority + } + + // Add the files to the sitemap and remove empty keys + sitemap.write(removeEmptyKeys(sitemapEntry)) + }) sitemap.end() diff --git a/lib/get-macro-options/__mocks__/fs.js b/lib/get-macro-options/__mocks__/fs.js index 6e36c26284..2667b5d96f 100644 --- a/lib/get-macro-options/__mocks__/fs.js +++ b/lib/get-macro-options/__mocks__/fs.js @@ -4,7 +4,7 @@ const fs = jest.genMockFromModule('fs') let mockedData -fs.__setMockData = data => { +fs.__setMockData = (data) => { mockedData = data } fs.readFileSync = (filename) => { diff --git a/lib/get-macro-options/__tests__/index.js b/lib/get-macro-options/__tests__/index.js index 9226dd01a3..384b6af5b3 100644 --- a/lib/get-macro-options/__tests__/index.js +++ b/lib/get-macro-options/__tests__/index.js @@ -19,14 +19,16 @@ const fixtures = { type: 'object' }, { - description: 'Options for a component that does not exist in the Design System (hint component).', + description: + 'Options for a component that does not exist in the Design System (hint component).', isComponent: true, name: 'hint', required: false, type: 'object' }, { - description: 'Options for a component that does not exist in the Design System (label component).', + description: + 'Options for a component that does not exist in the Design System (label component).', isComponent: true, name: 'label', required: false, @@ -43,7 +45,8 @@ const fixtures = { name: 'formGroup', params: [ { - description: 'Optional classes to add to the form group (e.g. to show error state forthe whole group)', + description: + 'Optional classes to add to the form group (e.g. to show error state forthe whole group)', name: 'classes', required: false, type: 'string' @@ -65,14 +68,16 @@ const fixtures = { required: false }, { - description: 'Options for a nested component that does not exist in the Design System (label component).', + description: + 'Options for a nested component that does not exist in the Design System (label component).', isComponent: true, name: 'label', required: false, type: 'object' }, { - description: 'Options for a nested component that exists in the Design System.', + description: + 'Options for a nested component that exists in the Design System.', isComponent: true, name: 'nestedComponentWithCamelCaseName', required: false, @@ -110,7 +115,9 @@ describe('getMacroOptions', () => { expect(output[0].options[0].slug).toBe('component-with-camel-case-name') }) it('appends slugs to nested options', () => { - expect(output[2].options[0].params[2].slug).toBe('nested-component-with-camel-case-name') + expect(output[2].options[0].params[2].slug).toBe( + 'nested-component-with-camel-case-name' + ) }) }) describe('nested options', () => { @@ -127,15 +134,14 @@ describe('getMacroOptions', () => { expect(output[5].id).toBe('label') }) it('should only output additional components once', () => { - const optionsWithLabelInName = - Object.entries(output) - .map(entry => { - const name = entry[1].name - return name - }) - .filter(name => { - return name.endsWith('label') - }) + const optionsWithLabelInName = Object.entries(output) + .map((entry) => { + const name = entry[1].name + return name + }) + .filter((name) => { + return name.endsWith('label') + }) expect(optionsWithLabelInName.length).toBe(1) }) @@ -147,10 +153,14 @@ describe('getMacroOptions', () => { }) describe('markdown rendering', () => { it('renders descriptions as markdown', () => { - expect(output[2].options[0].description).toBe('items for nested link') + expect(output[2].options[0].description).toBe( + 'items for nested link' + ) }) it('renders nested options descriptions as markdown', () => { - expect(output[2].options[0].params[0].description).toBe('link') + expect(output[2].options[0].params[0].description).toBe( + 'link' + ) }) }) }) diff --git a/lib/get-macro-options/index.js b/lib/get-macro-options/index.js index 4e3a1ed657..a66fa7f12a 100644 --- a/lib/get-macro-options/index.js +++ b/lib/get-macro-options/index.js @@ -6,23 +6,26 @@ const { marked } = require('marked') // Get reference to marked.js const renderer = new marked.Renderer() // Override marking up paragraphs -renderer.paragraph = text => text +renderer.paragraph = (text) => text -function getMacroOptionsJson (componentName) { - const optionsFilePath = join(dirname(require.resolve('govuk-frontend')), `components/${componentName}/macro-options.json`) +function getMacroOptionsJson(componentName) { + const optionsFilePath = join( + dirname(require.resolve('govuk-frontend')), + `components/${componentName}/macro-options.json` + ) return JSON.parse(fs.readFileSync(optionsFilePath, 'utf8')) } -function addSlugs (option) { +function addSlugs(option) { // camelCase into kebab-case - option.slug = (option.name).replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + option.slug = option.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() if (option.params) { option.params = option.params.map(addSlugs) } return option } -function renderDescriptionsAsMarkdown (option) { +function renderDescriptionsAsMarkdown(option) { if (option.description) { option.description = marked(option.description, { renderer }) } @@ -33,27 +36,29 @@ function renderDescriptionsAsMarkdown (option) { } // To display nested options that such as rows in a table, we need to make a separate group to be displayed. -function getNestedOptions (options) { - return options - .filter(option => option.params) - .map(option => { - let output = [option] - if (option.params) { - output = output.concat(getNestedOptions(option.params)) - } - return output - }) - // Flatten array - .reduce((a, b) => { - return a.concat(b) - }, []) +function getNestedOptions(options) { + return ( + options + .filter((option) => option.params) + .map((option) => { + let output = [option] + if (option.params) { + output = output.concat(getNestedOptions(option.params)) + } + return output + }) + // Flatten array + .reduce((a, b) => { + return a.concat(b) + }, []) + ) } // Some which are only used in other components are intentionally not displayed in the GOV.UK Design System guidance. // We want to add these as a separate group of options that can be linked to from the original options for the component. -function getAdditionalComponentOptions (options) { +function getAdditionalComponentOptions(options) { const names = options - .map(option => { + .map((option) => { let output = [] if (option.isComponent) { if (option.name === 'hint' || option.name === 'label') { @@ -136,16 +141,15 @@ function getAdditionalComponentOptions (options) { * ``` * * */ -function getMacroOptions (componentName) { +function getMacroOptions(componentName) { // The design system uses a different name for the input component. if (componentName === 'text-input') { componentName = 'input' } - const options = - getMacroOptionsJson(componentName) - .map(addSlugs) - .map(renderDescriptionsAsMarkdown) + const options = getMacroOptionsJson(componentName) + .map(addSlugs) + .map(renderDescriptionsAsMarkdown) const nestedOptions = getNestedOptions(options) const additionalComponents = getAdditionalComponentOptions(options) @@ -156,24 +160,28 @@ function getMacroOptions (componentName) { id: 'primary', options } - ].concat( - nestedOptions.map(option => { - return { - name: 'Options for ' + option.name, - id: option.name, - options: option.params - } - }) - ).concat( - additionalComponents.map(name => { - const additionalComponentOptions = getMacroOptionsJson(name).map(renderDescriptionsAsMarkdown) - return { - name: 'Options for ' + name, - id: name, - options: additionalComponentOptions - } - }) - ) + ] + .concat( + nestedOptions.map((option) => { + return { + name: 'Options for ' + option.name, + id: option.name, + options: option.params + } + }) + ) + .concat( + additionalComponents.map((name) => { + const additionalComponentOptions = getMacroOptionsJson(name).map( + renderDescriptionsAsMarkdown + ) + return { + name: 'Options for ' + name, + id: name, + options: additionalComponentOptions + } + }) + ) return optionGroups } diff --git a/lib/highlighter.js b/lib/highlighter.js index 1e719eb049..a9f14cdeab 100644 --- a/lib/highlighter.js +++ b/lib/highlighter.js @@ -7,8 +7,9 @@ const hljs = require('highlight.js') * @param {string} [language] - Code programming language * @returns {string} Code with syntax highlighting */ -function highlight (code, language) { - return hljs.highlight(code.trim(), { language: language || 'plaintext' }).value +function highlight(code, language) { + return hljs.highlight(code.trim(), { language: language || 'plaintext' }) + .value } module.exports = highlight diff --git a/lib/marked-renderer.js b/lib/marked-renderer.js index 19cdc33a18..8423388a14 100644 --- a/lib/marked-renderer.js +++ b/lib/marked-renderer.js @@ -8,20 +8,21 @@ class DesignSystemRenderer extends marked.Renderer { * Assume HTML when no code block language provided * (for example, HTML code inside Markdown) */ - code (code, language, isEscaped) { + code(code, language, isEscaped) { return !language ? super.html(code) - : super.code(code, language, isEscaped) + : super + .code(code, language, isEscaped) - // Ensure code blocks can be focused and scrolled - // with the keyboard via `tabindex="0"` - .replace('` image tags in `

` paragraphs */ - paragraph (text) { + paragraph(text) { return text.trim().startsWith(' { const outputPath = 'search-index.json' - const separator = '' + const separator = + '' const includedSections = navigationConfig - .filter(section => section.includeInSearch) - .map(section => section.label) + .filter((section) => section.includeInSearch) + .map((section) => section.label) - const documents = - Object.keys(files) - // Filter out any non html files - .filter(path => path.endsWith('.html')) - // Filter out anything not in an included section - .filter(path => { - const section = files[path].section - return includedSections.includes(section) - }) - - const documentResults = documents - .map(path => { - const file = files[path] - const permalink = file.permalink || path - return { - permalink, - title: file.title, - section: file.section, - aliases: file.aliases ?? undefined - } + const documents = Object.keys(files) + // Filter out any non html files + .filter((path) => path.endsWith('.html')) + // Filter out anything not in an included section + .filter((path) => { + const section = files[path].section + return includedSections.includes(section) }) + const documentResults = documents.map((path) => { + const file = files[path] + const permalink = file.permalink || path + return { + permalink, + title: file.title, + section: file.section, + aliases: file.aliases ?? undefined + } + }) + const pageHeadingDocuments = documents // Only include pages in the with showPageNav: true - .filter(path => { + .filter((path) => { return files[path].showPageNav }) // Filter out files that don't have extracted headings - .filter(path => files[path].headings) - .flatMap(path => { + .filter((path) => files[path].headings) + .flatMap((path) => { const file = files[path] const permalink = file.permalink || path - return file.headings - // only use

s - .filter(heading => heading.depth === 2) - .map(heading => { - return { - permalink: `${permalink}/#${heading.url}`, - title: heading.text, - page: file.title, - section: file.section, - aliases: heading.aliases ?? undefined - } - }) + return ( + file.headings + // only use

s + .filter((heading) => heading.depth === 2) + .map((heading) => { + return { + permalink: `${permalink}/#${heading.url}`, + title: heading.text, + page: file.title, + section: file.section, + aliases: heading.aliases ?? undefined + } + }) + ) }) // The search index only contains what's needed to match and identify a @@ -81,7 +82,7 @@ module.exports = function lunrPlugin () { this.pipeline.remove(lunr.stemmer) // Disable stemming of search terms run against this index this.searchPipeline.remove(lunr.stemmer) - documentResults.forEach(doc => { + documentResults.forEach((doc) => { store[doc.permalink] = { permalink: doc.permalink, title: doc.title, @@ -91,7 +92,7 @@ module.exports = function lunrPlugin () { this.add(doc) }) // store all headings into the Lunr store - pageHeadingDocuments.forEach(doc => { + pageHeadingDocuments.forEach((doc) => { store[doc.permalink] = { permalink: doc.permalink, title: doc.title, diff --git a/lib/metalsmith-lunr-index/index.test.js b/lib/metalsmith-lunr-index/index.test.js index b25c5dbcd0..2b095253e0 100644 --- a/lib/metalsmith-lunr-index/index.test.js +++ b/lib/metalsmith-lunr-index/index.test.js @@ -49,36 +49,43 @@ describe('metalsmith-lunr-index plugin', () => { }) it('contains the permalink to the document', () => { - const entry = Object.values(documentStore) - .find(({ title }) => title === 'Checkboxes') + const entry = Object.values(documentStore).find( + ({ title }) => title === 'Checkboxes' + ) expect(entry.permalink).toEqual('checkboxes.html') }) it('contains the section that the document is in', () => { - const entry = Object.values(documentStore) - .find(({ title }) => title === 'Checkboxes') + const entry = Object.values(documentStore).find( + ({ title }) => title === 'Checkboxes' + ) expect(entry.section).toEqual('Components') }) it('uses a custom permalink if in document metadata', () => { - const entry = Object.values(documentStore) - .find(({ title }) => title === 'With Permalink') + const entry = Object.values(documentStore).find( + ({ title }) => title === 'With Permalink' + ) expect(entry.permalink).toEqual('/with-permalink/') }) it('contains the page and heading level 2 entry', () => { - const entry = Object.values(documentStore) - .find(({ title }) => title === 'Heading level 2') + const entry = Object.values(documentStore).find( + ({ title }) => title === 'Heading level 2' + ) - expect(entry.permalink).toEqual('with-page-headings.html/#heading-level-2') + expect(entry.permalink).toEqual( + 'with-page-headings.html/#heading-level-2' + ) }) it('does not contain headings from documents in excluded sections', () => { - const paths = Object.keys(documentStore) - .filter(path => path.startsWith('about-larry.html')) + const paths = Object.keys(documentStore).filter((path) => + path.startsWith('about-larry.html') + ) expect(paths).toHaveLength(0) }) @@ -87,7 +94,7 @@ describe('metalsmith-lunr-index plugin', () => { describe('the generated index', () => { it('can be used to search for files in the site', () => { const searchResults = searchIndex.search('radios') - const pathsFromSearchResults = searchResults.map(result => result.ref) + const pathsFromSearchResults = searchResults.map((result) => result.ref) expect(pathsFromSearchResults).toEqual(['radios.html']) }) @@ -103,7 +110,9 @@ describe('metalsmith-lunr-index plugin', () => { const searchResults = searchIndex.search('radios') const resultRef = searchResults[0].ref - expect(documentStore[resultRef].aliases).toEqual('radio buttons, option buttons') + expect(documentStore[resultRef].aliases).toEqual( + 'radio buttons, option buttons' + ) }) it('stores the page heading of the page in the metadata', () => { diff --git a/lib/metalsmith-title-checker.js b/lib/metalsmith-title-checker.js index 07d129d960..ed7cc1662b 100644 --- a/lib/metalsmith-title-checker.js +++ b/lib/metalsmith-title-checker.js @@ -5,42 +5,40 @@ * @return {Function} */ -function groupFilesByTitle (filenames, files) { +function groupFilesByTitle(filenames, files) { const groupedFiles = {} - filenames - .forEach((filename, index) => { - const { title } = files[filename] - // If an title exists and is not the current index it means it is a duplicate. - if (!groupedFiles[title]) { - groupedFiles[title] = [] - } - groupedFiles[title].push({ - // Add the filename to the file object for convenience - __filename: filename, - ...files[filename] - }) + filenames.forEach((filename, index) => { + const { title } = files[filename] + // If an title exists and is not the current index it means it is a duplicate. + if (!groupedFiles[title]) { + groupedFiles[title] = [] + } + groupedFiles[title].push({ + // Add the filename to the file object for convenience + __filename: filename, + ...files[filename] }) + }) return groupedFiles } -function getDuplicateTitles (groupedFiles) { +function getDuplicateTitles(groupedFiles) { const duplicateGroupedTitles = {} for (const title in groupedFiles) { const group = groupedFiles[title] - const groupedWithoutExcludedFiles = - group + const groupedWithoutExcludedFiles = group // Default examples always have a duplicated title, // this is OK since the iframes are appended with 'example'. - .filter(file => !file.__filename.includes('/default/')) + .filter((file) => !file.__filename.includes('/default/')) // Filter out Windows default paths too: - .filter(file => !file.__filename.includes('\\default\\')) + .filter((file) => !file.__filename.includes('\\default\\')) // Code examples always have a duplicated title, // as it's only used for the code example not the iframe. - .filter(file => !file.__filename.endsWith('code.njk')) + .filter((file) => !file.__filename.endsWith('code.njk')) if (groupedWithoutExcludedFiles.length > 1) { duplicateGroupedTitles[title] = groupedWithoutExcludedFiles @@ -50,13 +48,11 @@ function getDuplicateTitles (groupedFiles) { return duplicateGroupedTitles } -module.exports = function titleChecker () { +module.exports = function titleChecker() { return (files, metalsmith, done) => { - const nunjucksFileNames = - Object - .keys(files) - // Filter files that are Nunjucks, which have frontmatter. - .filter(filename => filename.endsWith('.njk')) + const nunjucksFileNames = Object.keys(files) + // Filter files that are Nunjucks, which have frontmatter. + .filter((filename) => filename.endsWith('.njk')) const groupedFilesByTitle = groupFilesByTitle(nunjucksFileNames, files) const filesWithDuplicateTitles = getDuplicateTitles(groupedFilesByTitle) @@ -66,19 +62,23 @@ module.exports = function titleChecker () { for (const title in filesWithDuplicateTitles) { const group = filesWithDuplicateTitles[title] duplicateErrorMessage += `The title "${title}" is duplicated ${group.length} times in the following file(s):\n` - duplicateErrorMessage += group.map(file => `- ${file.__filename}`).join('\n') + duplicateErrorMessage += group + .map((file) => `- ${file.__filename}`) + .join('\n') duplicateErrorMessage += '\n\n' } throw Error(duplicateErrorMessage) } - const filesWithoutTitles = nunjucksFileNames.filter(filename => !files[filename].title) + const filesWithoutTitles = nunjucksFileNames.filter( + (filename) => !files[filename].title + ) if (filesWithoutTitles.length) { throw Error( 'The following file(s) do not have titles:\n\n' + - filesWithoutTitles.map(file => `- ${file}`).join('\n') + - '\n' + filesWithoutTitles.map((file) => `- ${file}`).join('\n') + + '\n' ) } done() diff --git a/lib/metalsmith.js b/lib/metalsmith.js index 61baea02b3..912e71ea5b 100644 --- a/lib/metalsmith.js +++ b/lib/metalsmith.js @@ -62,7 +62,9 @@ const nunjucksOptions = { filters: { slugger, kebabCase: (string) => { - return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase() + return string + .replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2') + .toLowerCase() } } } @@ -70,8 +72,10 @@ const nunjucksOptions = { module.exports = metalsmith // notify build starting - .use((files, metalsmith) => metalsmith.watch() && - metalsmith.debug('build').info('Metalsmith build running') + .use( + (files, metalsmith) => + metalsmith.watch() && + metalsmith.debug('build').info('Metalsmith build running') ) // source directory @@ -113,38 +117,39 @@ module.exports = metalsmith .use(extractPageHeadings()) // ignore files from build - .ignore([ - '.DS_Store', - '.eslintrc.js' - ]) + .ignore(['.DS_Store', '.eslintrc.js']) // convert *.scss files to *.css - .use(sass({ - quietDeps: true, - sourceMapIncludeSources: true, - sourceMap: true, - - // Resolve @imports via - loadPaths: [ - join(paths.root, 'node_modules'), - join(paths.source, 'stylesheets'), - - // Path to `govuk-frontend` export without `govuk/` suffix - join(dirname(require.resolve('govuk-frontend')), '../') - ] - })) - - .use(postcss({ - plugins: { - autoprefixer: {} - }, - map: { - inline: false - } - })) + .use( + sass({ + quietDeps: true, + sourceMapIncludeSources: true, + sourceMap: true, + + // Resolve @imports via + loadPaths: [ + join(paths.root, 'node_modules'), + join(paths.source, 'stylesheets'), + + // Path to `govuk-frontend` export without `govuk/` suffix + join(dirname(require.resolve('govuk-frontend')), '../') + ] + }) + ) + + .use( + postcss({ + plugins: { + autoprefixer: {} + }, + map: { + inline: false + } + }) + ) .use(async (files, metalsmith, done) => { - async function copyAssets (pattern, options) { + async function copyAssets(pattern, options) { const assets = await glob(pattern, options) for (const asset of assets) { @@ -200,42 +205,52 @@ module.exports = metalsmith .use(titleChecker()) // render templating syntax in source files - .use(inPlace({ - pattern: '**/*.{md,njk}', - transform: 'jstransformer-nunjucks', - engineOptions: nunjucksOptions - })) + .use( + inPlace({ + pattern: '**/*.{md,njk}', + transform: 'jstransformer-nunjucks', + engineOptions: nunjucksOptions + }) + ) // render markdown in source files - .use(inPlace({ - transform: 'jstransformer-marked', - engineOptions: { - breaks: true, // Enable line breaks - mangle: false, // Don't mangle emails - smartypants: true, // use "smart" typographic punctuation - highlight: highlighter, - - // Custom markdown renderer - renderer: new DesignSystemRenderer() - } - })) + .use( + inPlace({ + transform: 'jstransformer-marked', + engineOptions: { + breaks: true, // Enable line breaks + mangle: false, // Don't mangle emails + smartypants: true, // use "smart" typographic punctuation + highlight: highlighter, + + // Custom markdown renderer + renderer: new DesignSystemRenderer() + } + }) + ) // apply a permalink pattern to files - .use(permalinks({ - relative: false - })) + .use( + permalinks({ + relative: false + }) + ) // add a canonical url property to pages - .use(canonical({ - hostname: 'https://design-system.service.gov.uk', - omitIndex: true, - omitTrailingSlashes: false - })) + .use( + canonical({ + hostname: 'https://design-system.service.gov.uk', + omitIndex: true, + omitTrailingSlashes: false + }) + ) // apply navigation - .use(navigation({ - items: menuItems - })) + .use( + navigation({ + items: menuItems + }) + ) // generate a search index .use(lunr()) @@ -249,27 +264,31 @@ module.exports = metalsmith } return hashAssets({ - pattern: [ - 'search-index.json' - ] + pattern: ['search-index.json'] })(files, metalsmith, done) }) // apply layouts to source files - .use(layouts({ - default: 'layout.njk', - directory: join(paths.views, 'layouts'), - pattern: '**/*.html', - engineOptions: nunjucksOptions - })) + .use( + layouts({ + default: 'layout.njk', + directory: join(paths.views, 'layouts'), + pattern: '**/*.html', + engineOptions: nunjucksOptions + }) + ) // generate a sitemap.xml in public/ folder - .use(generateSitemap({ - hostname: 'https://design-system.service.gov.uk', - pattern: ['**/*.html', '!**/default/*.html'] - })) + .use( + generateSitemap({ + hostname: 'https://design-system.service.gov.uk', + pattern: ['**/*.html', '!**/default/*.html'] + }) + ) // notify build complete - .use((files, metalsmith) => metalsmith.watch() && - metalsmith.debug('build').info('Metalsmith build complete') + .use( + (files, metalsmith) => + metalsmith.watch() && + metalsmith.debug('build').info('Metalsmith build complete') ) diff --git a/lib/navigation.js b/lib/navigation.js index 6aaea761df..1ef90e82d7 100644 --- a/lib/navigation.js +++ b/lib/navigation.js @@ -37,7 +37,11 @@ module.exports = (config) => (files, metalsmith, done) => { const frontmatter = files[join(itemPath, 'index.html')] // Do not show drafts or ignored pages in navigation - if (!frontmatter || frontmatter.status === 'Draft' || frontmatter.ignoreInSitemap) { + if ( + !frontmatter || + frontmatter.status === 'Draft' || + frontmatter.ignoreInSitemap + ) { continue } @@ -51,13 +55,13 @@ module.exports = (config) => (files, metalsmith, done) => { theme: frontmatter.theme, // Override Markdown extracted headings plugin (optional) - headings: frontmatter.headings && frontmatter.showPageNav - ? frontmatter.headings - : undefined, + headings: + frontmatter.headings && frontmatter.showPageNav + ? frontmatter.headings + : undefined, // Additional search terms (optional) - aliases: frontmatter.aliases?.split(',') - .map(string => string.trim()) + aliases: frontmatter.aliases?.split(',').map((string) => string.trim()) }) } diff --git a/lib/puppeteer-helpers.js b/lib/puppeteer-helpers.js index f7b76a5134..b0de109675 100644 --- a/lib/puppeteer-helpers.js +++ b/lib/puppeteer-helpers.js @@ -7,7 +7,7 @@ const { ports } = require('../config') * @param {URL['pathname']} path - URL path * @returns {Promise} Puppeteer page object */ -async function goTo (page, path) { +async function goTo(page, path) { const { href } = new URL(path, `http://localhost:${ports.preview}`) // Navigate to blank page first to fix same page @@ -28,7 +28,7 @@ async function goTo (page, path) { * @param {string} attributeName - Attribute name to return value for * @returns {Promise} Attribute value */ -function getAttribute ($element, attributeName) { +function getAttribute($element, attributeName) { return $element.evaluate((el, name) => el.getAttribute(name), attributeName) } @@ -39,7 +39,7 @@ function getAttribute ($element, attributeName) { * @param {string} propertyName - Property name to return value for * @returns {Promise} Property value */ -async function getProperty ($element, propertyName) { +async function getProperty($element, propertyName) { const handle = await $element.getProperty(propertyName) return handle.jsonValue() } @@ -50,8 +50,8 @@ async function getProperty ($element, propertyName) { * @param {import('puppeteer').ElementHandle} $element - Puppeteer element handle * @returns {Promise} Element visibility */ -async function isVisible ($element) { - return !!await $element.boundingBox() +async function isVisible($element) { + return !!(await $element.boundingBox()) } module.exports = { diff --git a/lib/rollup/index.js b/lib/rollup/index.js index 09e31672e9..35c4d5aee9 100644 --- a/lib/rollup/index.js +++ b/lib/rollup/index.js @@ -27,8 +27,9 @@ const plugin = (modulePath, moduleName) => async (files, metalsmith, done) => { const bundle = await rollup({ ...config, cache, input }) // Compile Rollup bundle(s) - const results = [config.output].flat().map((output) => - bundle.generate({ ...output, name: moduleName })) + const results = [config.output] + .flat() + .map((output) => bundle.generate({ ...output, name: moduleName })) // Update Rollup cache cache = bundle.cache diff --git a/lib/rollup/index.test.js b/lib/rollup/index.test.js index 2c2a60dce3..99e25224ac 100644 --- a/lib/rollup/index.test.js +++ b/lib/rollup/index.test.js @@ -13,7 +13,6 @@ describe('Rollup plugin', () => { beforeAll((done) => { Metalsmith(__dirname) - // Use test fixtures .source(source) .destination(destination) @@ -33,8 +32,9 @@ describe('Rollup plugin', () => { }) it('compiles JavaScript to Metalsmith files', () => { - expect(output[normalize('javascripts/entry.js')]) - .toMatchObject({ contents: expect.any(Buffer) }) + expect(output[normalize('javascripts/entry.js')]).toMatchObject({ + contents: expect.any(Buffer) + }) // Source `*.mjs` entry removed expect(output['javascripts/entry.mjs']).toBeUndefined() diff --git a/rollup.config.js b/rollup.config.js index 0917ae46f0..b5afb9b21a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -17,26 +17,24 @@ module.exports = defineConfig({ // Output plugins plugins: [ - process.env.NODE_ENV !== 'development' && terser({ - format: { comments: false }, + process.env.NODE_ENV !== 'development' && + terser({ + format: { comments: false }, - // Include sources content from source maps - // to inspect GOV.UK Frontend code - sourceMap: { - includeSources: true - }, + // Include sources content from source maps + // to inspect GOV.UK Frontend code + sourceMap: { + includeSources: true + }, - // Compatibility workarounds - ecma: 5, - safari10: true - }) + // Compatibility workarounds + ecma: 5, + safari10: true + }) ] } ], // Input plugins - plugins: [ - resolve(), - commonjs() - ] + plugins: [resolve(), commonjs()] }) diff --git a/src/javascripts/application.mjs b/src/javascripts/application.mjs index 2e96adc6bd..5351f36846 100644 --- a/src/javascripts/application.mjs +++ b/src/javascripts/application.mjs @@ -1,7 +1,10 @@ import Analytics from './components/analytics.mjs' import BackToTop from './components/back-to-top.mjs' import CookieBanner from './components/cookie-banner.mjs' -import { getConsentCookie, isValidConsentCookie } from './components/cookie-functions.mjs' +import { + getConsentCookie, + isValidConsentCookie +} from './components/cookie-functions.mjs' import CookiesPage from './components/cookies-page.mjs' import Copy from './components/copy.mjs' import Example from './components/example.mjs' @@ -12,7 +15,9 @@ import Search from './components/search.mjs' import AppTabs from './components/tabs.mjs' // Initialise cookie banner -var $cookieBanner = document.querySelector('[data-module="govuk-cookie-banner"]') +var $cookieBanner = document.querySelector( + '[data-module="govuk-cookie-banner"]' +) new CookieBanner($cookieBanner).init() // Initialise analytics if consent is given diff --git a/src/javascripts/components/analytics.mjs b/src/javascripts/components/analytics.mjs index f7f0c293f9..7a9b056079 100644 --- a/src/javascripts/components/analytics.mjs +++ b/src/javascripts/components/analytics.mjs @@ -1,8 +1,9 @@ -export default function loadAnalytics () { +export default function loadAnalytics() { if (!window.ga || !window.ga.loaded) { // Load gtm script // Script based on snippet at https://developers.google.com/tag-manager/quickstart - (function (w, d, s, l, i) { + // prettier-ignore + ;(function (w, d, s, l, i) { w[l] = w[l] || [] w[l].push({ 'gtm.start': new Date().getTime(), diff --git a/src/javascripts/components/back-to-top.mjs b/src/javascripts/components/back-to-top.mjs index 1bf3649a87..9343d4955a 100644 --- a/src/javascripts/components/back-to-top.mjs +++ b/src/javascripts/components/back-to-top.mjs @@ -1,4 +1,4 @@ -function BackToTop ($module) { +function BackToTop($module) { this.$module = $module } @@ -26,39 +26,41 @@ BackToTop.prototype.init = function () { var subNavIsIntersecting = false var subNavIntersectionRatio = 0 - var observer = new window.IntersectionObserver(function (entries) { - // Find the elements we care about from the entries - var footerEntry = entries.find(function (entry) { - return entry.target === $footer - }) - var subNavEntry = entries.find(function (entry) { - return entry.target === $subNav - }) + var observer = new window.IntersectionObserver( + function (entries) { + // Find the elements we care about from the entries + var footerEntry = entries.find(function (entry) { + return entry.target === $footer + }) + var subNavEntry = entries.find(function (entry) { + return entry.target === $subNav + }) - // If there is an entry this means the element has changed so lets check if it's intersecting. - if (footerEntry) { - footerIsIntersecting = footerEntry.isIntersecting - } - if (subNavEntry) { - subNavIsIntersecting = subNavEntry.isIntersecting - subNavIntersectionRatio = subNavEntry.intersectionRatio - } + // If there is an entry this means the element has changed so lets check if it's intersecting. + if (footerEntry) { + footerIsIntersecting = footerEntry.isIntersecting + } + if (subNavEntry) { + subNavIsIntersecting = subNavEntry.isIntersecting + subNavIntersectionRatio = subNavEntry.intersectionRatio + } - // If the subnav or the footer not visible then fix the back to top link to follow the user - if (subNavIsIntersecting || footerIsIntersecting) { - this.$module.classList.remove('app-back-to-top--fixed') - } else { - this.$module.classList.add('app-back-to-top--fixed') - } + // If the subnav or the footer not visible then fix the back to top link to follow the user + if (subNavIsIntersecting || footerIsIntersecting) { + this.$module.classList.remove('app-back-to-top--fixed') + } else { + this.$module.classList.add('app-back-to-top--fixed') + } - // If the subnav is visible but you can see it all at once, then a back to top link is likely not as useful. - // We hide the link but make it focusable for screen readers users who might still find it useful. - if (subNavIsIntersecting && subNavIntersectionRatio === 1) { - this.$module.classList.add('app-back-to-top--hidden') - } else { - this.$module.classList.remove('app-back-to-top--hidden') - } - }.bind(this)) + // If the subnav is visible but you can see it all at once, then a back to top link is likely not as useful. + // We hide the link but make it focusable for screen readers users who might still find it useful. + if (subNavIsIntersecting && subNavIntersectionRatio === 1) { + this.$module.classList.add('app-back-to-top--hidden') + } else { + this.$module.classList.remove('app-back-to-top--hidden') + } + }.bind(this) + ) observer.observe($footer) observer.observe($subNav) diff --git a/src/javascripts/components/cookie-banner.mjs b/src/javascripts/components/cookie-banner.mjs index 3535672535..50e6f07d58 100644 --- a/src/javascripts/components/cookie-banner.mjs +++ b/src/javascripts/components/cookie-banner.mjs @@ -8,7 +8,7 @@ var cookieMessageSelector = '.js-cookie-banner-message' var cookieConfirmationAcceptSelector = '.js-cookie-banner-confirmation-accept' var cookieConfirmationRejectSelector = '.js-cookie-banner-confirmation-reject' -function CookieBanner ($module) { +function CookieBanner($module) { this.$module = $module } @@ -17,9 +17,15 @@ CookieBanner.prototype.init = function () { this.$acceptButton = this.$module.querySelector(cookieBannerAcceptSelector) this.$rejectButton = this.$module.querySelector(cookieBannerRejectSelector) this.$cookieMessage = this.$module.querySelector(cookieMessageSelector) - this.$cookieConfirmationAccept = this.$module.querySelector(cookieConfirmationAcceptSelector) - this.$cookieConfirmationReject = this.$module.querySelector(cookieConfirmationRejectSelector) - this.$cookieBannerHideButtons = this.$module.querySelectorAll(cookieBannerHideButtonSelector) + this.$cookieConfirmationAccept = this.$module.querySelector( + cookieConfirmationAcceptSelector + ) + this.$cookieConfirmationReject = this.$module.querySelector( + cookieConfirmationRejectSelector + ) + this.$cookieBannerHideButtons = this.$module.querySelectorAll( + cookieBannerHideButtonSelector + ) // Exit if no cookie banner module // or if we're on the cookies page to avoid circular journeys @@ -31,7 +37,10 @@ CookieBanner.prototype.init = function () { // outdated consent cookie var currentConsentCookie = CookieFunctions.getConsentCookie() - if (!currentConsentCookie || !CookieFunctions.isValidConsentCookie(currentConsentCookie)) { + if ( + !currentConsentCookie || + !CookieFunctions.isValidConsentCookie(currentConsentCookie) + ) { // If the consent cookie version is not valid, we need to remove any cookies which have been // set previously CookieFunctions.resetCookies() @@ -42,9 +51,15 @@ CookieBanner.prototype.init = function () { this.$acceptButton.addEventListener('click', this.acceptCookies.bind(this)) this.$rejectButton.addEventListener('click', this.rejectCookies.bind(this)) - nodeListForEach(this.$cookieBannerHideButtons, function ($cookieBannerHideButton) { - $cookieBannerHideButton.addEventListener('click', this.hideBanner.bind(this)) - }.bind(this)) + nodeListForEach( + this.$cookieBannerHideButtons, + function ($cookieBannerHideButton) { + $cookieBannerHideButton.addEventListener( + 'click', + this.hideBanner.bind(this) + ) + }.bind(this) + ) } CookieBanner.prototype.hideBanner = function () { @@ -69,7 +84,9 @@ CookieBanner.prototype.rejectCookies = function () { this.revealConfirmationMessage(this.$cookieConfirmationReject) } -CookieBanner.prototype.revealConfirmationMessage = function (confirmationMessage) { +CookieBanner.prototype.revealConfirmationMessage = function ( + confirmationMessage +) { confirmationMessage.removeAttribute('hidden') // Set tabindex to -1 to make the confirmation banner focusable with JavaScript diff --git a/src/javascripts/components/cookie-functions.mjs b/src/javascripts/components/cookie-functions.mjs index 58b2290afb..35bf5f3320 100644 --- a/src/javascripts/components/cookie-functions.mjs +++ b/src/javascripts/components/cookie-functions.mjs @@ -23,7 +23,12 @@ var TRACKING_LIVE_ID = '116229859-1' /* Users can (dis)allow different groups of cookies. */ var COOKIE_CATEGORIES = { - analytics: ['_ga', '_gid', '_gat_UA-' + TRACKING_PREVIEW_ID, '_gat_UA-' + TRACKING_LIVE_ID], + analytics: [ + '_ga', + '_gid', + '_gat_UA-' + TRACKING_PREVIEW_ID, + '_gat_UA-' + TRACKING_LIVE_ID + ], /* Essential cookies * * Essential cookies cannot be deselected, but we want our cookie code to @@ -58,7 +63,7 @@ var DEFAULT_COOKIE_CONSENT = { * Deleting a cookie: * Cookie('hobnob', null) */ -export function Cookie (name, value, options) { +export function Cookie(name, value, options) { if (typeof value !== 'undefined') { if (value === false || value === null) { deleteCookie(name) @@ -79,7 +84,7 @@ export function Cookie (name, value, options) { * If the consent cookie is malformed, or not present, * returns null. */ -export function getConsentCookie () { +export function getConsentCookie() { var consentCookie = getCookie(CONSENT_COOKIE_NAME) var consentCookieObj @@ -103,12 +108,12 @@ export function getConsentCookie () { * * This is also duplicated in cookie-banner.njk - the two need to be kept in sync */ -export function isValidConsentCookie (options) { - return (options && options.version >= window.GDS_CONSENT_COOKIE_VERSION) +export function isValidConsentCookie(options) { + return options && options.version >= window.GDS_CONSENT_COOKIE_VERSION } /** Update the user's cookie preferences. */ -export function setConsentCookie (options) { +export function setConsentCookie(options) { var cookieConsent = getConsentCookie() if (!cookieConsent) { @@ -136,7 +141,7 @@ export function setConsentCookie (options) { * * Deletes any cookies the user has not consented to. */ -export function resetCookies () { +export function resetCookies() { var options = getConsentCookie() // If no preferences or old version use the default @@ -178,7 +183,7 @@ export function resetCookies () { } } -function userAllowsCookieCategory (cookieCategory, cookiePreferences) { +function userAllowsCookieCategory(cookieCategory, cookiePreferences) { // Essential cookies are always allowed if (cookieCategory === 'essential') { return true @@ -193,7 +198,7 @@ function userAllowsCookieCategory (cookieCategory, cookiePreferences) { } } -function userAllowsCookie (cookieName) { +function userAllowsCookie(cookieName) { // Always allow setting the consent cookie if (cookieName === CONSENT_COOKIE_NAME) { return true @@ -219,7 +224,7 @@ function userAllowsCookie (cookieName) { return false } -function getCookie (name) { +function getCookie(name) { var nameEQ = name + '=' var cookies = document.cookie.split(';') for (var i = 0, len = cookies.length; i < len; i++) { @@ -234,7 +239,7 @@ function getCookie (name) { return null } -function setCookie (name, value, options) { +function setCookie(name, value, options) { if (userAllowsCookie(name)) { if (typeof options === 'undefined') { options = {} @@ -242,7 +247,7 @@ function setCookie (name, value, options) { var cookieString = name + '=' + value + '; path=/' if (options.days) { var date = new Date() - date.setTime(date.getTime() + (options.days * 24 * 60 * 60 * 1000)) + date.setTime(date.getTime() + options.days * 24 * 60 * 60 * 1000) cookieString = cookieString + '; expires=' + date.toGMTString() } if (document.location.protocol === 'https:') { @@ -252,14 +257,22 @@ function setCookie (name, value, options) { } } -function deleteCookie (name) { +function deleteCookie(name) { if (Cookie(name)) { // Cookies need to be deleted in the same level of specificity in which they were set // If a cookie was set with a specified domain, it needs to be specified when deleted // If a cookie wasn't set with the domain attribute, it shouldn't be there when deleted // You can't tell if a cookie was set with a domain attribute or not, so try both options document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/' - document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;domain=' + window.location.hostname + ';path=/' - document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;domain=.' + window.location.hostname + ';path=/' + document.cookie = + name + + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;domain=' + + window.location.hostname + + ';path=/' + document.cookie = + name + + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT;domain=.' + + window.location.hostname + + ';path=/' } } diff --git a/src/javascripts/components/cookies-page.mjs b/src/javascripts/components/cookies-page.mjs index 978ae1a582..c3d2dd0d6a 100644 --- a/src/javascripts/components/cookies-page.mjs +++ b/src/javascripts/components/cookies-page.mjs @@ -1,7 +1,7 @@ import { getConsentCookie, setConsentCookie } from './cookie-functions.mjs' import { nodeListForEach } from './helpers.mjs' -function CookiesPage ($module) { +function CookiesPage($module) { this.$module = $module } @@ -13,16 +13,25 @@ CookiesPage.prototype.init = function () { } this.$cookieForm = this.$cookiePage.querySelector('.js-cookies-page-form') - this.$cookieFormFieldsets = this.$cookieForm.querySelectorAll('.js-cookies-page-form-fieldset') - this.$successNotification = this.$cookiePage.querySelector('.js-cookies-page-success') - - nodeListForEach(this.$cookieFormFieldsets, function ($cookieFormFieldset) { - this.showUserPreference($cookieFormFieldset, getConsentCookie()) - $cookieFormFieldset.removeAttribute('hidden') - }.bind(this)) + this.$cookieFormFieldsets = this.$cookieForm.querySelectorAll( + '.js-cookies-page-form-fieldset' + ) + this.$successNotification = this.$cookiePage.querySelector( + '.js-cookies-page-success' + ) + + nodeListForEach( + this.$cookieFormFieldsets, + function ($cookieFormFieldset) { + this.showUserPreference($cookieFormFieldset, getConsentCookie()) + $cookieFormFieldset.removeAttribute('hidden') + }.bind(this) + ) // Show submit button - this.$cookieForm.querySelector('.js-cookies-form-button').removeAttribute('hidden') + this.$cookieForm + .querySelector('.js-cookies-form-button') + .removeAttribute('hidden') this.$cookieForm.addEventListener('submit', this.savePreferences.bind(this)) } @@ -33,19 +42,27 @@ CookiesPage.prototype.savePreferences = function (event) { var preferences = {} - nodeListForEach(this.$cookieFormFieldsets, function ($cookieFormFieldset) { - var cookieType = this.getCookieType($cookieFormFieldset) - var selectedItem = $cookieFormFieldset.querySelector('input[name=' + cookieType + ']:checked').value + nodeListForEach( + this.$cookieFormFieldsets, + function ($cookieFormFieldset) { + var cookieType = this.getCookieType($cookieFormFieldset) + var selectedItem = $cookieFormFieldset.querySelector( + 'input[name=' + cookieType + ']:checked' + ).value - preferences[cookieType] = selectedItem === 'yes' - }.bind(this)) + preferences[cookieType] = selectedItem === 'yes' + }.bind(this) + ) // Save preferences to cookie and show success notification setConsentCookie(preferences) this.showSuccessNotification() } -CookiesPage.prototype.showUserPreference = function ($cookieFormFieldset, preferences) { +CookiesPage.prototype.showUserPreference = function ( + $cookieFormFieldset, + preferences +) { var cookieType = this.getCookieType($cookieFormFieldset) var preference = false @@ -54,7 +71,9 @@ CookiesPage.prototype.showUserPreference = function ($cookieFormFieldset, prefer } var radioValue = preference ? 'yes' : 'no' - var radio = $cookieFormFieldset.querySelector('input[name=' + cookieType + '][value=' + radioValue + ']') + var radio = $cookieFormFieldset.querySelector( + 'input[name=' + cookieType + '][value=' + radioValue + ']' + ) radio.checked = true } diff --git a/src/javascripts/components/copy.mjs b/src/javascripts/components/copy.mjs index 84ef90c23f..b3bc1fba6c 100644 --- a/src/javascripts/components/copy.mjs +++ b/src/javascripts/components/copy.mjs @@ -1,6 +1,6 @@ import ClipboardJS from 'clipboard' -function Copy ($module) { +function Copy($module) { this.$module = $module } diff --git a/src/javascripts/components/example.mjs b/src/javascripts/components/example.mjs index eabddb1fca..58112aff52 100644 --- a/src/javascripts/components/example.mjs +++ b/src/javascripts/components/example.mjs @@ -8,7 +8,7 @@ import iFrameResize from 'iframe-resizer/js/iframeResizer.js' * * @param {Element} $module - HTML element to use for example */ -function Example ($module) { +function Example($module) { if (!($module instanceof HTMLIFrameElement)) { return } diff --git a/src/javascripts/components/helpers.mjs b/src/javascripts/components/helpers.mjs index cf9337ae8d..a51b4dd139 100644 --- a/src/javascripts/components/helpers.mjs +++ b/src/javascripts/components/helpers.mjs @@ -2,7 +2,7 @@ * TODO: Ideally this would be a NodeList.prototype.forEach polyfill * See: https://github.com/imagitama/nodelist-foreach-polyfill */ -export function nodeListForEach (nodes, callback) { +export function nodeListForEach(nodes, callback) { if (window.NodeList.prototype.forEach) { return nodes.forEach(callback) } diff --git a/src/javascripts/components/navigation.mjs b/src/javascripts/components/navigation.mjs index f96c4e61b4..5337c6c99b 100644 --- a/src/javascripts/components/navigation.mjs +++ b/src/javascripts/components/navigation.mjs @@ -6,7 +6,7 @@ var subNavActiveClass = 'app-navigation__subnav--active' // This one has the query dot at the beginning because it's only ever used in querySelector calls var subNavJSClass = '.js-app-navigation__subnav' -function Navigation ($module) { +function Navigation($module) { this.$module = $module || document this.$nav = this.$module.querySelector('.js-app-navigation') @@ -68,7 +68,10 @@ Navigation.prototype.setInitialAriaStates = function () { $nextSubNav.setAttribute('id', nextSubNavId) $button.setAttribute('id', subNavTogglerId) - $button.setAttribute('aria-expanded', $nextSubNav.hasAttribute('hidden') ? 'false' : 'true') + $button.setAttribute( + 'aria-expanded', + $nextSubNav.hasAttribute('hidden') ? 'false' : 'true' + ) $button.setAttribute('aria-controls', nextSubNavId) } }) @@ -79,25 +82,28 @@ Navigation.prototype.bindUIEvents = function () { var $navToggler = this.$navToggler var $navButtons = this.$navButtons - $navToggler.addEventListener('click', function (event) { - if (this.mobileNavOpen) { - $nav.classList.remove(navActiveClass) - $navToggler.classList.remove(navMenuButtonActiveClass) - $nav.setAttribute('hidden', '') + $navToggler.addEventListener( + 'click', + function (event) { + if (this.mobileNavOpen) { + $nav.classList.remove(navActiveClass) + $navToggler.classList.remove(navMenuButtonActiveClass) + $nav.setAttribute('hidden', '') - $navToggler.setAttribute('aria-expanded', 'false') + $navToggler.setAttribute('aria-expanded', 'false') - this.mobileNavOpen = false - } else { - $nav.classList.add(navActiveClass) - $navToggler.classList.add(navMenuButtonActiveClass) - $nav.removeAttribute('hidden') + this.mobileNavOpen = false + } else { + $nav.classList.add(navActiveClass) + $navToggler.classList.add(navMenuButtonActiveClass) + $nav.removeAttribute('hidden') - $navToggler.setAttribute('aria-expanded', 'true') + $navToggler.setAttribute('aria-expanded', 'true') - this.mobileNavOpen = true - } - }.bind(this)) + this.mobileNavOpen = true + } + }.bind(this) + ) nodeListForEach($navButtons, function ($button, index) { $button.addEventListener('click', function (event) { diff --git a/src/javascripts/components/options-table.mjs b/src/javascripts/components/options-table.mjs index 4dcde6116c..172557e09d 100644 --- a/src/javascripts/components/options-table.mjs +++ b/src/javascripts/components/options-table.mjs @@ -16,14 +16,22 @@ var OptionsTable = { } if (exampleName) { - var tabLink = document.querySelector('a[href="#' + exampleName + '-nunjucks"]') + var tabLink = document.querySelector( + 'a[href="#' + exampleName + '-nunjucks"]' + ) var tabHeading = tabLink ? tabLink.parentNode : null - var optionsDetailsElement = document.getElementById('options-' + exampleName + '-details') + var optionsDetailsElement = document.getElementById( + 'options-' + exampleName + '-details' + ) if (tabHeading && optionsDetailsElement) { var tabsElement = optionsDetailsElement.parentNode - var detailsSummary = optionsDetailsElement.querySelector('.govuk-details__summary') - var detailsText = optionsDetailsElement.querySelector('.govuk-details__text') + var detailsSummary = optionsDetailsElement.querySelector( + '.govuk-details__summary' + ) + var detailsText = optionsDetailsElement.querySelector( + '.govuk-details__text' + ) if (detailsSummary && detailsText) { tabLink.setAttribute('aria-expanded', 'true') diff --git a/src/javascripts/components/search.mjs b/src/javascripts/components/search.mjs index b683b5b180..a41f3f65d3 100644 --- a/src/javascripts/components/search.mjs +++ b/src/javascripts/components/search.mjs @@ -27,10 +27,10 @@ var DEBOUNCE_TIME_TO_WAIT = function () { // We want to be able to reduce this timeout in order to make sure // our tests do not run very slowly. var timeout = window.__SITE_SEARCH_TRACKING_TIMEOUT - return (typeof timeout !== 'undefined') ? timeout : 2000 // milliseconds + return typeof timeout !== 'undefined' ? timeout : 2000 // milliseconds } -function Search ($module) { +function Search($module) { this.$module = $module } @@ -99,7 +99,7 @@ Search.prototype.inputValueTemplate = function (result) { } Search.prototype.resultTemplate = function (result) { - function startsWithFilter (words, query) { + function startsWithFilter(words, query) { return words.filter(function (word) { return word.trim().toLowerCase().indexOf(query.toLowerCase()) === 0 }) @@ -122,7 +122,10 @@ Search.prototype.resultTemplate = function (result) { // Aliases containing words that start with the query var matchedAliases = aliases.reduce(function (aliasesFiltered, alias) { - var aliasWordsMatched = startsWithFilter(alias.match(/\w+/g) || [], searchQuery) + var aliasWordsMatched = startsWithFilter( + alias.match(/\w+/g) || [], + searchQuery + ) return aliasWordsMatched.length ? aliasesFiltered.concat([alias]) @@ -166,7 +169,9 @@ Search.prototype.init = function () { inputValue: this.inputValueTemplate, suggestion: this.resultTemplate }, - tNoResults: function () { return statusMessage } + tNoResults: function () { + return statusMessage + } }) var $input = $module.querySelector('.app-site-search__input') @@ -177,9 +182,12 @@ Search.prototype.init = function () { }) var searchIndexUrl = $module.getAttribute('data-search-index') - this.fetchSearchIndex(searchIndexUrl, function () { - this.renderResults() - }.bind(this)) + this.fetchSearchIndex( + searchIndexUrl, + function () { + this.renderResults() + }.bind(this) + ) } export default Search diff --git a/src/javascripts/components/search.tracking.mjs b/src/javascripts/components/search.tracking.mjs index c25ef10332..55e767ed14 100644 --- a/src/javascripts/components/search.tracking.mjs +++ b/src/javascripts/components/search.tracking.mjs @@ -1,39 +1,44 @@ -function addToDataLayer (payload) { +function addToDataLayer(payload) { window.dataLayer = window.dataLayer || [] window.dataLayer.push(payload) } -function stripPossiblePII (string) { +function stripPossiblePII(string) { // Try to detect emails, postcodes, and NI numbers, and redact them. // Regexes copied from GTM variable 'JS - Remove PII from Hit Payload' string = string.replace(/[^\s=/?&]+(?:@|%40)[^\s=/?&]+/g, '[REDACTED EMAIL]') - string = string.replace(/\b[A-PR-UWYZ][A-HJ-Z]?[0-9][0-9A-HJKMNPR-Y]?(?:[\s+]|%20)*[0-9](?!refund)[ABD-HJLNPQ-Z]{2,3}\b/gi, '[REDACTED POSTCODE]') - string = string.replace(/^\s*[a-zA-Z]{2}(?:\s*\d\s*){6}[a-zA-Z]?\s*$/g, '[REDACTED NI NUMBER]') + string = string.replace( + /\b[A-PR-UWYZ][A-HJ-Z]?[0-9][0-9A-HJKMNPR-Y]?(?:[\s+]|%20)*[0-9](?!refund)[ABD-HJLNPQ-Z]{2,3}\b/gi, + '[REDACTED POSTCODE]' + ) + string = string.replace( + /^\s*[a-zA-Z]{2}(?:\s*\d\s*){6}[a-zA-Z]?\s*$/g, + '[REDACTED NI NUMBER]' + ) // If someone has typed in a number it's likely not related so redact it string = string.replace(/[0-9]+/g, '[REDACTED NUMBER]') return string } -export function trackConfirm (searchQuery, searchResults, result) { +export function trackConfirm(searchQuery, searchResults, result) { if (window.DO_NOT_TRACK_ENABLED) { return } var searchTerm = stripPossiblePII(searchQuery) - var products = - searchResults - .map(function (result, key) { - return { - name: result.title, - category: result.section, - list: searchTerm, // Used to match an searchTerm with results - position: (key + 1) - } - }) - .filter(function (product) { - // Only return the product that matches what was clicked - return product.name === result.title - }) + var products = searchResults + .map(function (result, key) { + return { + name: result.title, + category: result.section, + list: searchTerm, // Used to match an searchTerm with results + position: key + 1 + } + }) + .filter(function (product) { + // Only return the product that matches what was clicked + return product.name === result.title + }) addToDataLayer({ event: 'site-search', @@ -51,21 +56,21 @@ export function trackConfirm (searchQuery, searchResults, result) { }) } -export function trackSearchResults (searchQuery, searchResults) { +export function trackSearchResults(searchQuery, searchResults) { if (window.DO_NOT_TRACK_ENABLED) { return } var searchTerm = stripPossiblePII(searchQuery) - var hasResults = (searchResults.length > 0) + var hasResults = searchResults.length > 0 // Impressions is Google Analytics lingo for what people have seen. var impressions = searchResults.map(function (result, key) { return { name: result.title, category: result.section, list: searchTerm, // Used to match an searchTerm with results - position: (key + 1) + position: key + 1 } }) diff --git a/src/javascripts/components/tabs.mjs b/src/javascripts/components/tabs.mjs index ab2246df40..f6c2ae1860 100644 --- a/src/javascripts/components/tabs.mjs +++ b/src/javascripts/components/tabs.mjs @@ -11,7 +11,7 @@ import { nodeListForEach } from './helpers.mjs' * - panels - the content that is shown/hidden/switched; same across all breakpoints */ -function AppTabs ($module) { +function AppTabs($module) { this.$module = $module this.$mobileTabs = this.$module.querySelectorAll('.js-tabs__heading a') this.$desktopTabs = this.$module.querySelectorAll('.js-tabs__item a') @@ -28,9 +28,12 @@ AppTabs.prototype.init = function () { this.enhanceMobileTabs() // Add bindings to desktop tabs - nodeListForEach(this.$desktopTabs, function ($tab) { - $tab.addEventListener('click', this.onClick.bind(this)) - }.bind(this)) + nodeListForEach( + this.$desktopTabs, + function ($tab) { + $tab.addEventListener('click', this.onClick.bind(this)) + }.bind(this) + ) // Reset all tabs and panels to closed state // We also add all our default ARIA goodness here @@ -74,20 +77,23 @@ AppTabs.prototype.onClick = function (event) { */ AppTabs.prototype.enhanceMobileTabs = function () { // Loop through mobile tabs... - nodeListForEach(this.$mobileTabs, function ($tab) { - // ...construct a button equivalent of each anchor... - var $button = document.createElement('button') - $button.setAttribute('aria-controls', $tab.getAttribute('aria-controls')) - $button.setAttribute('data-track', $tab.getAttribute('data-track')) - $button.classList.add('app-tabs__heading-button') - $button.innerHTML = $tab.innerHTML - // ...bind controls... - $button.bindClick = this.onClick.bind(this) - $button.addEventListener('click', $button.bindClick) - // ...and replace the anchor with the button - $tab.parentNode.appendChild($button) - $tab.parentNode.removeChild($tab) - }.bind(this)) + nodeListForEach( + this.$mobileTabs, + function ($tab) { + // ...construct a button equivalent of each anchor... + var $button = document.createElement('button') + $button.setAttribute('aria-controls', $tab.getAttribute('aria-controls')) + $button.setAttribute('data-track', $tab.getAttribute('data-track')) + $button.classList.add('app-tabs__heading-button') + $button.innerHTML = $tab.innerHTML + // ...bind controls... + $button.bindClick = this.onClick.bind(this) + $button.addEventListener('click', $button.bindClick) + // ...and replace the anchor with the button + $tab.parentNode.appendChild($button) + $tab.parentNode.removeChild($tab) + }.bind(this) + ) // Replace the value of $mobileTabs with the new buttons this.$mobileTabs = this.$module.querySelectorAll('.js-tabs__heading button') @@ -97,12 +103,15 @@ AppTabs.prototype.enhanceMobileTabs = function () { * Reset tabs and panels to closed state */ AppTabs.prototype.resetTabs = function () { - nodeListForEach(this.$panels, function ($panel) { - // We don't want to hide the panel if there are no tabs present to show it - if (!$panel.classList.contains('js-tabs__container--no-tabs')) { - this.closePanel($panel.id) - } - }.bind(this)) + nodeListForEach( + this.$panels, + function ($panel) { + // We don't want to hide the panel if there are no tabs present to show it + if (!$panel.classList.contains('js-tabs__container--no-tabs')) { + this.closePanel($panel.id) + } + }.bind(this) + ) } /** @@ -156,7 +165,9 @@ AppTabs.prototype.getMobileTab = function (panelId) { AppTabs.prototype.getDesktopTab = function (panelId) { var $desktopTabContainer = this.$module.querySelector('.app-tabs') if ($desktopTabContainer) { - return $desktopTabContainer.querySelector('[aria-controls="' + panelId + '"]') + return $desktopTabContainer.querySelector( + '[aria-controls="' + panelId + '"]' + ) } return null } diff --git a/src/javascripts/example.mjs b/src/javascripts/example.mjs index 08e08eeb83..01eb479c98 100644 --- a/src/javascripts/example.mjs +++ b/src/javascripts/example.mjs @@ -1,4 +1,4 @@ -function ExamplePage ($module) { +function ExamplePage($module) { this.$module = $module } ExamplePage.prototype.init = function () { diff --git a/tasks/build.js b/tasks/build.js index 101c6a8611..5a44902f78 100644 --- a/tasks/build.js +++ b/tasks/build.js @@ -2,5 +2,7 @@ const metalsmith = require('../lib/metalsmith') // configured static site genera // build to destination directory metalsmith.build(function (err, files) { - if (err) { throw err } + if (err) { + throw err + } })