Skip to content

Commit

Permalink
Extract and test error page builder
Browse files Browse the repository at this point in the history
  • Loading branch information
Quetzacoalt91 committed Jan 30, 2025
1 parent 9d637b3 commit e945508
Show file tree
Hide file tree
Showing 4 changed files with 213 additions and 33 deletions.
4 changes: 4 additions & 0 deletions _dev/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
49 changes: 49 additions & 0 deletions _dev/src/ts/components/ErrorPageBuilder.ts
Original file line number Diff line number Diff line change
@@ -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<ApiError, 'code' | 'type'>): 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;
}
}
39 changes: 6 additions & 33 deletions _dev/src/ts/pages/ErrorPage.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -18,7 +19,7 @@ export default class ErrorPage extends PageAbstract {

public mount = (): void => {
// If the error page is already present on the DOM (For instance on a whole page refresh),
// initalize it at once instead of waiting for an event.
// initialize it at once instead of waiting for an event.
const errorPageFromBackEnd = document.querySelector('.error-page');
if (errorPageFromBackEnd) {
this.#mountErrorPage(errorPageFromBackEnd);
Expand Down Expand Up @@ -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) {
Expand Down
154 changes: 154 additions & 0 deletions _dev/tests/components/ErrorPageBuilder.test.ts
Original file line number Diff line number Diff line change
@@ -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(`<div id="ua_error_placeholder" class="error-page">
<div class="error-page__container">
<div class="error-page__code">
<!-- Leave these 3 <span> on the same line or the code will be all broken -->
<span class="error-page__code-char" data-error-code-char-index="1"></span><span class="error-page__code-char" data-error-code-char-index="2"></span><span class="error-page__code-char" data-error-code-char-index="3"></span>
</div>
<div class="error-page__infos">
<h2 class="error-page__title">
Something went wrong...
</h2>
<div class="error-page__desc">
<div class="error-page__desc-404 hidden">
<p>The requested page or resource could not be found. This might be due to:</p>
<ul>
<li>A broken or outdated link.</li>
<li>The page being moved or deleted.</li>
<li>A typo in the URL.</li>
</ul>
</div>
<div class="error-page__desc-500 hidden">
<p>It seems there was an issue with the server. This type of error usually happens when:</p>
<ul>
<li>The server is temporarily unavailable.</li>
<li>There's a misconfiguration or unexpected problem on the server.</li>
</ul>
</div>
<div class="error-page__desc-502 hidden">
<p>It looks like there is no data received from the server. This can happen if:</p>
<ul>
<li>The server is temporarily unavailable.</li>
<li>There's an issue with the network or connection.</li>
</ul>
</div>
<div class="error-page__desc-ETIMEDOUT hidden">
<p>The request timed out. It seems the connection took too long to respond. This could be due to:</p>
<ul>
<li>A slow or unstable internet connection.</li>
<li>The server is currently busy or unresponsive.</li>
<li>Temporary network issues.</li>
</ul>
</div>
<div class="error-page__desc-APP_ERR_RESPONSE_BAD_TYPE hidden">
<p>The response received from the server in malformed. This can happen if:</p>
<ul>
<li>The request did not reach the module.</li>
<li>There’s an issue processing the response on the app or browser.</li>
</ul>
</div>
<div class="error-page__desc-APP_ERR_RESPONSE_EMPTY hidden">
<p>It looks like there is no data received from the server. This can happen if:</p>
<ul>
<li>The server is temporarily unavailable.</li>
<li>There's an issue with the network or connection.</li>
</ul>
</div>
</div>
<div class="error-page__buttons">
<form id="submit-error-report" name="submit-error-report" data-route-to-submit="update-step-update-submit-error-report">
<button class="btn btn-lg btn-default" type="submit">
<i class="material-icons">send</i>
Send error report
</button>
</form>
<form class="error-page__home-page-form hidden" id="home-page-form" name="home-page-form" data-route-to-submit="home-page">
<button class="btn btn-primary btn-lg" type="submit">
Go back to Update assistant
</button>
</form>
<a class="btn btn-primary btn-lg hidden" id="exit-button" href="/admin-dev">
<i class="material-icons">exit_to_app</i>
Exit
</a>
</div>
</div>
</div>
</div>`);
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');
});
});

0 comments on commit e945508

Please sign in to comment.