The testing library that we developed for this project was centred around the following functions which check that output is expected:
function isEqual(expected, output, message = `you expected ${expected} and got ${output}`){
if (expected === output) {
console.info(`%cPASS: ${message}`, 'color:green; font-size:12px');
}else{
console.info(`%cFAIl: ${message}`, "color:darkred; font-size:12px");
}
};
function isDifferent(expected, output, message = `you expected ${expected} and got ${output}`){
if (expected !== output){
console.info(`%cPASS: ${message}`, "color:green; font-size:12px");
}else{
console.info(`%cFAIL: ${message}`, 'color:darkred; font-size:12px');
}
}
We end-to-end tested all of the major functionality of the web app by mimicking user interaction. Here is an example of one of the user interaction tests:
function typeToCreate() {
let expected = 'test task';
let emptyTask = document.querySelectorAll('.item__description')[0];
emptyTask.value = 'test task';
emptyTask.dispatchEvent(pressEnter);
let testInput = document.querySelectorAll('.item__description')[1];
testInput.classList.add('test-task');
taskCollection.getAllTasksFromStorage();
let output = testInput.value;
isEqual(expected, output, 'created a test task');
};
These tests were bundled into a function runTests() that would run them automatically at key points in the development cycle:
function runTests(){
let allStrings = [
'typing a new task adds it to the stored list',
'editing an existing item will change the associated task',
'typing "/done" marks a task as completed',
'typing "/pending" marks a task as pending',
'typing "/delete" removes a task',
'the number of tasks displayed matches the number of task objects in storage'
];
let allTests = [
typeToCreate,
typeToEdit,
typeToComplete,
typeToUncheck,
typeToDelete,
trackTaskListInStorage
];
for(let i = 0; i < allTests.length; i++){
setTimeout(() => test(allStrings[i], allTests[i]), i * 1000);
}
}
The app was designed from the beginning to be modular and testable. Two main JS classes stored state and logic for the 'tasks'. These were instantiated in the main app entry point and their methods called in response to user input or testing. Here is an example TaskCollection class and sample methods:
export class TaskCollection {
constructor() {
this.allTasks = [];
}
addTask(task) {
this.allTasks.push(task);
this.saveAllTasksToStorage();
}
deleteTask(index) {
this.allTasks.splice(index, 1);
this.saveAllTasksToStorage();
}
editTask(index, newDescription, newStatus) {
let currentTask = this.allTasks[index];
currentTask.description = newDescription;
currentTask.status = newStatus;
this.saveAllTasksToStorage();
}
DOM manipulation was a mainstay of this project. It was used to display tasks and control how they are styled. As an example, the following function allows the user to toggle between a light and dark theme which is controlled by a 'data-theme' HTML attribute on the root element:
function toggleTheme(event) {
const element = event.srcElement;
const bodyElement = document.querySelector("body");
const currentTheme = bodyElement.getAttribute("data-theme");
if (currentTheme === "light") {
element.innerHTML = "☀";
bodyElement.setAttribute("data-theme", "dark");
} else {
element.innerHTML = "☾";
bodyElement.setAttribute("data-theme", "light");
}
}
The result looked like this:
Event listeners were used extensively in the app. Interaction with each 'task' text box was handled with a "keyup" event listener. In addition, checkbox interaction was handled with a "change" event listener.
Scope was carefully managed. An example of this can be found in the class properties of tha Task class. The constructor received two arguments which were then stored as class properties, only accessible from the instantiated Task object:
export class Task {
constructor(description, status = TaskStatus.Pending) {
this.description = description;
this.status = status;
}
The
#canvas {
display: grid;
height: 90vh;
width: 100vw;
position: absolute;
top: 10%;
left: 0;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(auto-fill, 2rem);
padding: 1em 0;
background: linear-gradient(var(--color-base), var(--color-highlights));
}
A 'mobile first' design methodology was used. One example of this was through setting up page width primitives that were automatically overridden for smaller screen sizes. This was achieved by the following CSS...
/*
Mobile first page width setting
-------------------------------
Set all 'width-<size>' CSS classes to % of viewport width by default
e.g. .width-small, .width-medium, .width-large ... */
[class*="width-"] {
max-width: 90vw;
}
/*
Larger viewport page width overrides
------------------------------------
Override screen width mobile-first settings for larger viewports */
@media only screen and (min-width: 768px) {
.width-small {
max-width: 20rem;
}
.width-medium {
max-width: 40rem;
}
.width-large {
max-width: 60rem;
}
}