210 lines
8.2 KiB
TypeScript
210 lines
8.2 KiB
TypeScript
import type { Page } from "@playwright/test";
|
|
import { test, expect } from "../../fixtures/test-base";
|
|
|
|
async function selectListOption(page: Page, testId: string, optionLabel: string) {
|
|
await page.getByTestId(testId).click();
|
|
await page.getByRole("listbox").getByRole("option", { name: optionLabel }).click();
|
|
}
|
|
|
|
async function taskRowTitles(page: Page): Promise<string[]> {
|
|
return page.getByTestId("tasks-list-row-title").allTextContents();
|
|
}
|
|
|
|
test.describe("Task List", () => {
|
|
test("seeded task appears in task list", async ({ testPage, apiClient, seedData }) => {
|
|
await apiClient.createTask(seedData.workspaceId, "Direct Navigate Task", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
|
|
// The /tasks page shows all tasks for the workspace in the list view (SSR)
|
|
await testPage.goto("/tasks");
|
|
await testPage.waitForLoadState("networkidle");
|
|
|
|
// The global AppSidebar also lists tasks (data-testid="sidebar-task-item"),
|
|
// so a bare getByText matches two elements. Scope to the page list.
|
|
await expect(
|
|
testPage.getByTestId("tasks-list").getByText("Direct Navigate Task"),
|
|
).toBeVisible();
|
|
await expect(testPage.getByTestId("kanban-header-search")).toBeVisible();
|
|
// The bar names the page through the shared title crumb.
|
|
await expect(testPage.locator('[data-slot="breadcrumb-page"]')).toHaveText("Tasks");
|
|
await expect(testPage.locator("main").getByRole("button", { name: "New Task" })).toHaveCount(0);
|
|
|
|
const rowBox = await testPage
|
|
.getByTestId("tasks-list-row")
|
|
.filter({ hasText: "Direct Navigate Task" })
|
|
.boundingBox();
|
|
expect(rowBox?.height).toBeLessThan(70);
|
|
});
|
|
|
|
test("subtasks render under their parent with indentation", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
const parent = await apiClient.createTask(seedData.workspaceId, "Hierarchy Parent Task", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
await apiClient.createTask(seedData.workspaceId, "Hierarchy Child Task", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
parent_id: parent.id,
|
|
});
|
|
|
|
await testPage.goto("/tasks");
|
|
await testPage.waitForLoadState("networkidle");
|
|
await testPage
|
|
.getByTestId("kanban-header-search")
|
|
.getByPlaceholder("Search tasks...")
|
|
.fill("Hierarchy");
|
|
|
|
const taskList = testPage.getByTestId("tasks-list");
|
|
const parentRow = taskList
|
|
.getByTestId("tasks-list-row-title")
|
|
.filter({ hasText: /^Hierarchy Parent Task$/ })
|
|
.locator("xpath=ancestor::*[@data-testid='tasks-list-row']");
|
|
const childRow = taskList
|
|
.getByTestId("tasks-list-row-title")
|
|
.filter({ hasText: /^Hierarchy Child Task$/ })
|
|
.locator("xpath=ancestor::*[@data-testid='tasks-list-row']");
|
|
|
|
// Wait for the filtered list to settle before reading row attributes. SSR
|
|
// and the client list can briefly overlap while the search result mounts.
|
|
await expect(parentRow).toHaveCount(1, { timeout: 5000 });
|
|
await expect(childRow).toHaveCount(1, { timeout: 5000 });
|
|
await expect(parentRow).toHaveAttribute("data-level", "0", { timeout: 5000 });
|
|
await expect(childRow).toHaveAttribute("data-level", "1", { timeout: 5000 });
|
|
await expect(taskList.getByTestId("tasks-list-row-title")).toHaveText([
|
|
"Hierarchy Parent Task",
|
|
"Hierarchy Child Task",
|
|
]);
|
|
});
|
|
|
|
test("sort and group options are deep-linkable and persisted", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
await apiClient.createTask(seedData.workspaceId, "Alpha sort task", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
await apiClient.createTask(seedData.workspaceId, "Zulu sort task", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
});
|
|
|
|
await testPage.goto("/tasks?sort=title_desc&group=none");
|
|
await testPage.waitForLoadState("networkidle");
|
|
await testPage
|
|
.getByTestId("kanban-header-search")
|
|
.getByPlaceholder("Search tasks...")
|
|
.fill("sort task");
|
|
|
|
await expect(testPage.getByTestId("tasks-list-sort")).toContainText("Title Z-A");
|
|
await expect(testPage.getByTestId("tasks-list-group")).toContainText("None");
|
|
await expect.poll(() => taskRowTitles(testPage)).toEqual(["Zulu sort task", "Alpha sort task"]);
|
|
|
|
await selectListOption(testPage, "tasks-list-sort", "Title A-Z");
|
|
await selectListOption(testPage, "tasks-list-group", "State");
|
|
|
|
await expect(testPage).toHaveURL((url) => {
|
|
return (
|
|
url.searchParams.get("sort") === "title_asc" && url.searchParams.get("group") === "state"
|
|
);
|
|
});
|
|
await expect.poll(() => taskRowTitles(testPage)).toEqual(["Alpha sort task", "Zulu sort task"]);
|
|
|
|
const settings = await apiClient.getUserSettings();
|
|
expect(settings.settings.tasks_list_sort).toBe("title_asc");
|
|
expect(settings.settings.tasks_list_group).toBe("state");
|
|
});
|
|
|
|
test("workflow grouping keeps duplicate workflow names separate", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
const workflowA = await apiClient.createWorkflow(seedData.workspaceId, "Duplicate Group");
|
|
const workflowB = await apiClient.createWorkflow(seedData.workspaceId, "Duplicate Group");
|
|
const stepA = await apiClient.createWorkflowStep(workflowA.id, "Start", 0, {
|
|
is_start_step: true,
|
|
});
|
|
const stepB = await apiClient.createWorkflowStep(workflowB.id, "Start", 0, {
|
|
is_start_step: true,
|
|
});
|
|
await apiClient.createTask(seedData.workspaceId, "Duplicate group Alpha", {
|
|
workflow_id: workflowA.id,
|
|
workflow_step_id: stepA.id,
|
|
});
|
|
await apiClient.createTask(seedData.workspaceId, "Duplicate group Beta", {
|
|
workflow_id: workflowB.id,
|
|
workflow_step_id: stepB.id,
|
|
});
|
|
await apiClient.saveUserSettings({
|
|
workspace_id: seedData.workspaceId,
|
|
workflow_filter_id: "",
|
|
});
|
|
|
|
await testPage.goto("/tasks?sort=title_asc&group=workflow");
|
|
await testPage.waitForLoadState("networkidle");
|
|
await testPage
|
|
.getByTestId("kanban-header-search")
|
|
.getByPlaceholder("Search tasks...")
|
|
.fill("Duplicate group");
|
|
|
|
const sections = testPage.getByTestId("tasks-list-section");
|
|
await expect(sections).toHaveCount(2);
|
|
await expect(sections.nth(0).getByTestId("tasks-list-row")).toHaveCount(1);
|
|
await expect(sections.nth(1).getByTestId("tasks-list-row")).toHaveCount(1);
|
|
await expect
|
|
.poll(() => taskRowTitles(testPage))
|
|
.toEqual(["Duplicate group Alpha", "Duplicate group Beta"]);
|
|
});
|
|
|
|
test("pagination shows totals, rows per page, and page numbers", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
await Promise.all(
|
|
Array.from({ length: 26 }, (_, index) =>
|
|
apiClient.createTask(
|
|
seedData.workspaceId,
|
|
`Paged list task ${String(index + 1).padStart(2, "0")}`,
|
|
{
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
},
|
|
),
|
|
),
|
|
);
|
|
|
|
await testPage.goto("/tasks?sort=title_asc&group=none");
|
|
await testPage.waitForLoadState("networkidle");
|
|
await testPage
|
|
.getByTestId("kanban-header-search")
|
|
.getByPlaceholder("Search tasks...")
|
|
.fill("Paged list task");
|
|
|
|
const pagination = testPage.getByTestId("tasks-pagination");
|
|
await expect(pagination).toContainText("Showing 1 to 25 of 26 results");
|
|
await expect(testPage.getByTestId("tasks-pagination-page-size")).toContainText("25");
|
|
await expect(pagination.getByRole("button", { name: "1", exact: true })).toHaveAttribute(
|
|
"aria-current",
|
|
"page",
|
|
);
|
|
|
|
await pagination.getByRole("button", { name: "Go to next page" }).click();
|
|
await expect(pagination).toContainText("Showing 26 to 26 of 26 results");
|
|
await expect.poll(() => taskRowTitles(testPage)).toEqual(["Paged list task 26"]);
|
|
|
|
await testPage.getByTestId("tasks-pagination-page-size").click();
|
|
await testPage.getByRole("listbox").getByRole("option", { name: "10" }).click();
|
|
await expect(pagination).toContainText("Showing 1 to 10 of 26 results");
|
|
await expect(testPage.getByTestId("tasks-pagination-page-size")).toContainText("10");
|
|
});
|
|
});
|