From 4376e046ea137b15a07025279cbc6946ea292cc0 Mon Sep 17 00:00:00 2001 From: Romaric Pascal Date: Fri, 18 Aug 2023 13:14:51 +0100 Subject: [PATCH] Implement check for type of `$module` request-checks: true --- .../govuk/components/accordion/accordion.mjs | 10 +---- .../components/accordion/accordion.test.js | 16 ++++++++ .../src/govuk/components/button/button.mjs | 10 +---- .../govuk/components/button/button.test.js | 37 ++++++++---------- .../character-count/character-count.mjs | 9 +---- .../character-count/character-count.test.js | 16 ++++++++ .../components/checkboxes/checkboxes.mjs | 7 +--- .../components/checkboxes/checkboxes.test.js | 16 ++++++++ .../error-summary/error-summary.mjs | 10 +---- .../error-summary/error-summary.test.js | 39 ++++++++----------- .../exit-this-page/exit-this-page.mjs | 9 +---- .../exit-this-page/exit-this-page.test.js | 16 ++++++++ .../src/govuk/components/header/header.mjs | 7 +--- .../govuk/components/header/header.test.js | 16 ++++++++ .../notification-banner.mjs | 10 +---- .../notification-banner.test.js | 16 ++++++++ .../src/govuk/components/radios/radios.mjs | 7 +--- .../govuk/components/radios/radios.test.js | 16 ++++++++ .../govuk/components/skip-link/skip-link.mjs | 23 +++++++---- .../components/skip-link/skip-link.test.js | 16 ++++++++ .../src/govuk/components/tabs/tabs.mjs | 7 +--- .../src/govuk/components/tabs/tabs.test.js | 16 ++++++++ .../src/govuk/govuk-frontend-component.mjs | 30 +++++++++++++- 23 files changed, 237 insertions(+), 122 deletions(-) diff --git a/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs b/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs index f4b5ea0f3b..cbed110704 100644 --- a/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs +++ b/packages/govuk-frontend/src/govuk/components/accordion/accordion.mjs @@ -113,18 +113,12 @@ export class Accordion extends GOVUKFrontendComponent { * @param {AccordionConfig} [config] - Accordion config */ constructor($module, config) { - super() - - if (!($module instanceof HTMLElement)) { - return this - } - - this.$module = $module + super($module) this.config = mergeConfigs( Accordion.defaults, config || {}, - normaliseDataset($module.dataset) + normaliseDataset(this.$module.dataset) ) this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n')) 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 99edf6c5d6..65a25f5e49 100644 --- a/packages/govuk-frontend/src/govuk/components/accordion/accordion.test.js +++ b/packages/govuk-frontend/src/govuk/components/accordion/accordion.test.js @@ -733,6 +733,22 @@ describe('/components/accordion', () => { message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'accordion', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLElement`' + }) + }) }) }) }) diff --git a/packages/govuk-frontend/src/govuk/components/button/button.mjs b/packages/govuk-frontend/src/govuk/components/button/button.mjs index cfe35eab62..b91193b664 100644 --- a/packages/govuk-frontend/src/govuk/components/button/button.mjs +++ b/packages/govuk-frontend/src/govuk/components/button/button.mjs @@ -28,18 +28,12 @@ export class Button extends GOVUKFrontendComponent { * @param {ButtonConfig} [config] - Button config */ constructor($module, config) { - super() - - if (!($module instanceof HTMLElement)) { - return this - } - - this.$module = $module + super($module) this.config = mergeConfigs( Button.defaults, config || {}, - normaliseDataset($module.dataset) + normaliseDataset(this.$module.dataset) ) this.$module.addEventListener('keydown', (event) => 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 5f51025d22..ea7a461492 100644 --- a/packages/govuk-frontend/src/govuk/components/button/button.test.js +++ b/packages/govuk-frontend/src/govuk/components/button/button.test.js @@ -1,5 +1,4 @@ const { - goTo, goToComponent, renderAndInitialise } = require('@govuk-frontend/helpers/puppeteer') @@ -18,26 +17,6 @@ describe('/components/button', () => { examples = await getExamples('button') }) - describe('mis-instantiation', () => { - it('does not prevent further JavaScript from running', async () => { - await goTo(page, '/tests/boilerplate') - - const result = await page.evaluate(async (exportName) => { - const namespace = await import('govuk-frontend') - - // `undefined` simulates the element being missing, - // from an unchecked `document.querySelector` for example - /* eslint-disable-next-line no-new */ - new namespace[exportName](undefined) - - // If our component initialisation breaks, this won't run - return true - }, 'Button') - - expect(result).toBe(true) - }) - }) - describe('/components/button/link', () => { beforeAll(async () => { await goToComponent(page, 'button', { @@ -359,6 +338,22 @@ describe('/components/button', () => { message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'button', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLElement`' + }) + }) }) }) }) 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 067cf59291..31f1f1867d 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 @@ -64,11 +64,7 @@ export class CharacterCount extends GOVUKFrontendComponent { * @param {CharacterCountConfig} [config] - Character count config */ constructor($module, config) { - super() - - if (!($module instanceof HTMLElement)) { - return this - } + super($module) const $textarea = $module.querySelector('.govuk-js-character-count') if ( @@ -81,7 +77,7 @@ export class CharacterCount extends GOVUKFrontendComponent { } // Read config set using dataset ('data-' values) - const datasetConfig = normaliseDataset($module.dataset) + const datasetConfig = normaliseDataset(this.$module.dataset) // To ensure data-attributes take complete precedence, even if they change the // type of count, we need to reset the `maxlength` and `maxwords` from the @@ -119,7 +115,6 @@ export class CharacterCount extends GOVUKFrontendComponent { return this } - this.$module = $module this.$textarea = $textarea const $textareaDescription = document.getElementById( 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 927ae7dfe4..81ae57ba23 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 @@ -784,6 +784,22 @@ describe('Character count', () => { message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'character-count', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLElement`' + }) + }) }) }) diff --git a/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs b/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs index 8d9d93fffc..33b3042a32 100644 --- a/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs +++ b/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.mjs @@ -24,11 +24,7 @@ export class Checkboxes extends GOVUKFrontendComponent { * @param {Element} $module - HTML element to use for checkboxes */ constructor($module) { - super() - - if (!($module instanceof HTMLElement)) { - return this - } + super($module) /** @satisfies {NodeListOf} */ const $inputs = $module.querySelectorAll('input[type="checkbox"]') @@ -36,7 +32,6 @@ export class Checkboxes extends GOVUKFrontendComponent { return this } - this.$module = $module this.$inputs = $inputs this.$inputs.forEach(($input) => { 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 6876358cd8..c03f6a5468 100644 --- a/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.test.js +++ b/packages/govuk-frontend/src/govuk/components/checkboxes/checkboxes.test.js @@ -339,6 +339,22 @@ describe('Checkboxes with multiple groups and a "None" checkbox and conditional message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'checkboxes', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLElement`' + }) + }) }) }) }) 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 a180bedc20..7dd05b2645 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 @@ -21,18 +21,12 @@ export class ErrorSummary extends GOVUKFrontendComponent { * @param {ErrorSummaryConfig} [config] - Error summary config */ constructor($module, config) { - super() - - if (!($module instanceof HTMLElement)) { - return this - } - - this.$module = $module + super($module) this.config = mergeConfigs( ErrorSummary.defaults, config || {}, - normaliseDataset($module.dataset) + normaliseDataset(this.$module.dataset) ) this.setFocus() 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 96fde8d9ae..05eab1e6a9 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 @@ -1,8 +1,7 @@ const { goToComponent, goToExample, - renderAndInitialise, - goTo + renderAndInitialise } = require('@govuk-frontend/helpers/puppeteer') const { getExamples } = require('@govuk-frontend/lib/components') @@ -94,26 +93,6 @@ describe('Error Summary', () => { }) }) - describe('using JavaScript configuration, with no elements on the page', () => { - it('does not prevent further JavaScript from running', async () => { - await goTo(page, '/tests/boilerplate') - - const result = await page.evaluate(async (exportName) => { - const namespace = await import('govuk-frontend') - - // `undefined` simulates the element being missing, - // from an unchecked `document.querySelector` for example - /* eslint-disable-next-line no-new */ - new namespace[exportName](undefined) - - // If our component initialisation breaks, this won't run - return true - }, 'ErrorSummary') - - expect(result).toBe(true) - }) - }) - describe('using JavaScript configuration, but enabled via data-attributes', () => { beforeAll(async () => { await renderAndInitialise(page, 'error-summary', { @@ -251,5 +230,21 @@ describe('Error Summary', () => { message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'error-summary', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLElement`' + }) + }) }) }) 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 2ab98f6fe0..9a0371ea56 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 @@ -75,11 +75,7 @@ export class ExitThisPage extends GOVUKFrontendComponent { * @param {ExitThisPageConfig} [config] - Exit This Page config */ constructor($module, config) { - super() - - if (!($module instanceof HTMLElement)) { - return this - } + super($module) const $button = $module.querySelector('.govuk-exit-this-page__button') if (!($button instanceof HTMLElement)) { @@ -89,11 +85,10 @@ export class ExitThisPage extends GOVUKFrontendComponent { this.config = mergeConfigs( ExitThisPage.defaults, config || {}, - normaliseDataset($module.dataset) + normaliseDataset(this.$module.dataset) ) this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n')) - this.$module = $module this.$button = $button const $skiplinkButton = document.querySelector( 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 e903b7f237..9a26a7b674 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 @@ -208,6 +208,22 @@ describe('/components/exit-this-page', () => { message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'exit-this-page', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLElement`' + }) + }) }) }) }) diff --git a/packages/govuk-frontend/src/govuk/components/header/header.mjs b/packages/govuk-frontend/src/govuk/components/header/header.mjs index 1cc9582b67..f33b64f6d0 100644 --- a/packages/govuk-frontend/src/govuk/components/header/header.mjs +++ b/packages/govuk-frontend/src/govuk/components/header/header.mjs @@ -38,13 +38,8 @@ export class Header extends GOVUKFrontendComponent { * @param {Element} $module - HTML element to use for header */ constructor($module) { - super() + super($module) - if (!($module instanceof HTMLElement)) { - return this - } - - this.$module = $module this.$menuButton = $module.querySelector('.govuk-js-header-toggle') this.$menu = this.$menuButton && 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 a5252e6d21..8a98ef1acc 100644 --- a/packages/govuk-frontend/src/govuk/components/header/header.test.js +++ b/packages/govuk-frontend/src/govuk/components/header/header.test.js @@ -188,6 +188,22 @@ describe('Header navigation', () => { message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'header', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLElement`' + }) + }) }) }) }) 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 64eb37b4b4..fad03025e0 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 @@ -19,18 +19,12 @@ export class NotificationBanner extends GOVUKFrontendComponent { * @param {NotificationBannerConfig} [config] - Notification banner config */ constructor($module, config) { - super() - - if (!($module instanceof HTMLElement)) { - return this - } - - this.$module = $module + super($module) this.config = mergeConfigs( NotificationBanner.defaults, config || {}, - normaliseDataset($module.dataset) + normaliseDataset(this.$module.dataset) ) this.setFocus() 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 cd5e04a578..ee7a0a6c8b 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 @@ -218,5 +218,21 @@ describe('Notification banner', () => { message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'notification-banner', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLElement`' + }) + }) }) }) diff --git a/packages/govuk-frontend/src/govuk/components/radios/radios.mjs b/packages/govuk-frontend/src/govuk/components/radios/radios.mjs index ca77106b81..dbc1f688ff 100644 --- a/packages/govuk-frontend/src/govuk/components/radios/radios.mjs +++ b/packages/govuk-frontend/src/govuk/components/radios/radios.mjs @@ -24,11 +24,7 @@ export class Radios extends GOVUKFrontendComponent { * @param {Element} $module - HTML element to use for radios */ constructor($module) { - super() - - if (!($module instanceof HTMLElement)) { - return this - } + super($module) /** @satisfies {NodeListOf} */ const $inputs = $module.querySelectorAll('input[type="radio"]') @@ -36,7 +32,6 @@ export class Radios extends GOVUKFrontendComponent { return this } - this.$module = $module this.$inputs = $inputs this.$inputs.forEach(($input) => { 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 6b29740cd3..daf3ca8766 100644 --- a/packages/govuk-frontend/src/govuk/components/radios/radios.test.js +++ b/packages/govuk-frontend/src/govuk/components/radios/radios.test.js @@ -290,5 +290,21 @@ describe('Radios', () => { message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'radios', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLElement`' + }) + }) }) }) 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 1aff30f340..0392f35fe8 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 @@ -7,7 +7,7 @@ import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs' */ export class SkipLink extends GOVUKFrontendComponent { /** - * @protected + * @override * @type {HTMLAnchorElement} */ $module = this.$module @@ -25,13 +25,7 @@ export class SkipLink extends GOVUKFrontendComponent { * @param {Element} $module - HTML element to use for skip link */ constructor($module) { - super() - - if (!($module instanceof HTMLAnchorElement)) { - return this - } - - this.$module = $module + super($module) // Check for linked element const $linkedElement = this.getLinkedElement() @@ -114,6 +108,19 @@ export class SkipLink extends GOVUKFrontendComponent { return this.$module.hash.split('#').pop() } + /** + * @override + */ + checkModuleType($module) { + if (!($module instanceof HTMLAnchorElement)) { + throw new TypeError( + 'Expected `$module` to be an instance of `HTMLAnchorElement`' + ) + } + + return $module + } + /** * Name for the component used when initialising using data-module attributes. */ 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 ad065b1a65..021c09a9a7 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 @@ -81,5 +81,21 @@ describe('Skip Link', () => { message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'skip-link', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLAnchorElement`' + }) + }) }) }) diff --git a/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs b/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs index a44284a654..3ffae1db60 100644 --- a/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs +++ b/packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs @@ -37,11 +37,7 @@ export class Tabs extends GOVUKFrontendComponent { * @param {Element} $module - HTML element to use for tabs */ constructor($module) { - super() - - if (!($module instanceof HTMLElement)) { - return this - } + super($module) /** @satisfies {NodeListOf} */ const $tabs = $module.querySelectorAll('a.govuk-tabs__tab') @@ -49,7 +45,6 @@ export class Tabs extends GOVUKFrontendComponent { return this } - this.$module = $module this.$tabs = $tabs // Save bounded functions to use when removing event listeners during teardown 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 bd96371f7c..9a78695972 100644 --- a/packages/govuk-frontend/src/govuk/components/tabs/tabs.test.js +++ b/packages/govuk-frontend/src/govuk/components/tabs/tabs.test.js @@ -268,6 +268,22 @@ describe('/components/tabs', () => { message: 'GOV.UK Frontend is not supported in this browser' }) }) + + it('throws when receiving the wrong type for $module', async () => { + await expect( + renderAndInitialise(page, 'tabs', { + params: examples.default, + beforeInitialisation() { + // Remove the root of the components as a way + // for the constructor to receive the wrong type for `$module` + document.querySelector('[data-module]').remove() + } + }) + ).rejects.toEqual({ + name: 'TypeError', + message: 'Expected `$module` to be an instance of `HTMLElement`' + }) + }) }) }) }) diff --git a/packages/govuk-frontend/src/govuk/govuk-frontend-component.mjs b/packages/govuk-frontend/src/govuk/govuk-frontend-component.mjs index 0f55c069a3..358232b625 100644 --- a/packages/govuk-frontend/src/govuk/govuk-frontend-component.mjs +++ b/packages/govuk-frontend/src/govuk/govuk-frontend-component.mjs @@ -19,10 +19,12 @@ export class GOVUKFrontendComponent { /** * Constructs a new component, validating that GOV.UK Frontend is supported * + * @param {Element} $module * @internal */ - constructor() { + constructor($module) { this.checkSupport() + this.$module = this.checkModuleType($module) } /** @@ -35,4 +37,30 @@ export class GOVUKFrontendComponent { throw new SupportError() } } + + /** + * Validates the type of $module and returns it (so TypeScript can follow) + * + * If a component requires a more specialised type for `$module`: + * - override `$module` in the child class to set the required type. + * Don't forget to intialise it to `this.$module` to avoid it + * being reset to `undefined` after the component's constructor + * calls `super()`. See: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields#examples + * - override this function to check for the relevant type + * and raise an error mentionning the appropriate type + * + * @protected + * @param {unknown} $module + * @returns {HTMLElement} `$module`, cast as the type expected by the class + */ + checkModuleType($module) { + if (!($module instanceof HTMLElement)) { + throw new TypeError( + 'Expected `$module` to be an instance of `HTMLElement`' + ) + } + + return $module + } }