diff --git a/src/govuk/components/accordion/accordion.puppeteer.test.js b/src/govuk/components/accordion/accordion.puppeteer.test.js index fabe7a041a..07949c585e 100644 --- a/src/govuk/components/accordion/accordion.puppeteer.test.js +++ b/src/govuk/components/accordion/accordion.puppeteer.test.js @@ -725,7 +725,7 @@ describe('/components/accordion', () => { ).rejects.toMatchObject({ name: 'InitError', message: - 'Root element (`$root`) already initialised (`govuk-accordion`)' + 'govuk-accordion: Root element (`$root`) already initialised' }) }) diff --git a/src/govuk/components/button/button.puppeteer.test.js b/src/govuk/components/button/button.puppeteer.test.js index b47803ee5d..3a03f03340 100644 --- a/src/govuk/components/button/button.puppeteer.test.js +++ b/src/govuk/components/button/button.puppeteer.test.js @@ -341,7 +341,7 @@ describe('/components/button', () => { }) ).rejects.toMatchObject({ name: 'InitError', - message: 'Root element (`$root`) already initialised (`govuk-button`)' + message: 'govuk-button: Root element (`$root`) already initialised' }) }) diff --git a/src/govuk/components/character-count/character-count.mjs b/src/govuk/components/character-count/character-count.mjs index 8ece6aaaf4..a7eb3f16e6 100644 --- a/src/govuk/components/character-count/character-count.mjs +++ b/src/govuk/components/character-count/character-count.mjs @@ -1,5 +1,9 @@ import { closestAttributeValue } from '../../common/closest-attribute-value.mjs' -import { mergeConfigs, validateConfig } from '../../common/index.mjs' +import { + formatErrorMessage, + mergeConfigs, + validateConfig +} from '../../common/index.mjs' import { normaliseDataset } from '../../common/normalise-dataset.mjs' import { ConfigError, ElementError } from '../../errors/index.mjs' import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs' @@ -115,7 +119,7 @@ export class CharacterCount extends GOVUKFrontendComponent { // Check for valid config const errors = validateConfig(CharacterCount.schema, this.config) if (errors[0]) { - throw new ConfigError(`Character count: ${errors[0]}`) + throw new ConfigError(formatErrorMessage(CharacterCount, errors[0])) } this.i18n = new I18n(this.config.i18n, { diff --git a/src/govuk/components/character-count/character-count.puppeteer.test.js b/src/govuk/components/character-count/character-count.puppeteer.test.js index 78c1d5780b..aafe1a3d18 100644 --- a/src/govuk/components/character-count/character-count.puppeteer.test.js +++ b/src/govuk/components/character-count/character-count.puppeteer.test.js @@ -825,7 +825,7 @@ describe('Character count', () => { ).rejects.toMatchObject({ name: 'InitError', message: - 'Root element (`$root`) already initialised (`govuk-character-count`)' + 'govuk-character-count: Root element (`$root`) already initialised' }) }) @@ -931,7 +931,7 @@ describe('Character count', () => { cause: { name: 'ConfigError', message: - 'Character count: Either "maxlength" or "maxwords" must be provided' + 'govuk-character-count: Either "maxlength" or "maxwords" must be provided' } }) }) diff --git a/src/govuk/components/checkboxes/checkboxes.puppeteer.test.js b/src/govuk/components/checkboxes/checkboxes.puppeteer.test.js index 8795c6f175..b377b2abff 100644 --- a/src/govuk/components/checkboxes/checkboxes.puppeteer.test.js +++ b/src/govuk/components/checkboxes/checkboxes.puppeteer.test.js @@ -380,7 +380,7 @@ describe('Checkboxes', () => { ).rejects.toMatchObject({ name: 'InitError', message: - 'Root element (`$root`) already initialised (`govuk-checkboxes`)' + 'govuk-checkboxes: Root element (`$root`) already initialised' }) }) diff --git a/src/govuk/components/error-summary/error-summary.puppeteer.test.js b/src/govuk/components/error-summary/error-summary.puppeteer.test.js index f89cdeba82..ffafe62798 100644 --- a/src/govuk/components/error-summary/error-summary.puppeteer.test.js +++ b/src/govuk/components/error-summary/error-summary.puppeteer.test.js @@ -249,7 +249,7 @@ describe('Error Summary', () => { ).rejects.toMatchObject({ name: 'InitError', message: - 'Root element (`$root`) already initialised (`govuk-error-summary`)' + 'govuk-error-summary: Root element (`$root`) already initialised' }) }) diff --git a/src/govuk/components/exit-this-page/exit-this-page.puppeteer.test.js b/src/govuk/components/exit-this-page/exit-this-page.puppeteer.test.js index 0b8339b05c..7c0ea3b7b0 100644 --- a/src/govuk/components/exit-this-page/exit-this-page.puppeteer.test.js +++ b/src/govuk/components/exit-this-page/exit-this-page.puppeteer.test.js @@ -244,7 +244,7 @@ describe('/components/exit-this-page', () => { ).rejects.toMatchObject({ name: 'InitError', message: - 'Root element (`$root`) already initialised (`govuk-exit-this-page`)' + 'govuk-exit-this-page: Root element (`$root`) already initialised' }) }) diff --git a/src/govuk/components/header/header.puppeteer.test.js b/src/govuk/components/header/header.puppeteer.test.js index 5577d1a372..4f9a4eeb7f 100644 --- a/src/govuk/components/header/header.puppeteer.test.js +++ b/src/govuk/components/header/header.puppeteer.test.js @@ -193,7 +193,7 @@ describe('Header navigation', () => { }) ).rejects.toMatchObject({ name: 'InitError', - message: 'Root element (`$root`) already initialised (`govuk-header`)' + message: 'govuk-header: Root element (`$root`) already initialised' }) }) diff --git a/src/govuk/components/notification-banner/notification-banner.puppeteer.test.js b/src/govuk/components/notification-banner/notification-banner.puppeteer.test.js index 1039b7b140..e446a624b7 100644 --- a/src/govuk/components/notification-banner/notification-banner.puppeteer.test.js +++ b/src/govuk/components/notification-banner/notification-banner.puppeteer.test.js @@ -249,7 +249,7 @@ describe('Notification banner', () => { ).rejects.toMatchObject({ name: 'InitError', message: - 'Root element (`$root`) already initialised (`govuk-notification-banner`)' + 'govuk-notification-banner: Root element (`$root`) already initialised' }) }) diff --git a/src/govuk/components/radios/radios.puppeteer.test.js b/src/govuk/components/radios/radios.puppeteer.test.js index acad9affc0..f83b2475ac 100644 --- a/src/govuk/components/radios/radios.puppeteer.test.js +++ b/src/govuk/components/radios/radios.puppeteer.test.js @@ -332,7 +332,7 @@ describe('Radios', () => { }) ).rejects.toMatchObject({ name: 'InitError', - message: 'Root element (`$root`) already initialised (`govuk-radios`)' + message: 'govuk-radios: Root element (`$root`) already initialised' }) }) diff --git a/src/govuk/components/skip-link/skip-link.puppeteer.test.js b/src/govuk/components/skip-link/skip-link.puppeteer.test.js index 29b8a491fc..ff2b0af34e 100644 --- a/src/govuk/components/skip-link/skip-link.puppeteer.test.js +++ b/src/govuk/components/skip-link/skip-link.puppeteer.test.js @@ -139,8 +139,7 @@ describe('Skip Link', () => { }) ).rejects.toMatchObject({ name: 'InitError', - message: - 'Root element (`$root`) already initialised (`govuk-skip-link`)' + message: 'govuk-skip-link: Root element (`$root`) already initialised' }) }) diff --git a/src/govuk/components/tabs/tabs.puppeteer.test.js b/src/govuk/components/tabs/tabs.puppeteer.test.js index 5ce06535be..212aa6df3f 100644 --- a/src/govuk/components/tabs/tabs.puppeteer.test.js +++ b/src/govuk/components/tabs/tabs.puppeteer.test.js @@ -281,7 +281,7 @@ describe('/components/tabs', () => { }) ).rejects.toMatchObject({ name: 'InitError', - message: 'Root element (`$root`) already initialised (`govuk-tabs`)' + message: 'govuk-tabs: Root element (`$root`) already initialised' }) }) diff --git a/src/govuk/errors/index.jsdom.test.mjs b/src/govuk/errors/index.jsdom.test.mjs index 0f149ae2ad..e33afbcec0 100644 --- a/src/govuk/errors/index.jsdom.test.mjs +++ b/src/govuk/errors/index.jsdom.test.mjs @@ -56,25 +56,21 @@ describe('errors', () => { describe('InitError', () => { it('is an instance of GOVUKFrontendError', () => { - expect(new InitError('govuk-accordion')).toBeInstanceOf( - GOVUKFrontendError - ) + expect(new InitError(Accordion)).toBeInstanceOf(GOVUKFrontendError) }) it('has its own name set', () => { - expect(new InitError('govuk-accordion').name).toBe('InitError') + expect(new InitError(Accordion).name).toBe('InitError') }) it('provides feedback for modules already initialised', () => { - expect(new InitError('govuk-accordion').message).toBe( - 'Root element (`$root`) already initialised (`govuk-accordion`)' + expect(new InitError(Accordion).message).toBe( + 'govuk-accordion: Root element (`$root`) already initialised' ) }) - it('provides feedback when no module name is provided', () => { - expect(new InitError(undefined, 'Accordion').message).toBe( - 'moduleName not defined in component (`Accordion`)' - ) + it('allows a custom message to be provided', () => { + expect(new InitError('custom message').message).toBe('custom message') }) }) diff --git a/src/govuk/errors/index.mjs b/src/govuk/errors/index.mjs index 655c828bb0..fef067f33c 100644 --- a/src/govuk/errors/index.mjs +++ b/src/govuk/errors/index.mjs @@ -107,17 +107,18 @@ export class InitError extends GOVUKFrontendError { /** * @internal - * @param {string|undefined} moduleName - name of the component module - * @param {string} [className] - name of the component module + * @param {ComponentWithModuleName | string} componentOrMessage - name of the component module */ - constructor(moduleName, className) { - let errorText = `moduleName not defined in component (\`${className}\`)` + constructor(componentOrMessage) { + const message = + typeof componentOrMessage === 'string' + ? componentOrMessage + : formatErrorMessage( + componentOrMessage, + `Root element (\`$root\`) already initialised` + ) - if (typeof moduleName === 'string') { - errorText = `Root element (\`$root\`) already initialised (\`${moduleName}\`)` - } - - super(errorText) + super(message) } } @@ -129,5 +130,9 @@ export class InitError extends GOVUKFrontendError { * @property {string} identifier - An identifier that'll let the user understand which element has an error. This is whatever makes the most sense * @property {Element | null} [element] - The element in error * @property {string} [expectedType] - The type that was expected for the identifier - * @property {import('../common/index.mjs').ComponentWithModuleName} component - Component throwing the error + * @property {ComponentWithModuleName} component - Component throwing the error + */ + +/** + * @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName */ diff --git a/src/govuk/govuk-frontend-component.mjs b/src/govuk/govuk-frontend-component.mjs index f762d89301..9940e72a57 100644 --- a/src/govuk/govuk-frontend-component.mjs +++ b/src/govuk/govuk-frontend-component.mjs @@ -20,17 +20,24 @@ export class GOVUKFrontendComponent { this.constructor ) + // TypeScript does not enforce that inheriting classes will define a `moduleName` + // (even if we add a `@virtual` `static moduleName` property to this class). + // While we trust users to do this correctly, we do a little check to provide them + // a helpful error message. + // + // After this, we'll be sure that `childConstructor` has a `moduleName` + // as expected of the `ChildClassConstructor` we've cast `this.constructor` to. + if (typeof childConstructor.moduleName !== 'string') { + throw new InitError(`\`moduleName\` not defined in component`) + } + childConstructor.checkSupport() this.checkInitialised($root) const moduleName = childConstructor.moduleName - if (typeof moduleName === 'string') { - moduleName && $root?.setAttribute(`data-${moduleName}-init`, '') - } else { - throw new InitError(moduleName) - } + $root?.setAttribute(`data-${moduleName}-init`, '') } /** @@ -41,11 +48,11 @@ export class GOVUKFrontendComponent { * @throws {InitError} when component is already initialised */ checkInitialised($root) { - const moduleName = /** @type {ChildClassConstructor} */ (this.constructor) - .moduleName + const constructor = /** @type {ChildClassConstructor} */ (this.constructor) + const moduleName = constructor.moduleName if ($root && moduleName && isInitialised($root, moduleName)) { - throw new InitError(moduleName) + throw new InitError(constructor) } } @@ -63,7 +70,7 @@ export class GOVUKFrontendComponent { /** * @typedef ChildClass - * @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component + * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component */ /**