Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw SupportError when instantiating components where GOV.UK Frontend is not supported #4030

Merged
merged 5 commits into from
Aug 11, 2023

Conversation

romaricpascal
Copy link
Member

@romaricpascal romaricpascal commented Jul 31, 2023

This PR updates all our JavaScript components to throw a GOVUKFrontendNotSupportedError if they get instantiated where GOV.UK Frontend is not supported.

Besides updating the components to throw this new error the PR:

  • introduces a GOVUKFrontendError for our errors to subclass. It allows users to easily separate our custom errors from regular JavaScript errors using instanceof.
  • updates renderAndInitialise to make running code before the component's initialisation more reliable (had issues where the initialiser – now renamed beforeInitialisation for clarity – didn't execute when run in the middle of the block instantiating the component).

Closes #4009

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 July 31, 2023 17:20 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 July 31, 2023 17:39 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 1, 2023 13:15 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 1, 2023 13:16 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 1, 2023 14:44 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 1, 2023 15:01 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 1, 2023 15:33 Inactive
@romaricpascal romaricpascal changed the title [WIP] Throw errors during our components' instantiation Throw error when instantiating components where GOV.UK Frontend is not supported Aug 1, 2023
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 1, 2023 15:40 Inactive
@romaricpascal romaricpascal marked this pull request as ready for review August 1, 2023 15:40
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 1, 2023 16:57 Inactive
@@ -103,6 +104,7 @@ export {
Checkboxes,
ErrorSummary,
ExitThisPage,
GOVUKFrontendError,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only export our base GOVUKFrontendError rather than all error classes with the aim of:

  • allowing separating our own errors from native ones in bulk, which users can do running instanceof GOVUKFrontendError
  • but not crowding our export with each error we throw, as users can check the type of error through their name property

Copy link
Contributor

@colinrotherham colinrotherham Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fab work in this PR Romaric

Do we want to discuss if we're ready for errors to be part of our "public" API?

Bit like the I18n class where we chose not to export it

(Gives us freedom to keep iterating without breaking changes)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up for discussion, I'll revive the thread on Slack where I proposed the approach I implemented here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's default to not exporting it yet, can always add it in another PR

* @abstract
*/
export class GOVUKFrontendError extends Error {
name = 'GOVUKFrontendError'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annoying, but necessary. Simply extending Error doesn't give the error a new name, unfortunately.

And going for something automated like this.name = this.constructor.name in GOVUKFrontendError's constructor means the error gets a mangled name once minifier have run over the code and rename the class to say n. While we may prevent the mangling in our files, we don't control how services minify theirs, so hardcoding the name limits risks of having an error with the name of n.

@@ -133,6 +133,40 @@ Avoid using namespace imports (`import * as namespace`) in code bundled for Comm

Prefer named exports over default exports to avoid compatibility issues with transpiler "synthetic default" as discussed in: https://github.com/alphagov/govuk-frontend/issues/2829

## Throwing errors
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a starter for 10 listing the spirit in which the errors have been implemented in this PR. Happy to discuss and update.

@@ -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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a random thought

Similar to how TypeError isn't NotCorrectTypeError, should we flip out the Not?

Suggested change
throw new GOVUKFrontendNotSupportedError()
throw new GOVUKFrontendSupportError()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, and that makes it shorter, I'll rename 😊

renderAndInitialise(page, 'accordion', {
params: examples.default,
beforeInitialisation () {
document.body.classList.remove('govuk-frontend-supported')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

@@ -648,7 +670,7 @@ describe('Character count', () => {
// Override maxlength to 10
maxlength: 10
},
initialiser ($module) {
beforeInitialisation ($module) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it's no longer with us, as a sign of respect for .init() shall we rename this? 😆

Suppose it matches initAll() a bit better too

Suggested change
beforeInitialisation ($module) {
beforeInit ($module) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not calling initAll as a way to intialise and init is gone, so I'm not sure we should go for the abreviated version. Could shorten it to before but I'd rather be verbose in case we add other before moments (though that'd probably be a hint for refactoring).

// for example if they initialise the component
// on their own by directly passing the result
// of `document.querySelector`.
// To avoid breaking further JavaScript initialisation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was quite fond of this message, all on its own in ErrorSummary

Thoughts on adding a little disclaimer to every component?

  1. Error throws
  2. Error console shows stack trace
  3. Stack trace uses original source via source maps
  4. Developer clicks to these lines and reads helpful message

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think most of that info should be summarised in the error message when implementing https://github.com/alphagov/govuk-frontend/issues/4035.

Regarding adding things to each components, I think this would rather be the role of a base Component class. I think with the error throwing for GOV.UK Frontend support and the type check of $module there's enough ground for creating it already (only to add new features to it, not refactor existing code into it) and centralise these checks and possible comments.

@@ -37,6 +37,7 @@ describe('GOV.UK Frontend', () => {
'Checkboxes',
'ErrorSummary',
'ExitThisPage',
'GOVUKFrontendError',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to https://github.com/alphagov/govuk-frontend/pull/4030/files#r1284215663

Should we not export our errors in the default export, we'll need to manage Terser mangling:

terser({
format: { comments: false },
mangle: { reserved: Object.keys(GOVUKFrontend) },

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand on what we'll need to manage on the Terser side if we don't export the errors internally?

If it's about the error naming after mangling, the hardcoded name property is there to handle the situation (especially as we can't guarantee that library users will take the same step as us towards not mangling the name of the errors/our exports).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that mangle.reserved option

But if the mangled error class name doesn't appear in stack traces then all good

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment about mangling on this PR: #4075.

The name is clear in the logged message, but mangled in the stacktrace. I think mangling is worth its own discussion, as it may be worth disabling it altogether rather than keep maintaining an evergrowing list of names to not mangle for our min.js file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And opened #4076 to explore our options.

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"', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice little test refactor with the describe() nesting, mind if we split it into another commit?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No probs, I'll split the extra describes in their own commit to separate them from the implementation of the error throwing.

await page.setJavaScriptEnabled(false)
})
describe('Radios', () => {
describe('with conditional reveals', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this one, separate commit?

Jest output is improved too ⭐

@@ -0,0 +1,33 @@
/**
* A base class for `Error`s thrown by GOV.UK Frontend.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind if we keep a short and sweet "title" at the top for any generated docs?

Suggested change
* A base class for `Error`s thrown by GOV.UK Frontend.
* GOV.UK Frontend error
*
* A base class for errors thrown by GOV.UK Frontend.

* to be thrown by our code.
*
* @example
* ```js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a reminder to use mjs over js for ES modules code blocks

Can't remember if ESLint is set up for these (like it is for Markdown) but it might matter one day

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example doesn't use anything module specific (import/export) so plain js is fine there 😊

Regarding ESLint, it doesn't seem we've enabled jsdoc/check-examples. We can discuss its enabling in a separate part as it may have some tricky side effects (eg. requiring this class to have its own JSDoc block inside the example).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah not so much the module specific stuff

We tend to use *.mjs as the default now, so some rules like the TypeScript parser are only for *.mjs

files: ['src/govuk/**/*.mjs'],
excludedFiles: ['**/*.test.mjs'],
parser: '@typescript-eslint/parser',

* name = "MissingRootError"
* }
* ```
* @abstract
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah do we not want to use this class directly, never ever?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, that's the intent. If we don't create subclasses with their own names, that means updating an error's message becomes a breaking change, as the message would be the only way to distinguish one error from another. Subclassing (with a specific name) ensures we can adjust the wording of the messages freely as the name (or class itself should we/when we export it) is the public API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth enforcing this in the constructor too?

export class GOVUKFrontendError extends Error {
  name = 'GOVUKFrontendError'

  constructor () {
    if (this.constructor == GOVUKFrontendError) {
      throw new Error("GOVUKFrontendError is an abstract class and can't be instantiated");
    }
  }
}

Or is errors throwing errors too confusing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it's worth enforcing the @abstract in code. I'd rather lean towards shipping less code and if we get evidence that people misuse it, clamp down on it rather than be pre-emptively defensive 😊

export class GOVUKFrontendNotSupportedError extends GOVUKFrontendError {
name = 'GOVUKFrontendNotSupportedError'

/** */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** */

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, good spot! 🦅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, no, that wasn't whoops, it's making ESLint happy as there's not much to document on that constructor 🤔 I'll replace with a more explicit disabling of ESLint for that line 😊

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 10, 2023 15:04 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 10, 2023 15:39 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 10, 2023 15:50 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 10, 2023 15:59 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 10, 2023 16:04 Inactive
* @param {HTMLElement} [$scope] - The `<body>` element of the document to check for support
* @returns {boolean} Whether GOV.UK Frontend is supported on this page
*/
export function isSupported($scope = document.body) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have browser support for parameter defaults without Babel polyfills?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, Babel leaves that line as is in dist. Support looks a bit older than what we're configured to support. And if transpilation happened, there's no polyfills involved. So all good there 🎉

…ialise`

- Clarify naming as it's actually code that runs before initialisation rather than does any kind of initialisation of the component itself
- Update how the hook is run to be directly called by `page.evaluate`.
  Having it run as part of the function that did the initialisation didn't run reliably for updating the `govuk-frontend-supported` class,
  or even other simpler scenarios like logging or throwing. Calling it directly with `page.evaluate` seems to run more reliably.
Adds a couple of extra `describe` to group existing tests
in anticipation of new tests sections for the errors
Allows components to indicate they didn't inistantiate because GOV.UK Frontend is not supported
It's the same code for all components and the architecture we're looking to implement
so we may as well start introducing it, keeping it internal
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4030 August 11, 2023 09:24 Inactive
@romaricpascal romaricpascal merged commit dc1d5c9 into main Aug 11, 2023
41 checks passed
@romaricpascal romaricpascal deleted the component-throw-errors branch August 11, 2023 09:50
@colinrotherham colinrotherham changed the title Throw error when instantiating components where GOV.UK Frontend is not supported Throw SupportError when instantiating components where GOV.UK Frontend is not supported Sep 7, 2023
colinrotherham added a commit that referenced this pull request Sep 8, 2023
colinrotherham added a commit that referenced this pull request Sep 8, 2023
colinrotherham added a commit that referenced this pull request Sep 27, 2023
colinrotherham added a commit that referenced this pull request Sep 27, 2023
@romaricpascal romaricpascal mentioned this pull request Dec 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Throw errors if GOV.UK Frontend is not supported
4 participants