From a533c7e41522e136b6c0f2d715b5660e01742f54 Mon Sep 17 00:00:00 2001 From: Romaric Pascal Date: Thu, 27 Jul 2023 18:00:28 +0100 Subject: [PATCH] Add `GOVUKFrontendNotSupported` error Allows components to indicate they didn't inistantiate because GOV.UK Frontend is not supported --- packages/govuk-frontend/src/govuk/all.mjs | 2 ++ .../govuk/components/accordion/accordion.mjs | 7 +++- .../components/accordion/accordion.test.js | 22 +++++++++++++ .../src/govuk/components/globals.test.js | 1 + .../govuk-frontend/src/govuk/errors/index.mjs | 33 +++++++++++++++++++ .../src/govuk/errors/index.unit.test.mjs | 31 +++++++++++++++++ shared/helpers/puppeteer.js | 22 ++++++++++--- 7 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 packages/govuk-frontend/src/govuk/errors/index.mjs create mode 100644 packages/govuk-frontend/src/govuk/errors/index.unit.test.mjs diff --git a/packages/govuk-frontend/src/govuk/all.mjs b/packages/govuk-frontend/src/govuk/all.mjs index 264db6f4a7..56e11bf9c3 100644 --- a/packages/govuk-frontend/src/govuk/all.mjs +++ b/packages/govuk-frontend/src/govuk/all.mjs @@ -12,6 +12,7 @@ import { NotificationBanner } from './components/notification-banner/notificatio import { Radios } from './components/radios/radios.mjs' import { SkipLink } from './components/skip-link/skip-link.mjs' import { Tabs } from './components/tabs/tabs.mjs' +import { GOVUKFrontendError } from './errors/index.mjs' /** * Initialise all components @@ -103,6 +104,7 @@ export { Checkboxes, ErrorSummary, ExitThisPage, + GOVUKFrontendError, Header, NotificationBanner, Radios, diff --git a/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs b/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs index 3a50fcaee4..046c8b68fb 100644 --- a/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs +++ b/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs @@ -1,5 +1,6 @@ import { mergeConfigs, extractConfigByNamespace } from '../../common/index.mjs' import { normaliseDataset } from '../../common/normalise-dataset.mjs' +import { GOVUKFrontendNotSupportedError } from '../../errors/index.mjs' import { I18n } from '../../i18n.mjs' /** @@ -113,7 +114,11 @@ export class Accordion { * @param {AccordionConfig} [config] - Accordion config */ constructor ($module, config) { - if (!($module instanceof HTMLElement) || !document.body.classList.contains('govuk-frontend-supported')) { + if (!document.body.classList.contains('govuk-frontend-supported')) { + throw new GOVUKFrontendNotSupportedError() + } + + if (!($module instanceof HTMLElement)) { return this } diff --git a/packages/govuk-frontend/src/govuk/components/accordion/accordion.test.js b/packages/govuk-frontend/src/govuk/components/accordion/accordion.test.js index a032adb840..9312ae65e9 100644 --- a/packages/govuk-frontend/src/govuk/components/accordion/accordion.test.js +++ b/packages/govuk-frontend/src/govuk/components/accordion/accordion.test.js @@ -508,6 +508,28 @@ describe('/components/accordion', () => { ) }) }) + + describe('Errors at instantiation', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('accordion') + }) + + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'accordion', { + params: examples.default, + beforeInitialisation () { + document.body.classList.remove('govuk-frontend-supported') + } + }) + ).rejects.toEqual({ + name: 'GOVUKFrontendNotSupportedError', + message: 'GOV.UK Frontend is not supported in this browser' + }) + }) + }) }) }) }) diff --git a/packages/govuk-frontend/src/govuk/components/globals.test.js b/packages/govuk-frontend/src/govuk/components/globals.test.js index f412f8bf90..ca32701dc0 100644 --- a/packages/govuk-frontend/src/govuk/components/globals.test.js +++ b/packages/govuk-frontend/src/govuk/components/globals.test.js @@ -37,6 +37,7 @@ describe('GOV.UK Frontend', () => { 'Checkboxes', 'ErrorSummary', 'ExitThisPage', + 'GOVUKFrontendError', 'Header', 'NotificationBanner', 'Radios', diff --git a/packages/govuk-frontend/src/govuk/errors/index.mjs b/packages/govuk-frontend/src/govuk/errors/index.mjs new file mode 100644 index 0000000000..336ee74245 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/errors/index.mjs @@ -0,0 +1,33 @@ +/** + * A base class for `Error`s thrown by GOV.UK Frontend. + * + * It is meant to be extended into specific types of errors + * to be thrown by our code. + * + * @example + * ```js + * class MissingRootError extends GOVUKFrontendError { + * // Setting an explicit name is important as extending the class will not + * // set a new `name` on the subclass. The `name` property is important + * // to ensure intelligible error names even if the class name gets + * // mangled by a minifier + * name = "MissingRootError" + * } + * ``` + * @abstract + */ +export class GOVUKFrontendError extends Error { + name = 'GOVUKFrontendError' +} + +/** + * Indicates that GOV.UK Frontend is not supported + */ +export class GOVUKFrontendNotSupportedError extends GOVUKFrontendError { + name = 'GOVUKFrontendNotSupportedError' + + /** */ + constructor () { + super('GOV.UK Frontend is not supported in this browser') + } +} diff --git a/packages/govuk-frontend/src/govuk/errors/index.unit.test.mjs b/packages/govuk-frontend/src/govuk/errors/index.unit.test.mjs new file mode 100644 index 0000000000..fef19d3987 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/errors/index.unit.test.mjs @@ -0,0 +1,31 @@ +import { GOVUKFrontendError, GOVUKFrontendNotSupportedError } from './index.mjs' + +describe('errors', () => { + describe('GOVUKFrontendError', () => { + it('allows subclasses to set a custom name', () => { + class CustomError extends GOVUKFrontendError { + name = 'CustomName' + } + + expect(new CustomError().name).toBe('CustomName') + }) + }) + + describe('GOVUKFrontendNotSupportedError', () => { + it('is an instance of GOVUKFrontendError', () => { + expect(new GOVUKFrontendNotSupportedError()).toBeInstanceOf( + GOVUKFrontendError + ) + }) + it('has its own name set', () => { + expect(new GOVUKFrontendNotSupportedError().name).toBe( + 'GOVUKFrontendNotSupportedError' + ) + }) + it('provides meaningfull feedback to users', () => { + expect(new GOVUKFrontendNotSupportedError().message).toBe( + 'GOV.UK Frontend is not supported in this browser' + ) + }) + }) +}) diff --git a/shared/helpers/puppeteer.js b/shared/helpers/puppeteer.js index ffc1751404..e7c8d5c787 100644 --- a/shared/helpers/puppeteer.js +++ b/shared/helpers/puppeteer.js @@ -98,11 +98,25 @@ async function renderAndInitialise (page, componentName, options) { } // Run a script to init the JavaScript component - await componentRootHandle.evaluate(async ($module, exportName, options) => { + // Puppeteer returns very little information on errors thrown during `evaluate`, + // only a `name` that maps to the error class (and not its `name` property, + // which means we get a mangled value). + // As a workaround, we can gather and `return` the values we need from inside the browser, + // and throw them when back in Jest (to keep them triggering a Promise rejection) + const error = await componentRootHandle.evaluate(async ($module, exportName, options) => { const namespace = await import('govuk-frontend') - /* eslint-disable-next-line no-new */ - new namespace[exportName]($module, options.config) - }, componentNameToClassName(componentName), options, options.beforeInitialisation) + + try { + /* eslint-disable-next-line no-new */ + new namespace[exportName]($module, options.config) + } catch ({ name, message }) { + return { name, message } + } + }, componentNameToClassName(componentName), options) + + if (error) { + throw error + } return page }