Skip to content

Commit

Permalink
initall onerror
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickpatrickpatrick committed Aug 29, 2024
1 parent 69763be commit 1fc0977
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 6 deletions.
120 changes: 119 additions & 1 deletion packages/govuk-frontend/src/govuk/init.jsdom.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,40 @@ describe('createAll', () => {
expect(result).toStrictEqual([])
})

it('executes specified onError callback and returns empty array if not supported', () => {
document.body.classList.remove('govuk-frontend-supported')

const errorCallback = jest.fn((error, context) => {
console.log(error)
console.log(context)
})

// Silence warnings in test output, and allow us to 'expect' them
jest.spyOn(global.console, 'log').mockImplementation()

expect(() => {
createAll(
MockComponent,
{ attribute: 'random' },
{ onError: errorCallback }
)
}).not.toThrow()

expect(errorCallback).toHaveBeenCalled()

expect(global.console.log).toHaveBeenCalledWith(
expect.objectContaining({
message: 'GOV.UK Frontend is not supported in this browser'
})
)
expect(global.console.log).toHaveBeenCalledWith(
expect.objectContaining({
component: MockComponent,
config: { attribute: 'random' }
})
)
})

it('returns an empty array if no matching components exist on the page', () => {
const componentRoot = document.createElement('div')
componentRoot.setAttribute(
Expand Down Expand Up @@ -290,7 +324,7 @@ describe('createAll', () => {
})
})

describe('when a $scope is passed', () => {
describe('when a $scope is passed as third parameter', () => {
it('only initialises components within that scope', () => {
document.body.innerHTML = `
<div data-module="mock-component"></div>
Expand All @@ -313,6 +347,28 @@ describe('createAll', () => {
document.querySelector('.my-scope [data-module="mock-component"]')
])
})

it('only initialises components within that scope if scope passed as options attribute', () => {
document.body.innerHTML = `
<div data-module="mock-component"></div>
<div class="not-in-scope">
<div data-module="mock-component"></div>
</div>'
<div class="my-scope">
<div data-module="mock-component"></div>
</div>`

const result = createAll(MockComponent, undefined, {
onError: (e, x) => {},
scope: document.querySelector('.my-scope')
})

expect(result).toStrictEqual([expect.any(MockComponent)])

expect(result[0].args).toStrictEqual([
document.querySelector('.my-scope [data-module="mock-component"]')
])
})
})

describe('when components throw errors', () => {
Expand All @@ -325,6 +381,68 @@ describe('createAll', () => {
}
}

it('executes callback if specified as part of options object', () => {
document.body.innerHTML = `<div data-module="mock-component" data-boom></div>`

const errorCallback = jest.fn((error, context) => {
console.log(error)
console.log(context)
})

// Silence warnings in test output, and allow us to 'expect' them
jest.spyOn(global.console, 'log').mockImplementation()

expect(() => {
createAll(
MockComponentThatErrors,
{ attribute: 'random' },
{ onError: errorCallback }
)
}).not.toThrow()

expect(errorCallback).toHaveBeenCalled()

expect(global.console.log).toHaveBeenCalledWith(expect.any(Error))
expect(global.console.log).toHaveBeenCalledWith(
expect.objectContaining({
component: MockComponentThatErrors,
config: { attribute: 'random' },
element: document.querySelector('[data-module="mock-component"]')
})
)
})

it('executes callback if specified as function', () => {
document.body.innerHTML = `<div data-module="mock-component" data-boom></div>`

const errorCallback = jest.fn((error, context) => {
console.log(error)
console.log(context)
})

// Silence warnings in test output, and allow us to 'expect' them
jest.spyOn(global.console, 'log').mockImplementation()

expect(() => {
createAll(
MockComponentThatErrors,
{ attribute: 'random' },
errorCallback
)
}).not.toThrow()

expect(errorCallback).toHaveBeenCalled()

expect(global.console.log).toHaveBeenCalledWith(expect.any(Error))
expect(global.console.log).toHaveBeenCalledWith(
expect.objectContaining({
component: MockComponentThatErrors,
config: { attribute: 'random' },
element: document.querySelector('[data-module="mock-component"]')
})
)
})

it('catches errors thrown by components and logs them to the console', () => {
document.body.innerHTML = `<div data-module="mock-component" data-boom></div>`

Expand Down
69 changes: 64 additions & 5 deletions packages/govuk-frontend/src/govuk/init.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,46 @@ function initAll(config) {
*
* @template {CompatibleClass} T
* @param {T} Component - class of the component to create
* @param {T["defaults"]} [config] - config for the component
* @param {Element|Document} [$scope] - scope of the document to search within
* @param {T["defaults"]} [config] - Config supplied to component
* @param {OnErrorCallback<T> | Element | Document | CreateAllOptions<T> } [createAllOptions] - options for createAll including scope of the document to search within and callback function if error throw by component on init
* @returns {Array<InstanceType<T>>} - array of instantiated components
*/
function createAll(Component, config, $scope = document) {
function createAll(Component, config, createAllOptions) {
let /** @type {Element | Document} */ $scope = document
let /** @type {OnErrorCallback<Component> | undefined} */ onError

if (typeof createAllOptions === 'object') {
createAllOptions = /** @type {CreateAllOptions<Component>} */ (
// eslint-disable-next-line no-self-assign
createAllOptions
)

$scope = createAllOptions.scope ?? $scope
onError = createAllOptions.onError
}

if (typeof createAllOptions === 'function') {
onError = createAllOptions
}

if (createAllOptions instanceof HTMLElement) {
$scope = createAllOptions
}

const $elements = $scope.querySelectorAll(
`[data-module="${Component.moduleName}"]`
)

// Skip initialisation when GOV.UK Frontend is not supported
if (!isSupported()) {
console.log(new SupportError())
if (onError) {
onError(new SupportError(), {
component: Component,
config
})
} else {
console.log(new SupportError())
}
return []
}

Expand All @@ -96,7 +124,16 @@ function createAll(Component, config, $scope = document) {
? new Component($element, config)
: new Component($element)
} catch (error) {
console.log(error)
if (onError && error instanceof Error) {
onError(error, {
element: $element,
component: Component,
config
})
} else {
console.log(error)
}

return null
}
})
Expand Down Expand Up @@ -150,3 +187,25 @@ export { initAll, createAll }
*
* @typedef {keyof Config} ConfigKey
*/

/**
* @template {CompatibleClass} T
* @typedef {object} ErrorContext
* @property {Element} [element] - Element used for component module initialisation
* @property {T} component - Class of component
* @property {T["defaults"]} config - Config supplied to component
*/

/**
* @template {CompatibleClass} T
* @callback OnErrorCallback
* @param {Error} error - Thrown error
* @param {ErrorContext<T>} context - Object containing the element, component class and configuration
*/

/**
* @template {CompatibleClass} T
* @typedef {object} CreateAllOptions
* @property {Element | Document} [scope] - scope of the document to search within
* @property {OnErrorCallback<T>} [onError] - callback function if error throw by component on init
*/

0 comments on commit 1fc0977

Please sign in to comment.