From 34960e52eecb4af1e19d35d31f161c0c892cb94b Mon Sep 17 00:00:00 2001 From: Thomas Nabord Date: Wed, 29 Jan 2025 18:06:28 +0000 Subject: [PATCH] Extract and test error page builder --- _dev/jest.setup.ts | 4 + _dev/src/ts/components/ErrorPageBuilder.ts | 49 ++++++ _dev/src/ts/pages/ErrorPage.ts | 37 +---- .../tests/components/ErrorPageBuilder.test.ts | 154 ++++++++++++++++++ 4 files changed, 212 insertions(+), 32 deletions(-) create mode 100644 _dev/src/ts/components/ErrorPageBuilder.ts create mode 100644 _dev/tests/components/ErrorPageBuilder.test.ts diff --git a/_dev/jest.setup.ts b/_dev/jest.setup.ts index e9a7b2b5b..d01f0b58a 100644 --- a/_dev/jest.setup.ts +++ b/_dev/jest.setup.ts @@ -1,3 +1,7 @@ +import { TextEncoder, TextDecoder } from 'util'; +// Needed to avoid error "ReferenceError: TextEncoder is not defined" when using JSDOM in tests +Object.assign(global, { TextDecoder, TextEncoder }); + // We don't wait for the call to beforeAll to define window properties. window.AutoUpgradeVariables = { token: 'test-token', diff --git a/_dev/src/ts/components/ErrorPageBuilder.ts b/_dev/src/ts/components/ErrorPageBuilder.ts new file mode 100644 index 000000000..38c312692 --- /dev/null +++ b/_dev/src/ts/components/ErrorPageBuilder.ts @@ -0,0 +1,49 @@ +import { ApiError } from '../types/apiTypes'; + +export default class ErrorPageBuilder { + public constructor(private readonly errorElement: DocumentFragment) {} + + /** + * Replace the id of the cloned element + */ + public updateId(type: ApiError['type']): void { + const errorChild = this.errorElement.getElementById('ua_error_placeholder'); + if (errorChild) { + errorChild.id = `ua_error_${type}`; + } + } + + /** + * If code is a HTTP error number (i.e 404, 500 etc.), let's change the text in the left column with it. + */ + public updateLeftColumn(code: ApiError['code']): void { + if (this.#isHttpErrorCode(code)) { + const stringifiedCode = (code as number).toString().replaceAll('0', 'O'); + const errorCodeSlotElements = this.errorElement.querySelectorAll('.error-page__code-char'); + errorCodeSlotElements.forEach((element: Element, index: number) => { + element.innerHTML = stringifiedCode[index]; + }); + } else { + this.errorElement.querySelector('.error-page__code')?.classList.add('hidden'); + } + } + + /** + * Display a user friendly text related to the code if it exists, otherwise write the error code. + */ + public updateDescriptionBlock(errorDetails: Pick): void { + const errorDescriptionElement = this.errorElement.querySelector('.error-page__desc'); + const userFriendlyDescriptionElement = errorDescriptionElement?.querySelector( + `.error-page__desc-${this.#isHttpErrorCode(errorDetails.code) ? errorDetails.code : errorDetails.type}` + ); + if (userFriendlyDescriptionElement) { + userFriendlyDescriptionElement.classList.remove('hidden'); + } else if (errorDescriptionElement && errorDetails.type) { + errorDescriptionElement.innerHTML = errorDetails.type; + } + } + + #isHttpErrorCode(code?: number): boolean { + return typeof code === 'number' && code >= 300 && code.toString().length === 3; + } +} diff --git a/_dev/src/ts/pages/ErrorPage.ts b/_dev/src/ts/pages/ErrorPage.ts index 4818547ab..4b11e6c84 100644 --- a/_dev/src/ts/pages/ErrorPage.ts +++ b/_dev/src/ts/pages/ErrorPage.ts @@ -1,4 +1,5 @@ import api from '../api/RequestHandler'; +import ErrorPageBuilder from '../components/ErrorPageBuilder'; import { logStore } from '../store/LogStore'; import { ApiError } from '../types/apiTypes'; import { Severity } from '../types/logsTypes'; @@ -60,38 +61,10 @@ export default class ErrorPage extends PageAbstract { // Duplicate the error template before alteration const errorElement = this.#errorTemplateElement.content.cloneNode(true) as DocumentFragment; - // Set the id of the cloned element - const errorChild = errorElement.getElementById('ua_error_placeholder'); - if (errorChild) { - errorChild.id = `ua_error_${event.detail.type}`; - } - - const isHttpErrorCode = - typeof event.detail.code === 'number' && - event.detail.code >= 300 && - event.detail.code.toString().length === 3; - - // If code is a HTTP error number (i.e 404, 500 etc.), let's change the text in the left column with it. - if (isHttpErrorCode) { - const stringifiedCode = (event.detail.code as number).toString().replaceAll('0', 'O'); - const errorCodeSlotElements = errorElement.querySelectorAll('.error-page__code-char'); - errorCodeSlotElements.forEach((element: Element, index: number) => { - element.innerHTML = stringifiedCode[index]; - }); - } else { - errorElement.querySelector('.error-page__code')?.classList.add('hidden'); - } - - // Display a user friendly text related to the code if it exists, otherwise write the error code. - const errorDescriptionElement = errorElement.querySelector('.error-page__desc'); - const userFriendlyDescriptionElement = errorDescriptionElement?.querySelector( - `.error-page__desc-${isHttpErrorCode ? event.detail.code : event.detail.type}` - ); - if (userFriendlyDescriptionElement) { - userFriendlyDescriptionElement.classList.remove('hidden'); - } else if (errorDescriptionElement && event.detail.type) { - errorDescriptionElement.innerHTML = event.detail.type; - } + const pageBuilder = new ErrorPageBuilder(errorElement); + pageBuilder.updateId(event.detail.type); + pageBuilder.updateLeftColumn(event.detail.code); + pageBuilder.updateDescriptionBlock(event.detail); // Store the contents in the logs so it can be used in the error reporting modal if (event.detail.additionalContents) { diff --git a/_dev/tests/components/ErrorPageBuilder.test.ts b/_dev/tests/components/ErrorPageBuilder.test.ts new file mode 100644 index 000000000..ec8a511e8 --- /dev/null +++ b/_dev/tests/components/ErrorPageBuilder.test.ts @@ -0,0 +1,154 @@ +import { JSDOM } from 'jsdom'; +import ErrorPageBuilder from '../../src/ts/components/ErrorPageBuilder'; + +describe('ErrorPageBuilder', () => { + let errorElement: DocumentFragment; + let errorPageBuilder: ErrorPageBuilder; + + beforeEach(() => { + errorElement = JSDOM.fragment(`
+
+
+ + +
+ +
+

+ Something went wrong... +

+ +
+ + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+
+
+
`); + errorPageBuilder = new ErrorPageBuilder(errorElement); + }); + + test('updateId should update the id of the error placeholder', () => { + const referenceToDiv = errorElement.getElementById('ua_error_placeholder'); + errorPageBuilder.updateId('404'); + expect(referenceToDiv!.id).toBe('ua_error_404'); + }); + + test('updateLeftColumn should update error code display with HTTP 404', () => { + errorPageBuilder.updateLeftColumn(404); + const chars = errorElement.querySelectorAll('.error-page__code-char'); + expect(chars[0].innerHTML).toBe('4'); + expect(chars[1].innerHTML).toBe('O'); + expect(chars[2].innerHTML).toBe('4'); + }); + + test('updateLeftColumn should update error code display with HTTP 500', () => { + errorPageBuilder.updateLeftColumn(500); + const chars = errorElement.querySelectorAll('.error-page__code-char'); + expect(chars[0].innerHTML).toBe('5'); + expect(chars[1].innerHTML).toBe('O'); + expect(chars[2].innerHTML).toBe('O'); + }); + + test('updateLeftColumn should hide the panel if not an HTTP error', () => { + errorPageBuilder.updateLeftColumn(1234); + expect(errorElement.querySelector('.error-page__code')!.classList.contains('hidden')).toBe( + true + ); + }); + + test('updateLeftColumn should hide the panel if code is empty', () => { + errorPageBuilder.updateLeftColumn(undefined); + expect(errorElement.querySelector('.error-page__code')!.classList.contains('hidden')).toBe( + true + ); + }); + + test('updateDescriptionBlock should show a user-friendly message of a HTTP code if available', () => { + errorPageBuilder.updateDescriptionBlock({ code: 404, type: 'NOT_FOUND' }); + expect(errorElement.querySelector('.error-page__desc-404')!.classList.contains('hidden')).toBe( + false + ); + }); + + test('updateDescriptionBlock should show a user-friendly message of a error type if available', () => { + errorPageBuilder.updateDescriptionBlock({ code: undefined, type: 'APP_ERR_RESPONSE_EMPTY' }); + expect( + errorElement + .querySelector('.error-page__desc-APP_ERR_RESPONSE_EMPTY')! + .classList.contains('hidden') + ).toBe(false); + }); + + test('updateDescriptionBlock should set error type as text if no message available', () => { + errorPageBuilder.updateDescriptionBlock({ code: 999, type: 'CUSTOM_ERROR' }); + expect(errorElement.querySelector('.error-page__desc')!.innerHTML).toBe('CUSTOM_ERROR'); + }); +});