Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web/tests/e2e/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./navigation";
export * from "./pages";
export * from "./permissions";
export * from "./toast";
export * from "./todo";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export * from "./todo";
export * from "./todo";

43 changes: 43 additions & 0 deletions web/tests/e2e/helpers/todo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Page } from "@playwright/test";

export async function waitForTodoSheet(page: Page, timeout = 5000): Promise<void> {
await page.getByRole("heading", { name: "Todo Items" }).waitFor({ timeout });
}

export async function openTodoListViaUI(page: Page): Promise<void> {
await page.getByRole("button", { name: "Show todo list" }).click();
await waitForTodoSheet(page);
}

export async function openTodoListViaKeyboard(page: Page): Promise<void> {
await page.keyboard.press("Shift+T");
await page.keyboard.press("S");
await waitForTodoSheet(page);
}

export async function openTodoListViaCommandPalette(page: Page): Promise<void> {
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<void> {
// 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<void> {
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();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

3 changes: 3 additions & 0 deletions web/tests/e2e/sections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
73 changes: 73 additions & 0 deletions web/tests/e2e/sections/todo-create-form-section.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
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<void> {
const checkbox = this.page.getByRole("checkbox", { name: /create more/i });
const isChecked = await checkbox.isChecked();
if (checked !== isChecked) {
await checkbox.click();
}
}

async submit(): Promise<void> {
await this.form.submit("Add todo");
}

async createTodo(fields: {
title: string;
description?: string;
priority?: "normal" | "important" | "urgent" | "critical";
dueDate?: Date;
createMore?: boolean;
}): Promise<void> {
await this.fillTodoFields(fields);
if (fields.createMore) {
await this.setCreateMore(true);
}
await this.submit();
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

75 changes: 75 additions & 0 deletions web/tests/e2e/sections/todo-edit-form-section.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await waitForElementVisible(
this.page.getByRole("heading", { name: "Edit Todo" }),
options
);
}

async fillTodoFields(fields: {
title?: string;
description?: string;
priority?: "normal" | "important" | "urgent" | "critical";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have a TodoPriority type in the generated client that we could use here.

dueDate?: Date | null;
}): Promise<void> {
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<void> {
await this.form.submit("Update todo");
}

async updateTodo(fields: {
title?: string;
description?: string;
priority?: "normal" | "important" | "urgent" | "critical";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the TodoPriority type exists, we could use that here too.

dueDate?: Date | null;
}): Promise<void> {
await this.fillTodoFields(fields);
await this.submit();
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

53 changes: 53 additions & 0 deletions web/tests/e2e/sections/todo-section.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
await waitForElementVisible(
this.page.getByRole("heading", { name: "Todo Items" }),
options
);
}

getTodoSheet(): Locator {
return this.page.getByRole("dialog").filter({ hasText: "Todo Items" });
}

async isOpen(): Promise<boolean> {
return await this.getTodoSheet().isVisible();
}

async close(): Promise<void> {
await this.page.keyboard.press("Escape");
}

getAddTodoButton(): Locator {
return this.page.getByRole("button", { name: "Add Todo" });
}

async clickAddTodo(): Promise<void> {
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<number> {
return await this.getTodoItems().count();
}

getEmptyState(): Locator {
return this.getTodoSheet().getByText("No todos found");
}
}
88 changes: 88 additions & 0 deletions web/tests/e2e/todo-complete.spec.ts
Original file line number Diff line number Diff line change
@@ -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/);
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
});
});

Loading
Loading