diff --git a/frontend/tests-examples/demo-todo-app.spec.ts b/frontend/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 00000000..c06e8461 --- /dev/null +++ b/frontend/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,489 @@ +import { test, expect, type Page } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("https://demo.playwright.dev/todomvc"); +}); + +const TODO_ITEMS = [ + "buy some cheese", + "feed the cat", + "book a doctors appointment", +]; + +test.describe("New Todo", () => { + test("should allow me to add todo items", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press("Enter"); + + // Make sure the list only has one todo item. + await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0]]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press("Enter"); + + // Make sure the list now has two todo items. + await expect(page.getByTestId("todo-title")).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[1], + ]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test("should clear text input field when an item is added", async ({ + page, + }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press("Enter"); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test("should append new items to the bottom of the list", async ({ + page, + }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId("todo-count"); + + // Check test using different methods. + await expect(page.getByText("3 items left")).toBeVisible(); + await expect(todoCount).toHaveText("3 items left"); + await expect(todoCount).toContainText("3"); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId("todo-title")).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe("Mark all as completed", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test("should allow me to mark all items as completed", async ({ page }) => { + // Complete all todos. + await page.getByLabel("Mark all as complete").check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId("todo-item")).toHaveClass([ + "completed", + "completed", + "completed", + ]); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test("should allow me to clear the complete state of all items", async ({ + page, + }) => { + const toggleAll = page.getByLabel("Mark all as complete"); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId("todo-item")).toHaveClass(["", "", ""]); + }); + + test("complete all checkbox should update state when items are completed / cleared", async ({ + page, + }) => { + const toggleAll = page.getByLabel("Mark all as complete"); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId("todo-item").nth(0); + await firstTodo.getByRole("checkbox").uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole("checkbox").check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe("Item", () => { + test("should allow me to mark items as complete", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } + + // Check first item. + const firstTodo = page.getByTestId("todo-item").nth(0); + await firstTodo.getByRole("checkbox").check(); + await expect(firstTodo).toHaveClass("completed"); + + // Check second item. + const secondTodo = page.getByTestId("todo-item").nth(1); + await expect(secondTodo).not.toHaveClass("completed"); + await secondTodo.getByRole("checkbox").check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass("completed"); + await expect(secondTodo).toHaveClass("completed"); + }); + + test("should allow me to un-mark items as complete", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } + + const firstTodo = page.getByTestId("todo-item").nth(0); + const secondTodo = page.getByTestId("todo-item").nth(1); + const firstTodoCheckbox = firstTodo.getByRole("checkbox"); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass("completed"); + await expect(secondTodo).not.toHaveClass("completed"); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass("completed"); + await expect(secondTodo).not.toHaveClass("completed"); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test("should allow me to edit an item", async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId("todo-item"); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole("textbox", { name: "Edit" })).toHaveValue( + TODO_ITEMS[1] + ); + await secondTodo + .getByRole("textbox", { name: "Edit" }) + .fill("buy some sausages"); + await secondTodo.getByRole("textbox", { name: "Edit" }).press("Enter"); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + "buy some sausages", + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, "buy some sausages"); + }); +}); + +test.describe("Editing", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test("should hide other controls when editing", async ({ page }) => { + const todoItem = page.getByTestId("todo-item").nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole("checkbox")).not.toBeVisible(); + await expect( + todoItem.locator("label", { + hasText: TODO_ITEMS[1], + }) + ).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test("should save edits on blur", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems + .nth(1) + .getByRole("textbox", { name: "Edit" }) + .fill("buy some sausages"); + await todoItems + .nth(1) + .getByRole("textbox", { name: "Edit" }) + .dispatchEvent("blur"); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + "buy some sausages", + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, "buy some sausages"); + }); + + test("should trim entered text", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems + .nth(1) + .getByRole("textbox", { name: "Edit" }) + .fill(" buy some sausages "); + await todoItems + .nth(1) + .getByRole("textbox", { name: "Edit" }) + .press("Enter"); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + "buy some sausages", + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, "buy some sausages"); + }); + + test("should remove the item if an empty text string was entered", async ({ + page, + }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill(""); + await todoItems + .nth(1) + .getByRole("textbox", { name: "Edit" }) + .press("Enter"); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test("should cancel edits on escape", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems + .nth(1) + .getByRole("textbox", { name: "Edit" }) + .fill("buy some sausages"); + await todoItems + .nth(1) + .getByRole("textbox", { name: "Edit" }) + .press("Escape"); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe("Counter", () => { + test("should display the current number of todo items", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // create a todo count locator + const todoCount = page.getByTestId("todo-count"); + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press("Enter"); + + await expect(todoCount).toContainText("1"); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press("Enter"); + await expect(todoCount).toContainText("2"); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe("Clear completed button", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test("should display the correct text", async ({ page }) => { + await page.locator(".todo-list li .toggle").first().check(); + await expect( + page.getByRole("button", { name: "Clear completed" }) + ).toBeVisible(); + }); + + test("should remove completed items when clicked", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).getByRole("checkbox").check(); + await page.getByRole("button", { name: "Clear completed" }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test("should be hidden when there are no items that are completed", async ({ + page, + }) => { + await page.locator(".todo-list li .toggle").first().check(); + await page.getByRole("button", { name: "Clear completed" }).click(); + await expect( + page.getByRole("button", { name: "Clear completed" }) + ).toBeHidden(); + }); +}); + +test.describe("Persistence", () => { + test("should persist its data", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } + + const todoItems = page.getByTestId("todo-item"); + const firstTodoCheck = todoItems.nth(0).getByRole("checkbox"); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(["completed", ""]); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(["completed", ""]); + }); +}); + +test.describe("Routing", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test("should allow me to display active items", async ({ page }) => { + const todoItem = page.getByTestId("todo-item"); + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole("link", { name: "Active" }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test("should respect the back button", async ({ page }) => { + const todoItem = page.getByTestId("todo-item"); + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step("Showing all items", async () => { + await page.getByRole("link", { name: "All" }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step("Showing active items", async () => { + await page.getByRole("link", { name: "Active" }).click(); + }); + + await test.step("Showing completed items", async () => { + await page.getByRole("link", { name: "Completed" }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test("should allow me to display completed items", async ({ page }) => { + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole("link", { name: "Completed" }).click(); + await expect(page.getByTestId("todo-item")).toHaveCount(1); + }); + + test("should allow me to display all items", async ({ page }) => { + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole("link", { name: "Active" }).click(); + await page.getByRole("link", { name: "Completed" }).click(); + await page.getByRole("link", { name: "All" }).click(); + await expect(page.getByTestId("todo-item")).toHaveCount(3); + }); + + test("should highlight the currently applied filter", async ({ page }) => { + await expect(page.getByRole("link", { name: "All" })).toHaveClass( + "selected" + ); + + //create locators for active and completed links + const activeLink = page.getByRole("link", { name: "Active" }); + const completedLink = page.getByRole("link", { name: "Completed" }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass("selected"); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass("selected"); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return JSON.parse(localStorage["react-todos"]).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage( + page: Page, + expected: number +) { + return await page.waitForFunction((e) => { + return ( + JSON.parse(localStorage["react-todos"]).filter( + (todo: any) => todo.completed + ).length === e + ); + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction((t) => { + return JSON.parse(localStorage["react-todos"]) + .map((todo: any) => todo.title) + .includes(t); + }, title); +}