diff --git a/web/tests/e2e/helpers/index.ts b/web/tests/e2e/helpers/index.ts index 081e2d01..74f85866 100644 --- a/web/tests/e2e/helpers/index.ts +++ b/web/tests/e2e/helpers/index.ts @@ -7,3 +7,4 @@ export * from "./navigation"; export * from "./pages"; export * from "./permissions"; export * from "./toast"; +export * from "./todo"; \ No newline at end of file diff --git a/web/tests/e2e/helpers/todo.ts b/web/tests/e2e/helpers/todo.ts new file mode 100644 index 00000000..409261e4 --- /dev/null +++ b/web/tests/e2e/helpers/todo.ts @@ -0,0 +1,43 @@ +import type { Page } from "@playwright/test"; + +export async function waitForTodoSheet(page: Page, timeout = 5000): Promise { + await page.getByRole("heading", { name: "Todo Items" }).waitFor({ timeout }); +} + +export async function openTodoListViaUI(page: Page): Promise { + await page.getByRole("button", { name: "Show todo list" }).click(); + await waitForTodoSheet(page); +} + +export async function openTodoListViaKeyboard(page: Page): Promise { + await page.keyboard.press("Shift+T"); + await page.keyboard.press("S"); + await waitForTodoSheet(page); +} + +export async function openTodoListViaCommandPalette(page: Page): Promise { + const isMac = process.platform === "darwin"; + const modifier = isMac ? "Meta" : "Control"; + + await page.keyboard.press(`${modifier}+K`); + await page.getByPlaceholder(/Type a command/i).waitFor(); + await page.getByRole("option", { name: /Show Todos/i }).click(); + await waitForTodoSheet(page); +} + +export async function openAddTodoFormViaKeyboard(page: Page): Promise { + // Shift + T + N + await page.keyboard.press("Shift+T"); + await page.keyboard.press("N"); + await page.getByRole("heading", { name: "Add Todo" }).waitFor(); +} + +export async function openAddTodoFormViaCommandPalette(page: Page): Promise { + const isMac = process.platform === "darwin"; + const modifier = isMac ? "Meta" : "Control"; + + await page.keyboard.press(`${modifier}+K`); + await page.getByPlaceholder(/Type a command/i).waitFor(); + await page.getByRole("option", { name: /Add Todo/i }).click(); + await page.getByRole("heading", { name: "Add Todo" }).waitFor(); +} \ No newline at end of file diff --git a/web/tests/e2e/sections/index.ts b/web/tests/e2e/sections/index.ts index 1d2a3360..dcc884e7 100644 --- a/web/tests/e2e/sections/index.ts +++ b/web/tests/e2e/sections/index.ts @@ -13,3 +13,6 @@ export { RoleMembersSection } from "./role-members-section"; export { RolePermissionDraftSection } from "./role-permission-draft-section"; export { RolePermissionsSection } from "./role-permissions-section"; export { LoginSection } from "./login-section"; +export { TodoSection } from "./todo-section"; +export { TodoCreateFormSection } from "./todo-create-form-section"; +export { TodoEditFormSection} from './todo-edit-form-section' diff --git a/web/tests/e2e/sections/todo-create-form-section.ts b/web/tests/e2e/sections/todo-create-form-section.ts new file mode 100644 index 00000000..f699eddf --- /dev/null +++ b/web/tests/e2e/sections/todo-create-form-section.ts @@ -0,0 +1,73 @@ +import type { Page } from "@playwright/test"; + +import { Form } from "../components"; +import { BaseComponent } from "../components/base"; +import { waitForElementVisible } from "../helpers"; + +export class TodoCreateFormSection extends BaseComponent { + private form: Form; + + constructor(page: Page) { + super(page); + this.form = new Form(page); + } + + async waitForLoad(options?: { timeout?: number }): Promise { + await waitForElementVisible( + this.page.getByRole("heading", { name: "Add Todo" }), + options + ); + } + + async fillTodoFields(fields: { + title: string; + description?: string; + priority?: "normal" | "important" | "urgent" | "critical"; + dueDate?: Date; + }): Promise { + await this.form.fillFields({ Title: fields.title }); + + if (fields.description) { + await this.page.getByPlaceholder(/Enter todo description/i).fill(fields.description); + } + + if (fields.priority) { + await this.page.getByRole("combobox", { name: /priority/i }).click(); + await this.page.getByRole("option", { name: fields.priority, exact: true }).click(); + } + + if (fields.dueDate) { + const dateButton = this.page.getByRole("button", { name: /due date/i }); + await dateButton.click(); + // Select date from calendar picker + const formattedDate = fields.dueDate.getDate().toString(); + await this.page.getByRole("gridcell", { name: formattedDate, exact: true }).click(); + } + } + + async setCreateMore(checked: boolean): Promise { + const checkbox = this.page.getByRole("checkbox", { name: /create more/i }); + const isChecked = await checkbox.isChecked(); + if (checked !== isChecked) { + await checkbox.click(); + } + } + + async submit(): Promise { + await this.form.submit("Add todo"); + } + + async createTodo(fields: { + title: string; + description?: string; + priority?: "normal" | "important" | "urgent" | "critical"; + dueDate?: Date; + createMore?: boolean; + }): Promise { + await this.fillTodoFields(fields); + if (fields.createMore) { + await this.setCreateMore(true); + } + await this.submit(); + } +} \ No newline at end of file diff --git a/web/tests/e2e/sections/todo-edit-form-section.ts b/web/tests/e2e/sections/todo-edit-form-section.ts new file mode 100644 index 00000000..83fd20bf --- /dev/null +++ b/web/tests/e2e/sections/todo-edit-form-section.ts @@ -0,0 +1,75 @@ +import type { Page } from "@playwright/test"; + +import { Form } from "../components"; +import { BaseComponent } from "../components/base"; +import { waitForElementVisible } from "../helpers"; + +export class TodoEditFormSection extends BaseComponent { + private form: Form; + + constructor(page: Page) { + super(page); + this.form = new Form(page); + } + + async waitForLoad(options?: { timeout?: number }): Promise { + await waitForElementVisible( + this.page.getByRole("heading", { name: "Edit Todo" }), + options + ); + } + + async fillTodoFields(fields: { + title?: string; + description?: string; + priority?: "normal" | "important" | "urgent" | "critical"; + dueDate?: Date | null; + }): Promise { + if (fields.title !== undefined) { + const titleInput = this.page.getByLabel(/title/i); + await titleInput.clear(); + await titleInput.fill(fields.title); + } + + if (fields.description !== undefined) { + const descInput = this.page.getByPlaceholder(/Enter todo description/i); + await descInput.clear(); + await descInput.fill(fields.description); + } + + if (fields.priority) { + await this.page.getByRole("combobox", { name: /priority/i }).click(); + await this.page.getByRole("option", { name: fields.priority, exact: true }).click(); + } + + if (fields.dueDate !== undefined) { + if (fields.dueDate === null) { + // Clear date + const dateButton = this.page.getByRole("button", { name: /due date/i }); + const clearButton = dateButton.locator('[aria-label="Clear"]'); + if (await clearButton.isVisible()) { + await clearButton.click(); + } + } else { + const dateButton = this.page.getByRole("button", { name: /due date/i }); + await dateButton.click(); + const formattedDate = fields.dueDate.getDate().toString(); + await this.page.getByRole("gridcell", { name: formattedDate, exact: true }).click(); + } + } + } + + async submit(): Promise { + await this.form.submit("Update todo"); + } + + async updateTodo(fields: { + title?: string; + description?: string; + priority?: "normal" | "important" | "urgent" | "critical"; + dueDate?: Date | null; + }): Promise { + await this.fillTodoFields(fields); + await this.submit(); + } +} \ No newline at end of file diff --git a/web/tests/e2e/sections/todo-section.ts b/web/tests/e2e/sections/todo-section.ts new file mode 100644 index 00000000..b52cd46a --- /dev/null +++ b/web/tests/e2e/sections/todo-section.ts @@ -0,0 +1,53 @@ +import type { Locator, Page } from "@playwright/test"; + +import { BaseComponent } from "../components/base"; +import { waitForElementVisible } from "../helpers"; + +export class TodoSection extends BaseComponent { + constructor(page: Page) { + super(page); + } + + async waitForLoad(options?: { timeout?: number }): Promise { + await waitForElementVisible( + this.page.getByRole("heading", { name: "Todo Items" }), + options + ); + } + + getTodoSheet(): Locator { + return this.page.getByRole("dialog").filter({ hasText: "Todo Items" }); + } + + async isOpen(): Promise { + return await this.getTodoSheet().isVisible(); + } + + async close(): Promise { + await this.page.keyboard.press("Escape"); + } + + getAddTodoButton(): Locator { + return this.page.getByRole("button", { name: "Add Todo" }); + } + + async clickAddTodo(): Promise { + await this.getAddTodoButton().click(); + } + + getTodoItems(): Locator { + return this.getTodoSheet().locator('[class*="group"]').filter({ hasText: /^(?!No todos found)/ }); + } + + getTodoByTitle(title: string): Locator { + return this.getTodoSheet().locator('[class*="group"]').filter({ hasText: title }).first(); + } + + async getTodoCount(): Promise { + return await this.getTodoItems().count(); + } + + getEmptyState(): Locator { + return this.getTodoSheet().getByText("No todos found"); + } +} diff --git a/web/tests/e2e/todo-complete.spec.ts b/web/tests/e2e/todo-complete.spec.ts new file mode 100644 index 00000000..87fe86a4 --- /dev/null +++ b/web/tests/e2e/todo-complete.spec.ts @@ -0,0 +1,88 @@ +import { expect, test } from "./fixtures"; +import { waitForSuccessToast } from "./helpers"; +import { openTodoListViaUI } from "./helpers/todo"; +import { TodoCreateFormSection } from "./sections/todo-create-form-section"; +import { TodoSection } from "./sections/todo-section"; +import { USER_DEFAULT_PASSWORD, loginUser } from "./utils/auth"; +import { createUser } from "./utils/db"; +import { generateTitleOnly } from "./utils/todo"; + +test.describe("@todo.complete TODO Complete/Uncomplete E2E Tests", () => { + test("should mark TODO as complete", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateTitleOnly(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Mark as complete").click(); + + await waitForSuccessToast(page, "marked as completed"); + expect(await todoItem.getByText("Completed").isVisible()).toBe(true); + }); + + test("should mark completed TODO as incomplete", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateTitleOnly(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Mark as complete").click(); + await waitForSuccessToast(page, "marked as completed"); + + await todoItem.hover(); + await todoItem.getByTitle("Mark as incomplete").click(); + + await waitForSuccessToast(page, "marked as incomplete"); + expect(await todoItem.getByText("Completed").isVisible()).toBe(false); + }); + + test("should apply completed styling to completed TODO", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateTitleOnly(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Mark as complete").click(); + await waitForSuccessToast(page, "marked as completed"); + + // Check for strikethrough on title + const title = todoItem.locator("h4").first(); + await expect(title).toHaveClass(/line-through/); + + // Check for reduced opacity on todo item + await expect(todoItem).toHaveClass(/opacity-75/); + }); +}); \ No newline at end of file diff --git a/web/tests/e2e/todo-create-spec.ts b/web/tests/e2e/todo-create-spec.ts new file mode 100644 index 00000000..b63a833b --- /dev/null +++ b/web/tests/e2e/todo-create-spec.ts @@ -0,0 +1,140 @@ +import { expect, test } from "./fixtures"; +import { waitForSuccessToast } from "./helpers"; +import { openAddTodoFormViaCommandPalette, openAddTodoFormViaKeyboard, openTodoListViaUI } from "./helpers/todo"; +import { TodoCreateFormSection } from "./sections/todo-create-form-section"; +import { TodoSection } from "./sections/todo-section"; +import { USER_DEFAULT_PASSWORD, loginUser } from "./utils/auth"; +import { createUser } from "./utils/db"; +import { generateComplete, generateTitleOnly, generateWithDescription, generateWithPriority } from "./utils/todo"; + +test.describe("@todo.create TODO Create E2E Tests", () => { + test("should create TODO with title only via UI button", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateTitleOnly(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + + await waitForSuccessToast(page, "Todo added successfully"); + expect(await todoSection.getTodoByTitle(todoData.title).isVisible()).toBe(true); + }); + + test("should create TODO with title and description", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateWithDescription(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + + await waitForSuccessToast(page, "Todo added successfully"); + const todoItem = todoSection.getTodoByTitle(todoData.title); + expect(await todoItem.isVisible()).toBe(true); + expect(await todoItem.getByText(todoData.description!).isVisible()).toBe(true); + }); + + test("should create TODO with title and priority", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateWithPriority("urgent"); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + + await waitForSuccessToast(page, "Todo added successfully"); + const todoItem = todoSection.getTodoByTitle(todoData.title); + expect(await todoItem.isVisible()).toBe(true); + expect(await todoItem.getByText("urgent").isVisible()).toBe(true); + }); + + test("should create TODO with all fields", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateComplete(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + + await waitForSuccessToast(page, "Todo added successfully"); + const todoItem = todoSection.getTodoByTitle(todoData.title); + expect(await todoItem.isVisible()).toBe(true); + }); + + test("should create TODO via keyboard shortcut (Shift+T+N)", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const createForm = new TodoCreateFormSection(page); + const todoData = generateTitleOnly(); + + await openAddTodoFormViaKeyboard(page); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + + await waitForSuccessToast(page, "Todo added successfully"); + }); + + test("should create TODO via command palette", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const createForm = new TodoCreateFormSection(page); + const todoData = generateTitleOnly(); + + await openAddTodoFormViaCommandPalette(page); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + + await waitForSuccessToast(page, "Todo added successfully"); + }); + + test("should create multiple TODOs with 'Create More' checkbox", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todo1 = generateTitleOnly(); + const todo2 = generateTitleOnly(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo({ ...todo1, createMore: true }); + + await waitForSuccessToast(page, "Todo added successfully"); + + // Form should still be open + await createForm.waitForLoad(); + await createForm.createTodo(todo2); + + await waitForSuccessToast(page, "Todo added successfully"); + expect(await todoSection.getTodoCount()).toBeGreaterThanOrEqual(2); + }); +}); \ No newline at end of file diff --git a/web/tests/e2e/todo-delete.spec.ts b/web/tests/e2e/todo-delete.spec.ts new file mode 100644 index 00000000..a58fb615 --- /dev/null +++ b/web/tests/e2e/todo-delete.spec.ts @@ -0,0 +1,83 @@ +import { expect, test } from "./fixtures"; +import { waitForSuccessToast } from "./helpers"; +import { openTodoListViaUI } from "./helpers/todo"; +import { TodoCreateFormSection } from "./sections/todo-create-form-section"; +import { TodoSection } from "./sections/todo-section"; +import { USER_DEFAULT_PASSWORD, loginUser } from "./utils/auth"; +import { createUser } from "./utils/db"; +import { generateTitleOnly } from "./utils/todo"; + +test.describe("@todo.delete TODO Delete E2E Tests", () => { + test("should delete TODO item", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateTitleOnly(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Delete todo").click(); + + await waitForSuccessToast(page, "Todo deleted"); + expect(await todoItem.isVisible()).toBe(false); + }); + + test("should delete completed TODO item", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateTitleOnly(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Mark as complete").click(); + await waitForSuccessToast(page, "marked as completed"); + + await todoItem.hover(); + await todoItem.getByTitle("Delete todo").click(); + + await waitForSuccessToast(page, "Todo deleted"); + expect(await todoItem.isVisible()).toBe(false); + }); + + test("should show empty state after deleting all TODOs", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateTitleOnly(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Delete todo").click(); + await waitForSuccessToast(page, "Todo deleted"); + + expect(await todoSection.getEmptyState().isVisible()).toBe(true); + }); +}); diff --git a/web/tests/e2e/todo-open-close.spec.ts b/web/tests/e2e/todo-open-close.spec.ts new file mode 100644 index 00000000..0be5a191 --- /dev/null +++ b/web/tests/e2e/todo-open-close.spec.ts @@ -0,0 +1,56 @@ +import { expect, test } from "./fixtures"; +import {openTodoListViaCommandPalette, openTodoListViaKeyboard, openTodoListViaUI, } from "./helpers/todo"; +import { TodoSection } from "./sections/todo-section"; +import { USER_DEFAULT_PASSWORD, loginUser } from "./utils/auth"; +import { createUser } from "./utils/db"; + +test.describe("@todo.open-close TODO List Open/Close E2E Tests", () => { + test("should open TODO list via UI button click", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + + expect(await todoSection.isOpen()).toBe(true); + }); + + test("should close TODO list via Escape key", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.close(); + + expect(await todoSection.isOpen()).toBe(false); + }); + + test("should open TODO list via keyboard shortcut (Shift+T+S)", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + + await openTodoListViaKeyboard(page); + await todoSection.waitForLoad(); + + expect(await todoSection.isOpen()).toBe(true); + }); + + test("should open TODO list via command palette", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + + await openTodoListViaCommandPalette(page); + await todoSection.waitForLoad(); + + expect(await todoSection.isOpen()).toBe(true); + }); +}); \ No newline at end of file diff --git a/web/tests/e2e/todo-update.spec.ts b/web/tests/e2e/todo-update.spec.ts new file mode 100644 index 00000000..a5a43818 --- /dev/null +++ b/web/tests/e2e/todo-update.spec.ts @@ -0,0 +1,159 @@ +import { expect, test } from "./fixtures"; +import { waitForSuccessToast } from "./helpers"; +import { openTodoListViaUI } from "./helpers/todo"; +import { TodoCreateFormSection } from "./sections/todo-create-form-section"; +import { TodoEditFormSection } from "./sections/todo-edit-form-section"; +import { TodoSection } from "./sections/todo-section"; +import { USER_DEFAULT_PASSWORD, loginUser } from "./utils/auth"; +import { createUser } from "./utils/db"; +import { generateComplete } from "./utils/todo"; + +test.describe("@todo.update TODO Update E2E Tests", () => { + test("should update TODO title", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const editForm = new TodoEditFormSection(page); + const todoData = generateComplete(); + + // Create a TODO first + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + // Update the title + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Edit todo").click(); + await editForm.waitForLoad(); + + const newTitle = `Updated ${todoData.title}`; + await editForm.updateTodo({ title: newTitle }); + + await waitForSuccessToast(page, "Todo updated successfully"); + expect(await todoSection.getTodoByTitle(newTitle).isVisible()).toBe(true); + }); + + test("should update TODO description", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const editForm = new TodoEditFormSection(page); + const todoData = generateComplete(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Edit todo").click(); + await editForm.waitForLoad(); + + const newDescription = "Updated description"; + await editForm.updateTodo({ description: newDescription }); + + await waitForSuccessToast(page, "Todo updated successfully"); + expect(await todoItem.getByText(newDescription).isVisible()).toBe(true); + }); + + test("should update TODO priority", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const editForm = new TodoEditFormSection(page); + const todoData = generateComplete(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Edit todo").click(); + await editForm.waitForLoad(); + + await editForm.updateTodo({ priority: "critical" }); + + await waitForSuccessToast(page, "Todo updated successfully"); + expect(await todoItem.getByText("critical").isVisible()).toBe(true); + }); + + test("should update multiple fields simultaneously", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const editForm = new TodoEditFormSection(page); + const todoData = generateComplete(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Edit todo").click(); + await editForm.waitForLoad(); + + const newTitle = `Multi-update ${todoData.title}`; + const newDescription = "Multi-field update description"; + await editForm.updateTodo({ + title: newTitle, + description: newDescription, + priority: "urgent" + }); + + await waitForSuccessToast(page, "Todo updated successfully"); + const updatedItem = todoSection.getTodoByTitle(newTitle); + expect(await updatedItem.isVisible()).toBe(true); + expect(await updatedItem.getByText(newDescription).isVisible()).toBe(true); + expect(await updatedItem.getByText("urgent").isVisible()).toBe(true); + }); + + test("should not allow editing completed TODOs", async ({ page, testConfig }) => { + const testUser = await createUser(testConfig); + await loginUser(page, { email: testUser.email, password: USER_DEFAULT_PASSWORD }); + + const todoSection = new TodoSection(page); + const createForm = new TodoCreateFormSection(page); + const todoData = generateComplete(); + + await openTodoListViaUI(page); + await todoSection.waitForLoad(); + await todoSection.clickAddTodo(); + await createForm.waitForLoad(); + await createForm.createTodo(todoData); + await waitForSuccessToast(page, "Todo added successfully"); + + // Complete the TODO + const todoItem = todoSection.getTodoByTitle(todoData.title); + await todoItem.hover(); + await todoItem.getByTitle("Mark as complete").click(); + await waitForSuccessToast(page, "Todo updated"); + + // Try to edit - button should be disabled + await todoItem.hover(); + const editButton = todoItem.getByTitle("Edit todo"); + expect(await editButton.isDisabled()).toBe(true); + }); +}); \ No newline at end of file diff --git a/web/tests/e2e/utils/todo.ts b/web/tests/e2e/utils/todo.ts new file mode 100644 index 00000000..d4d1cb48 --- /dev/null +++ b/web/tests/e2e/utils/todo.ts @@ -0,0 +1,53 @@ +import { getRandomString } from "./random"; + +export interface TodoTestData { + title: string; + description?: string; + priority?: "normal" | "important" | "urgent" | "critical"; + dueDate?: Date; +} + +export function generateTodoData(overrides?: Partial): TodoTestData { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + + return { + title: `Test TODO ${getRandomString(8)}`, + description: `Description for test TODO ${getRandomString(8)}`, + priority: "normal", + dueDate: tomorrow, + ...overrides, + }; +} + +export function generateTitleOnly(): TodoTestData { + return { + title: `TODO ${getRandomString(8)}`, + }; +} + +export function generateWithDescription(): TodoTestData { + return { + title: `TODO ${getRandomString(8)}`, + description: `Description ${getRandomString(12)}`, + }; +} + +export function generateWithPriority(priority: "normal" | "important" | "urgent" | "critical"): TodoTestData { + return { + title: `TODO ${getRandomString(8)}`, + priority, + }; +} + +export function generateComplete(): TodoTestData { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + + return { + title: `TODO ${getRandomString(8)}`, + description: `Description ${getRandomString(12)}`, + priority: "important", + dueDate: tomorrow, + }; +} \ No newline at end of file