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..369ac5f600 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/button/button.mjs b/packages/govuk-frontend/src/govuk/components/button/button.mjs index a2304c9a88..ba07fe7689 100644 --- a/packages/govuk-frontend/src/govuk/components/button/button.mjs +++ b/packages/govuk-frontend/src/govuk/components/button/button.mjs @@ -1,5 +1,6 @@ import { mergeConfigs } from '../../common/index.mjs' import { normaliseDataset } from '../../common/normalise-dataset.mjs' +import { GOVUKFrontendNotSupportedError } from '../../errors/index.mjs' const KEY_SPACE = 32 const DEBOUNCE_TIMEOUT_IN_SECONDS = 1 @@ -29,7 +30,11 @@ export class Button { * @param {ButtonConfig} [config] - Button 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/button/button.test.js b/packages/govuk-frontend/src/govuk/components/button/button.test.js index d06737b5f5..466096c02f 100644 --- a/packages/govuk-frontend/src/govuk/components/button/button.test.js +++ b/packages/govuk-frontend/src/govuk/components/button/button.test.js @@ -312,5 +312,27 @@ describe('/components/button', () => { expect(button2Counts.debounce).toBe(0) }) }) + + describe('errors at instantiation', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('button') + }) + + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'button', { + 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/character-count/character-count.mjs b/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs index 54edb24dc0..2b29568373 100644 --- a/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs +++ b/packages/govuk-frontend/src/govuk/components/character-count/character-count.mjs @@ -1,6 +1,7 @@ import { closestAttributeValue } from '../../common/closest-attribute-value.mjs' import { extractConfigByNamespace, mergeConfigs } from '../../common/index.mjs' import { normaliseDataset } from '../../common/normalise-dataset.mjs' +import { GOVUKFrontendNotSupportedError } from '../../errors/index.mjs' import { I18n } from '../../i18n.mjs' /** @@ -64,7 +65,11 @@ export class CharacterCount { * @param {CharacterCountConfig} [config] - Character count 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/character-count/character-count.test.js b/packages/govuk-frontend/src/govuk/components/character-count/character-count.test.js index a2e9e445b0..7a9de0c6a1 100644 --- a/packages/govuk-frontend/src/govuk/components/character-count/character-count.test.js +++ b/packages/govuk-frontend/src/govuk/components/character-count/character-count.test.js @@ -634,6 +634,28 @@ describe('Character count', () => { ) }) }) + + describe('errors at instantiation', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('character-count') + }) + + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'character-count', { + 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' + }) + }) + }) }) describe('in mismatched locale', () => { diff --git a/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs b/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs index cddcb5dc9f..92615aad00 100644 --- a/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs +++ b/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs @@ -1,3 +1,5 @@ +import { GOVUKFrontendNotSupportedError } from '../../errors/index.mjs' + /** * Checkboxes component */ @@ -23,7 +25,11 @@ export class Checkboxes { * @param {Element} $module - HTML element to use for checkboxes */ constructor ($module) { - 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/checkboxes/checkboxes.test.js b/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.test.js index c953f2d943..d67dd7d704 100644 --- a/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.test.js +++ b/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.test.js @@ -1,4 +1,5 @@ -const { goToComponent, goToExample, getAttribute, getProperty, isVisible } = require('govuk-frontend-helpers/puppeteer') +const { goToComponent, goToExample, getAttribute, getProperty, isVisible, renderAndInitialise } = require('govuk-frontend-helpers/puppeteer') +const { getExamples } = require('govuk-frontend-lib/files.js') describe('Checkboxes with conditional reveals', () => { describe('when JavaScript is unavailable or fails', () => { @@ -286,5 +287,27 @@ describe('Checkboxes with multiple groups and a "None" checkbox and conditional // Expect conditional content to have been collapsed expect(await isVisible($conditionalPrimary)).toBe(false) }) + + describe('errors at instantiation', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('checkboxes') + }) + + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'checkboxes', { + 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/error-summary/error-summary.mjs b/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.mjs index 1d4204ae60..05e676ca56 100644 --- a/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.mjs +++ b/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.mjs @@ -1,5 +1,6 @@ import { mergeConfigs } from '../../common/index.mjs' import { normaliseDataset } from '../../common/normalise-dataset.mjs' +import { GOVUKFrontendNotSupportedError } from '../../errors/index.mjs' /** * Error summary component @@ -22,14 +23,11 @@ export class ErrorSummary { * @param {ErrorSummaryConfig} [config] - Error summary config */ constructor ($module, config) { - // Some consuming code may not be passing a module, - // for example if they initialise the component - // on their own by directly passing the result - // of `document.querySelector`. - // To avoid breaking further JavaScript initialisation - // we need to safeguard against this so things keep - // working the same now we read the elements data attributes - 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/error-summary/error-summary.test.js b/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.test.js index 6c779bd1d3..ce53d5d72c 100644 --- a/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.test.js +++ b/packages/govuk-frontend/src/govuk/components/error-summary/error-summary.test.js @@ -195,4 +195,26 @@ describe('Error Summary', () => { expect(hash).toBe('') }) }) + + describe('errors at instantiation', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('error-summary') + }) + + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'error-summary', { + 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/exit-this-page/exit-this-page.mjs b/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.mjs index 3f5a2d4c8b..a26170b6d6 100644 --- a/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.mjs +++ b/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.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' /** @@ -75,7 +76,11 @@ export class ExitThisPage { * @param {ExitThisPageConfig} [config] - Exit This Page 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/exit-this-page/exit-this-page.test.js b/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.test.js index 1a1e6ec123..8a5fc16cde 100644 --- a/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.test.js +++ b/packages/govuk-frontend/src/govuk/components/exit-this-page/exit-this-page.test.js @@ -1,4 +1,5 @@ -const { goToComponent, goToExample } = require('govuk-frontend-helpers/puppeteer') +const { goToComponent, goToExample, renderAndInitialise } = require('govuk-frontend-helpers/puppeteer') +const { getExamples } = require('govuk-frontend-lib/files.js') const buttonClass = '.govuk-js-exit-this-page-button' const skiplinkClass = '.govuk-js-exit-this-page-skiplink' @@ -164,5 +165,27 @@ describe('/components/exit-this-page', () => { expect(message).toBe('Exit this page expired.') }) }) + + describe('errors at instantiation', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('exit-this-page') + }) + + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'exit-this-page', { + 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/components/header/header.mjs b/packages/govuk-frontend/src/govuk/components/header/header.mjs index 44e1e9e319..2d0506462f 100644 --- a/packages/govuk-frontend/src/govuk/components/header/header.mjs +++ b/packages/govuk-frontend/src/govuk/components/header/header.mjs @@ -1,3 +1,5 @@ +import { GOVUKFrontendNotSupportedError } from '../../errors/index.mjs' + /** * Header component */ @@ -37,7 +39,11 @@ export class Header { * @param {Element} $module - HTML element to use for header */ constructor ($module) { - 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/header/header.test.js b/packages/govuk-frontend/src/govuk/components/header/header.test.js index 7be6cc54c7..e66bcdad62 100644 --- a/packages/govuk-frontend/src/govuk/components/header/header.test.js +++ b/packages/govuk-frontend/src/govuk/components/header/header.test.js @@ -1,4 +1,5 @@ -const { goToComponent } = require('govuk-frontend-helpers/puppeteer') +const { goToComponent, renderAndInitialise } = require('govuk-frontend-helpers/puppeteer') +const { getExamples } = require('govuk-frontend-lib/files.js') const { KnownDevices } = require('puppeteer') const iPhone = KnownDevices['iPhone 6'] @@ -152,5 +153,27 @@ describe('Header navigation', () => { expect(ariaExpanded).toBe('false') }) }) + + describe('errors at instantiation', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('header') + }) + + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'header', { + 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/notification-banner/notification-banner.mjs b/packages/govuk-frontend/src/govuk/components/notification-banner/notification-banner.mjs index 9dcfc52c35..e3a402e4c7 100644 --- a/packages/govuk-frontend/src/govuk/components/notification-banner/notification-banner.mjs +++ b/packages/govuk-frontend/src/govuk/components/notification-banner/notification-banner.mjs @@ -1,5 +1,6 @@ import { mergeConfigs } from '../../common/index.mjs' import { normaliseDataset } from '../../common/normalise-dataset.mjs' +import { GOVUKFrontendNotSupportedError } from '../../errors/index.mjs' /** * Notification Banner component @@ -19,7 +20,11 @@ export class NotificationBanner { * @param {NotificationBannerConfig} [config] - Notification banner 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/notification-banner/notification-banner.test.js b/packages/govuk-frontend/src/govuk/components/notification-banner/notification-banner.test.js index 2f677cbdf3..355b7ff9f6 100644 --- a/packages/govuk-frontend/src/govuk/components/notification-banner/notification-banner.test.js +++ b/packages/govuk-frontend/src/govuk/components/notification-banner/notification-banner.test.js @@ -1,165 +1,219 @@ const { renderAndInitialise, goToComponent } = require('govuk-frontend-helpers/puppeteer') const { getExamples } = require('govuk-frontend-lib/files') -describe('Notification banner, when type is set to "success"', () => { +describe('Notification banner', () => { let examples beforeAll(async () => { examples = await getExamples('notification-banner') }) - it('has the correct tabindex attribute to be focused with JavaScript', async () => { - await goToComponent(page, 'notification-banner', { - exampleName: 'with-type-as-success' + describe('when type is set to "success"', () => { + it('has the correct tabindex attribute to be focused with JavaScript', async () => { + await goToComponent(page, 'notification-banner', { + exampleName: 'with-type-as-success' + }) + + const tabindex = await page.$eval('.govuk-notification-banner', (el) => + el.getAttribute('tabindex') + ) + + expect(tabindex).toEqual('-1') }) - const tabindex = await page.$eval('.govuk-notification-banner', el => el.getAttribute('tabindex')) + it('is automatically focused when the page loads', async () => { + await goToComponent(page, 'notification-banner', { + exampleName: 'with-type-as-success' + }) - expect(tabindex).toEqual('-1') - }) + const activeElement = await page.evaluate(() => + document.activeElement.getAttribute('data-module') + ) - it('is automatically focused when the page loads', async () => { - await goToComponent(page, 'notification-banner', { - exampleName: 'with-type-as-success' + expect(activeElement).toBe('govuk-notification-banner') }) - const activeElement = await page.evaluate(() => document.activeElement.getAttribute('data-module')) + it('removes the tabindex attribute on blur', async () => { + await goToComponent(page, 'notification-banner', { + exampleName: 'with-type-as-success' + }) - expect(activeElement).toBe('govuk-notification-banner') - }) + await page.$eval( + '.govuk-notification-banner', + (el) => el instanceof window.HTMLElement && el.blur() + ) - it('removes the tabindex attribute on blur', async () => { - await goToComponent(page, 'notification-banner', { - exampleName: 'with-type-as-success' + const tabindex = await page.$eval('.govuk-notification-banner', (el) => + el.getAttribute('tabindex') + ) + expect(tabindex).toBeNull() }) - await page.$eval('.govuk-notification-banner', el => el instanceof window.HTMLElement && el.blur()) + describe('and auto-focus is disabled using data attributes', () => { + beforeAll(async () => { + await goToComponent(page, 'notification-banner', { + exampleName: 'auto-focus-disabled,-with-type-as-success' + }) + }) - const tabindex = await page.$eval('.govuk-notification-banner', el => el.getAttribute('tabindex')) - expect(tabindex).toBeNull() - }) + it('does not have a tabindex attribute', async () => { + const tabindex = await page.$eval('.govuk-notification-banner', (el) => + el.getAttribute('tabindex') + ) - describe('and auto-focus is disabled using data attributes', () => { - beforeAll(async () => { - await goToComponent(page, 'notification-banner', { - exampleName: 'auto-focus-disabled,-with-type-as-success' + expect(tabindex).toBeNull() }) - }) - it('does not have a tabindex attribute', async () => { - const tabindex = await page.$eval('.govuk-notification-banner', el => el.getAttribute('tabindex')) + it('does not focus the notification banner', async () => { + const activeElement = await page.evaluate(() => + document.activeElement.getAttribute('data-module') + ) - expect(tabindex).toBeNull() + expect(activeElement).not.toBe('govuk-notification-banner') + }) }) - it('does not focus the notification banner', async () => { - const activeElement = await page.evaluate(() => document.activeElement.getAttribute('data-module')) + describe('and auto-focus is disabled using JavaScript configuration', () => { + beforeAll(async () => { + await renderAndInitialise(page, 'notification-banner', { + params: examples['with type as success'], + config: { + disableAutoFocus: true + } + }) + }) - expect(activeElement).not.toBe('govuk-notification-banner') - }) - }) + it('does not have a tabindex attribute', async () => { + const tabindex = await page.$eval('.govuk-notification-banner', (el) => + el.getAttribute('tabindex') + ) - describe('and auto-focus is disabled using JavaScript configuration', () => { - beforeAll(async () => { - await renderAndInitialise(page, 'notification-banner', { - params: examples['with type as success'], - config: { - disableAutoFocus: true - } + expect(tabindex).toBeNull() }) - }) - it('does not have a tabindex attribute', async () => { - const tabindex = await page.$eval('.govuk-notification-banner', el => el.getAttribute('tabindex')) + it('does not focus the notification banner', async () => { + const activeElement = await page.evaluate(() => + document.activeElement.getAttribute('data-module') + ) - expect(tabindex).toBeNull() + expect(activeElement).not.toBe('govuk-notification-banner') + }) }) - it('does not focus the notification banner', async () => { - const activeElement = await page.evaluate(() => document.activeElement.getAttribute('data-module')) + describe('and auto-focus is disabled using options passed to initAll', () => { + beforeAll(async () => { + await renderAndInitialise(page, 'notification-banner', { + params: examples['with type as success'], + config: { + disableAutoFocus: true + } + }) + }) - expect(activeElement).not.toBe('govuk-notification-banner') - }) - }) + it('does not have a tabindex attribute', async () => { + const tabindex = await page.$eval('.govuk-notification-banner', (el) => + el.getAttribute('tabindex') + ) - describe('and auto-focus is disabled using options passed to initAll', () => { - beforeAll(async () => { - await renderAndInitialise(page, 'notification-banner', { - params: examples['with type as success'], - config: { - disableAutoFocus: true - } + expect(tabindex).toBeNull() }) - }) - it('does not have a tabindex attribute', async () => { - const tabindex = await page.$eval('.govuk-notification-banner', el => el.getAttribute('tabindex')) + it('does not focus the notification banner', async () => { + const activeElement = await page.evaluate(() => + document.activeElement.getAttribute('data-module') + ) - expect(tabindex).toBeNull() + expect(activeElement).not.toBe('govuk-notification-banner') + }) }) - it('does not focus the notification banner', async () => { - const activeElement = await page.evaluate(() => document.activeElement.getAttribute('data-module')) + describe('and autofocus is disabled in JS but enabled in data attributes', () => { + beforeAll(async () => { + await renderAndInitialise(page, 'notification-banner', { + params: + examples['auto-focus explicitly enabled, with type as success'], + config: { + disableAutoFocus: true + } + }) + }) - expect(activeElement).not.toBe('govuk-notification-banner') - }) - }) + it('has the correct tabindex attribute to be focused with JavaScript', async () => { + const tabindex = await page.$eval('.govuk-notification-banner', (el) => + el.getAttribute('tabindex') + ) - describe('and autofocus is disabled in JS but enabled in data attributes', () => { - beforeAll(async () => { - await renderAndInitialise(page, 'notification-banner', { - params: examples['auto-focus explicitly enabled, with type as success'], - config: { - disableAutoFocus: true - } + expect(tabindex).toEqual('-1') }) - }) - it('has the correct tabindex attribute to be focused with JavaScript', async () => { - const tabindex = await page.$eval('.govuk-notification-banner', el => el.getAttribute('tabindex')) + it('is automatically focused when the page loads', async () => { + const activeElement = await page.evaluate(() => + document.activeElement.getAttribute('data-module') + ) - expect(tabindex).toEqual('-1') + expect(activeElement).toBe('govuk-notification-banner') + }) }) - it('is automatically focused when the page loads', async () => { - const activeElement = await page.evaluate(() => document.activeElement.getAttribute('data-module')) + describe('and role is overridden to "region"', () => { + beforeAll(async () => { + await goToComponent(page, 'notification-banner', { + exampleName: + 'role=alert-overridden-to-role=region,-with-type-as-success' + }) + }) - expect(activeElement).toBe('govuk-notification-banner') - }) - }) + it('does not have a tabindex attribute', async () => { + const tabindex = await page.$eval('.govuk-notification-banner', (el) => + el.getAttribute('tabindex') + ) - describe('and role is overridden to "region"', () => { - beforeAll(async () => { - await goToComponent(page, 'notification-banner', { - exampleName: 'role=alert-overridden-to-role=region,-with-type-as-success' + expect(tabindex).toBeNull() }) - }) - it('does not have a tabindex attribute', async () => { - const tabindex = await page.$eval('.govuk-notification-banner', el => el.getAttribute('tabindex')) + it('does not focus the notification banner', async () => { + const activeElement = await page.evaluate(() => + document.activeElement.getAttribute('data-module') + ) - expect(tabindex).toBeNull() + expect(activeElement).not.toBe('govuk-notification-banner') + }) }) - it('does not focus the notification banner', async () => { - const activeElement = await page.evaluate(() => document.activeElement.getAttribute('data-module')) + describe('and a custom tabindex is set', () => { + beforeAll(async () => { + await goToComponent(page, 'notification-banner', { + exampleName: 'custom-tabindex' + }) + }) - expect(activeElement).not.toBe('govuk-notification-banner') - }) - }) + it('does not remove the tabindex attribute on blur', async () => { + await page.$eval( + '.govuk-notification-banner', + (el) => el instanceof window.HTMLElement && el.blur() + ) - describe('and a custom tabindex is set', () => { - beforeAll(async () => { - await goToComponent(page, 'notification-banner', { - exampleName: 'custom-tabindex' + const tabindex = await page.$eval('.govuk-notification-banner', (el) => + el.getAttribute('tabindex') + ) + expect(tabindex).toEqual('2') }) }) + }) - it('does not remove the tabindex attribute on blur', async () => { - await page.$eval('.govuk-notification-banner', el => el instanceof window.HTMLElement && el.blur()) - - const tabindex = await page.$eval('.govuk-notification-banner', el => el.getAttribute('tabindex')) - expect(tabindex).toEqual('2') + describe('errors at instantiation', () => { + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'notification-banner', { + 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/radios/radios.mjs b/packages/govuk-frontend/src/govuk/components/radios/radios.mjs index 360374e035..8a23bba5d2 100644 --- a/packages/govuk-frontend/src/govuk/components/radios/radios.mjs +++ b/packages/govuk-frontend/src/govuk/components/radios/radios.mjs @@ -1,3 +1,5 @@ +import { GOVUKFrontendNotSupportedError } from '../../errors/index.mjs' + /** * Radios component */ @@ -23,7 +25,11 @@ export class Radios { * @param {Element} $module - HTML element to use for radios */ constructor ($module) { - 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/radios/radios.test.js b/packages/govuk-frontend/src/govuk/components/radios/radios.test.js index 8e32c7a0aa..9c97ef430f 100644 --- a/packages/govuk-frontend/src/govuk/components/radios/radios.test.js +++ b/packages/govuk-frontend/src/govuk/components/radios/radios.test.js @@ -1,236 +1,261 @@ -const { goToComponent, goToExample, getProperty, getAttribute, isVisible } = require('govuk-frontend-helpers/puppeteer') +const { goToComponent, goToExample, getProperty, getAttribute, isVisible, renderAndInitialise } = require('govuk-frontend-helpers/puppeteer') +const { getExamples } = require('govuk-frontend-lib/files.js') -describe('Radios with conditional reveals', () => { - describe('when JavaScript is unavailable or fails', () => { - beforeAll(async () => { - await page.setJavaScriptEnabled(false) - }) +describe('Radios', () => { + describe('with conditional reveals', () => { + describe('when JavaScript is unavailable or fails', () => { + beforeAll(async () => { + await page.setJavaScriptEnabled(false) + }) - afterAll(async () => { - await page.setJavaScriptEnabled(true) - }) + afterAll(async () => { + await page.setJavaScriptEnabled(true) + }) - describe('with conditional items', () => { - let $component - let $inputs - let $conditionals + describe('with conditional items', () => { + let $component + let $inputs + let $conditionals - beforeAll(async () => { - await goToComponent(page, 'radios', { - exampleName: 'with-conditional-items' - }) + beforeAll(async () => { + await goToComponent(page, 'radios', { + exampleName: 'with-conditional-items' + }) - $component = await page.$('.govuk-radios') - $inputs = await $component.$$('.govuk-radios__input') - $conditionals = await $component.$$('.govuk-radios__conditional') + $component = await page.$('.govuk-radios') + $inputs = await $component.$$('.govuk-radios__input') + $conditionals = await $component.$$('.govuk-radios__conditional') - expect($inputs.length).toBe(3) - expect($conditionals.length).toBe(3) - }) + expect($inputs.length).toBe(3) + expect($conditionals.length).toBe(3) + }) - it('has no ARIA attributes applied', async () => { - const $inputsWithAriaExpanded = await $component.$$('.govuk-radios__input[aria-expanded]') - const $inputsWithAriaControls = await $component.$$('.govuk-radios__input[aria-controls]') + it('has no ARIA attributes applied', async () => { + const $inputsWithAriaExpanded = await $component.$$('.govuk-radios__input[aria-expanded]') + const $inputsWithAriaControls = await $component.$$('.govuk-radios__input[aria-controls]') - expect($inputsWithAriaExpanded.length).toBe(0) - expect($inputsWithAriaControls.length).toBe(0) - }) + expect($inputsWithAriaExpanded.length).toBe(0) + expect($inputsWithAriaControls.length).toBe(0) + }) - it('falls back to making all conditional content visible', async () => { - return Promise.all($conditionals.map(async ($conditional) => { - return expect(await isVisible($conditional)).toBe(true) - })) + it('falls back to making all conditional content visible', async () => { + return Promise.all($conditionals.map(async ($conditional) => { + return expect(await isVisible($conditional)).toBe(true) + })) + }) }) }) - }) - describe('when JavaScript is available', () => { - describe('with conditional item checked', () => { - let $component - let $inputs + describe('when JavaScript is available', () => { + describe('with conditional item checked', () => { + let $component + let $inputs - beforeEach(async () => { - await goToComponent(page, 'radios', { - exampleName: 'with-conditional-item-checked' - }) + beforeEach(async () => { + await goToComponent(page, 'radios', { + exampleName: 'with-conditional-item-checked' + }) - $component = await page.$('.govuk-radios') - $inputs = await $component.$$('.govuk-radios__input') - }) + $component = await page.$('.govuk-radios') + $inputs = await $component.$$('.govuk-radios__input') + }) - it('has conditional content revealed that is associated with a checked input', async () => { - const $input = $inputs[0] // First input, checked - const $conditional = await $component.$(`[id="${await getAttribute($input, 'aria-controls')}"]`) + it('has conditional content revealed that is associated with a checked input', async () => { + const $input = $inputs[0] // First input, checked + const $conditional = await $component.$(`[id="${await getAttribute($input, 'aria-controls')}"]`) - expect(await getProperty($input, 'checked')).toBe(true) - expect(await isVisible($conditional)).toBe(true) - }) + expect(await getProperty($input, 'checked')).toBe(true) + expect(await isVisible($conditional)).toBe(true) + }) - it('has no conditional content revealed that is associated with an unchecked input', async () => { - const $input = $inputs[$inputs.length - 1] // Last input, unchecked - const $conditional = await $component.$(`[id="${await getAttribute($input, 'aria-controls')}"]`) + it('has no conditional content revealed that is associated with an unchecked input', async () => { + const $input = $inputs[$inputs.length - 1] // Last input, unchecked + const $conditional = await $component.$(`[id="${await getAttribute($input, 'aria-controls')}"]`) - expect(await getProperty($input, 'checked')).toBe(false) - expect(await isVisible($conditional)).toBe(false) + expect(await getProperty($input, 'checked')).toBe(false) + expect(await isVisible($conditional)).toBe(false) + }) }) - }) - describe('with conditional items', () => { - let $component - let $inputs + describe('with conditional items', () => { + let $component + let $inputs - beforeEach(async () => { - await goToComponent(page, 'radios', { - exampleName: 'with-conditional-items' - }) + beforeEach(async () => { + await goToComponent(page, 'radios', { + exampleName: 'with-conditional-items' + }) - $component = await page.$('.govuk-radios') - $inputs = await $component.$$('.govuk-radios__input') - }) + $component = await page.$('.govuk-radios') + $inputs = await $component.$$('.govuk-radios__input') + }) - it('indicates when conditional content is collapsed or revealed', async () => { - const $input = $inputs[0] // First input, with conditional content + it('indicates when conditional content is collapsed or revealed', async () => { + const $input = $inputs[0] // First input, with conditional content - // Initially collapsed - expect(await getProperty($input, 'checked')).toBe(false) - expect(await getAttribute($input, 'aria-expanded')).toBe('false') + // Initially collapsed + expect(await getProperty($input, 'checked')).toBe(false) + expect(await getAttribute($input, 'aria-expanded')).toBe('false') - // Toggle revealed - await $input.click() + // Toggle revealed + await $input.click() - expect(await getProperty($input, 'checked')).toBe(true) - expect(await getAttribute($input, 'aria-expanded')).toBe('true') + expect(await getProperty($input, 'checked')).toBe(true) + expect(await getAttribute($input, 'aria-expanded')).toBe('true') - // Stays revealed (unlike radios) - await $input.click() + // Stays revealed (unlike radios) + await $input.click() - expect(await getProperty($input, 'checked')).toBe(true) - expect(await getAttribute($input, 'aria-expanded')).toBe('true') - }) + expect(await getProperty($input, 'checked')).toBe(true) + expect(await getAttribute($input, 'aria-expanded')).toBe('true') + }) - it('toggles the conditional content when clicking an input', async () => { - const $input = $inputs[0] // First input, with conditional content - const $conditional = await $component.$(`[id="${await getAttribute($input, 'aria-controls')}"]`) + it('toggles the conditional content when clicking an input', async () => { + const $input = $inputs[0] // First input, with conditional content + const $conditional = await $component.$(`[id="${await getAttribute($input, 'aria-controls')}"]`) - // Initially collapsed - expect(await getProperty($input, 'checked')).toBe(false) - expect(await isVisible($conditional)).toBe(false) + // Initially collapsed + expect(await getProperty($input, 'checked')).toBe(false) + expect(await isVisible($conditional)).toBe(false) - // Toggle revealed - await $input.click() + // Toggle revealed + await $input.click() - expect(await getProperty($input, 'checked')).toBe(true) - expect(await isVisible($conditional)).toBe(true) + expect(await getProperty($input, 'checked')).toBe(true) + expect(await isVisible($conditional)).toBe(true) - // Stays revealed (unlike radios) - await $input.click() + // Stays revealed (unlike radios) + await $input.click() - expect(await getProperty($input, 'checked')).toBe(true) - expect(await isVisible($conditional)).toBe(true) - }) + expect(await getProperty($input, 'checked')).toBe(true) + expect(await isVisible($conditional)).toBe(true) + }) - it('toggles the conditional content when using an input with a keyboard', async () => { - const $input = $inputs[0] // First input, with conditional content - const $conditional = await $component.$(`[id="${await getAttribute($input, 'aria-controls')}"]`) + it('toggles the conditional content when using an input with a keyboard', async () => { + const $input = $inputs[0] // First input, with conditional content + const $conditional = await $component.$(`[id="${await getAttribute($input, 'aria-controls')}"]`) - // Initially collapsed - expect(await getProperty($input, 'checked')).toBe(false) - expect(await isVisible($conditional)).toBe(false) + // Initially collapsed + expect(await getProperty($input, 'checked')).toBe(false) + expect(await isVisible($conditional)).toBe(false) - // Toggle revealed - await $input.focus() - await page.keyboard.press('Space') + // Toggle revealed + await $input.focus() + await page.keyboard.press('Space') - expect(await getProperty($input, 'checked')).toBe(true) - expect(await isVisible($conditional)).toBe(true) + expect(await getProperty($input, 'checked')).toBe(true) + expect(await isVisible($conditional)).toBe(true) - // Stays revealed (unlike radios) - await page.keyboard.press('Space') + // Stays revealed (unlike radios) + await page.keyboard.press('Space') - expect(await getProperty($input, 'checked')).toBe(true) - expect(await isVisible($conditional)).toBe(true) + expect(await getProperty($input, 'checked')).toBe(true) + expect(await isVisible($conditional)).toBe(true) + }) }) - }) - describe('with conditional items with special characters', () => { - it('does not error when ID of revealed content contains special characters', async () => { - // Errors logged to the console will cause this test to fail - await goToComponent(page, 'radios', { - exampleName: 'with-conditional-items-with-special-characters' + describe('with conditional items with special characters', () => { + it('does not error when ID of revealed content contains special characters', async () => { + // Errors logged to the console will cause this test to fail + await goToComponent(page, 'radios', { + exampleName: 'with-conditional-items-with-special-characters' + }) }) }) }) }) -}) -describe('Radios with multiple groups', () => { - describe('when JavaScript is available', () => { - let $inputsWarm - let $inputsCool - let $inputsNotInForm + describe('with multiple groups', () => { + describe('when JavaScript is available', () => { + let $inputsWarm + let $inputsCool + let $inputsNotInForm - beforeEach(async () => { - await goToExample(page, 'multiple-radio-groups') + beforeEach(async () => { + await goToExample(page, 'multiple-radio-groups') - $inputsWarm = await page.$$('.govuk-radios__input[id^="warm"]') - $inputsCool = await page.$$('.govuk-radios__input[id^="cool"]') - $inputsNotInForm = await page.$$('.govuk-radios__input[id^="question-not-in-form"]') - }) + $inputsWarm = await page.$$('.govuk-radios__input[id^="warm"]') + $inputsCool = await page.$$('.govuk-radios__input[id^="cool"]') + $inputsNotInForm = await page.$$('.govuk-radios__input[id^="question-not-in-form"]') + }) - it('toggles conditional reveals in other groups', async () => { - const $conditionalWarm = await page.$(`[id="${await getAttribute($inputsWarm[0], 'aria-controls')}"]`) - const $conditionalCool = await page.$(`[id="${await getAttribute($inputsCool[0], 'aria-controls')}"]`) + it('toggles conditional reveals in other groups', async () => { + const $conditionalWarm = await page.$(`[id="${await getAttribute($inputsWarm[0], 'aria-controls')}"]`) + const $conditionalCool = await page.$(`[id="${await getAttribute($inputsCool[0], 'aria-controls')}"]`) - // Select red in warm colours - await $inputsWarm[0].click() + // Select red in warm colours + await $inputsWarm[0].click() - expect(await isVisible($conditionalWarm)).toBe(true) - expect(await isVisible($conditionalCool)).toBe(false) + expect(await isVisible($conditionalWarm)).toBe(true) + expect(await isVisible($conditionalCool)).toBe(false) - // Select blue in cool colours - await $inputsCool[0].click() + // Select blue in cool colours + await $inputsCool[0].click() - expect(await isVisible($conditionalWarm)).toBe(false) - expect(await isVisible($conditionalCool)).toBe(true) - }) + expect(await isVisible($conditionalWarm)).toBe(false) + expect(await isVisible($conditionalCool)).toBe(true) + }) - it('toggles conditional reveals when not in a form', async () => { - const $conditionalWarm = await page.$(`[id="${await getAttribute($inputsWarm[0], 'aria-controls')}"]`) + it('toggles conditional reveals when not in a form', async () => { + const $conditionalWarm = await page.$(`[id="${await getAttribute($inputsWarm[0], 'aria-controls')}"]`) - // Select first input in radios not in a form - await $inputsNotInForm[0].click() + // Select first input in radios not in a form + await $inputsNotInForm[0].click() - expect(await isVisible($conditionalWarm)).toBe(false) + expect(await isVisible($conditionalWarm)).toBe(false) + }) }) }) -}) -describe('Radios with multiple groups and conditional reveals', () => { - describe('when JavaScript is available', () => { - let $inputsPrimary - let $inputsOther + describe('with multiple groups and conditional reveals', () => { + describe('when JavaScript is available', () => { + let $inputsPrimary + let $inputsOther - beforeEach(async () => { - await goToExample(page, 'conditional-reveals') + beforeEach(async () => { + await goToExample(page, 'conditional-reveals') - $inputsPrimary = await page.$$('.govuk-radios__input[id^="fave-primary"]') - $inputsOther = await page.$$('.govuk-radios__input[id^="fave-other"]') - }) + $inputsPrimary = await page.$$('.govuk-radios__input[id^="fave-primary"]') + $inputsOther = await page.$$('.govuk-radios__input[id^="fave-other"]') + }) + + it('hides conditional reveals in other groups', async () => { + const $conditionalPrimary = await page.$(`[id="${await getAttribute($inputsPrimary[1], 'aria-controls')}"]`) - it('hides conditional reveals in other groups', async () => { - const $conditionalPrimary = await page.$(`[id="${await getAttribute($inputsPrimary[1], 'aria-controls')}"]`) + // Choose the second radio in the first group, which reveals additional content + await $inputsPrimary[1].click() - // Choose the second radio in the first group, which reveals additional content - await $inputsPrimary[1].click() + // Assert that conditional content is revealed + expect(await isVisible($conditionalPrimary)).toBe(true) - // Assert that conditional content is revealed - expect(await isVisible($conditionalPrimary)).toBe(true) + // Choose a different radio with the same name, but in a different group + await $inputsOther[1].click() - // Choose a different radio with the same name, but in a different group - await $inputsOther[1].click() + // Expect conditional content to have been collapsed + expect(await isVisible($conditionalPrimary)).toBe(false) + }) + }) + }) - // Expect conditional content to have been collapsed - expect(await isVisible($conditionalPrimary)).toBe(false) + describe('errors at instantiation', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('radios') + }) + + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'radios', { + 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/skip-link/skip-link.mjs b/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs index 91b549fa46..91f8113e71 100644 --- a/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs +++ b/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs @@ -1,3 +1,5 @@ +import { GOVUKFrontendNotSupportedError } from '../../errors/index.mjs' + /** * Skip link component */ @@ -19,7 +21,11 @@ export class SkipLink { * @param {Element} $module - HTML element to use for skip link */ constructor ($module) { - if (!($module instanceof HTMLAnchorElement) || !document.body.classList.contains('govuk-frontend-supported')) { + if (!document.body.classList.contains('govuk-frontend-supported')) { + throw new GOVUKFrontendNotSupportedError() + } + + if (!($module instanceof HTMLAnchorElement)) { return this } diff --git a/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.test.js b/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.test.js index 9d8b70cec9..d067941175 100644 --- a/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.test.js +++ b/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.test.js @@ -1,7 +1,8 @@ -const { goToExample } = require('govuk-frontend-helpers/puppeteer') +const { goToExample, renderAndInitialise } = require('govuk-frontend-helpers/puppeteer') +const { getExamples } = require('govuk-frontend-lib/files.js') -describe('/examples/template-default', () => { - describe('skip link', () => { +describe('Skip Link', () => { + describe('/examples/template-default', () => { beforeAll(async () => { await goToExample(page, 'template-default') await page.keyboard.press('Tab') @@ -42,4 +43,26 @@ describe('/examples/template-default', () => { expect(cssClass).not.toContain('govuk-skip-link-focused-element') }) }) + + describe('errors at instantiation', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('skip-link') + }) + + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'skip-link', { + 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/tabs/tabs.mjs b/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs index f25d2e02ba..2488cf212c 100644 --- a/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs +++ b/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs @@ -1,3 +1,5 @@ +import { GOVUKFrontendNotSupportedError } from '../../errors/index.mjs' + /** * Tabs component */ @@ -36,7 +38,11 @@ export class Tabs { * @param {Element} $module - HTML element to use for tabs */ constructor ($module) { - 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/tabs/tabs.test.js b/packages/govuk-frontend/src/govuk/components/tabs/tabs.test.js index 7d4467bd10..465cd4ddfe 100644 --- a/packages/govuk-frontend/src/govuk/components/tabs/tabs.test.js +++ b/packages/govuk-frontend/src/govuk/components/tabs/tabs.test.js @@ -1,4 +1,5 @@ -const { goTo, goToComponent } = require('govuk-frontend-helpers/puppeteer') +const { goTo, goToComponent, renderAndInitialise } = require('govuk-frontend-helpers/puppeteer') +const { getExamples } = require('govuk-frontend-lib/files.js') const { KnownDevices } = require('puppeteer') const iPhone = KnownDevices['iPhone 6'] @@ -160,5 +161,27 @@ describe('/components/tabs', () => { expect(isContentVisible).toBeTruthy() }) }) + + describe('errors at instantiation', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('tabs') + }) + + it('throws when GOV.UK Frontend is not supported', async () => { + await expect( + renderAndInitialise(page, 'tabs', { + 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/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' + ) + }) + }) +})