Context
I am working on unit tests for a TS project led by Dave Gray. (YouTube, GitHub) More specificaly, I am testing the HTML display which is managed by the class ListTemplate. Because there is only one list into our application, this class is a Singleton.
The singleton defined is slightly different compared to Refactoring.Guru singleton
In the project,static instance: ListTemplate = new ListTemplate() is used while Refactoring.Guru is using
public static getInstance(): ListTemplate {
if (!ListTemplate .instance) {
ListTemplate .instance = new ListTemplate ();
}
return ListTemplate .instance;
}
Problem
While the behavior of the application remains the same between theses two syntaxes, unit test is not. I properly configured the jsdom environment so "document" can be altered as we want.
Call stack - Syntax Singleton Refactoring Guru
"-- FullList constructor --"
"ListTemplate"
"beforeEach global"
"beforeEach local ListTemplate"
"should display html"
"-- ListTemplate constuctor --"
"<ul id="listItems"></ul>"
"HTMLUListElement {}"
Call stack - Syntax Singleton Dave Gray
"-- FullList constructor --"
"-- ListTemplate constructor --"
""
"null"
"ListTemplate"
"beforeEach global"
"beforeEach local ListTemplate"
"should display html"
TypeError: Cannot set properties of null (setting 'innerHTML')
40 |
41 | clear(): void {
> 42 | this.ul.innerHTML = "";
| ^
43 | }
44 |
45 | debugDocAgain(): void {
at ListTemplate.clear (src/templates/ListTemplate.ts:42:22)
at ListTemplate.render (src/templates/ListTemplate.ts:50:10)
at Object.<anonymous> (tests/templates/ListTemplate.test.ts:513:27)
With this syntax, the constructor of ListTemplate runs before the initialization of the dom in "beforeEach global", which is not the case with the syntax of Refactoring Guru. Why ?
Files
Please find below:
- ListTemplate.ts
- ListTemplate.test.ts
import FullList from "../model/FullList";
interface DOMList {
ul: HTMLUListElement;
clear(): void;
render(fullList: FullList): void;
}
export default class ListTemplate implements DOMList {
ul: HTMLUListElement;
// Syntax 1 - Singleton - Dave Gray
static instance: ListTemplate = new ListTemplate();
// Syntax 2 - Singleton - Refactoring Guru
// private static instance: ListTemplate;
private constructor() {
console.log("-- ListTemplate constructor --");
// console.log(document);
console.log(document.body.innerHTML);
// console.log(document.body.outerHTML);
this.ul = document.getElementById("listItems") as HTMLUListElement;
console.log(this.ul);
}
// Syntax 2 - Singleton - Refactoring Guru
// public static getInstance(): ListTemplate {
// if (!ListTemplate.instance) {
// ListTemplate.instance = new ListTemplate();
// }
// return ListTemplate.instance;
// }
clear(): void {
this.ul.innerHTML = "";
}
render(fullList: FullList): void {
this.clear();
fullList.list.forEach((item) => {
const li = document.createElement("li") as HTMLLIElement;
li.className = "item";
// Checkbox
const check = document.createElement("input") as HTMLInputElement;
check.type = "checkbox";
check.id = item.id;
check.tabIndex = 0;
check.checked = item.checked;
li.append(check);
check.addEventListener("change", () => {
item.checked = !item.checked;
fullList.save();
});
// Label
const label = document.createElement("label") as HTMLLabelElement;
label.htmlFor = item.id;
label.textContent = item.item;
li.append(label);
// Button
const button = document.createElement("button") as HTMLButtonElement;
button.className = "button";
button.textContent = "X";
li.append(button);
button.addEventListener("click", () => {
fullList.removeItem(item.id);
this.render(fullList);
});
this.ul.append(li);
});
}
}
/**
* @jest-environment jsdom
*/
import FullList from "../../src/model/FullList";
import ListItem from "../../src/model/ListItem";
import ListTemplate from "../../src/templates/ListTemplate";
import { getAllByRole, getByText, waitFor } from "@testing-library/dom";
beforeEach(() => {
console.log("beforeEach global");
document.body.innerHTML = `<ul id="listItems"></ul>`;
});
describe("ListTemplate", () => {
console.log("ListTemplate");
beforeEach(() => {
console.log("beforeEach local ListTemplate");
// Doesn't change anything
// document.body.innerHTML = `<ul id="listItems"></ul>`;
});
test("should display html", async () => {
console.log("should display html");
// Create items
const item1 = new ListItem("1", "item1", true);
const item2 = new ListItem("2", "item2", true);
// Mocking the getter list of FullList to return mocked data
jest
.spyOn(FullList.prototype, "list", "get")
.mockReturnValue([item1, item2]);
// Syntax 1 - Singleton - Dave Gray - NOK - Reason ?
ListTemplate.instance.render(FullList.instance);
// Syntax 2 - Singleton - Refactoring Guru - OK
// const singletonInstance = ListTemplate.getInstance();
// singletonInstance.render(FullList.instance);
let ulElement: HTMLElement = document.getElementById(
"listItems"
) as HTMLElement;
expect(ulElement).toBeInTheDocument();
await waitFor(() => {
// Li
const li = getAllByRole(ulElement, "listitem");
// console.log(li);
expect(li).toHaveLength(2);
// Checkbox
const checkbox = getAllByRole(ulElement, "checkbox");
// console.log(checkbox);
expect(checkbox).toHaveLength(2);
checkbox.forEach((item) => {
expect(item).toBeChecked();
});
// Labels
expect(getByText(ulElement, "item1")).toBeInTheDocument();
expect(getByText(ulElement, "item2")).toBeInTheDocument();
// Buttons
const buttons = getAllByRole(ulElement, "button");
// console.log(buttons);
expect(buttons).toHaveLength(2);
});
});
});
Configuration
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@babel/preset-typescript": "^7.23.3",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.4.2",
"@types/jest": "^29.5.12",
"babel-jest": "^29.7.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jsdom": "^24.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.2.2",
"vite": "^5.1.4"
}
I thought this was related to the Setup and Teardown of Jest. However, with another related subject, after some tests, position of beforeEach doesn't change anything.
Expectations
The test should run without problem regardless of the syntax used.
I hope I'm clear on the context, the problem and the expectations. If not, I would be happy to edit the message and give you more information.
Thanks in advance for your time and your expertise on the subject.