test(frontend): add TaskBoard + TaskCard coverage and full coverage config
This commit is contained in:
committed by
Abhimanyu Saharan
parent
d4f9831ecb
commit
6c897d7faf
@@ -8,6 +8,7 @@
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"test": "vitest run --passWithNoTests --coverage",
|
||||
"test:full-coverage": "vitest run --passWithNoTests --coverage --config ./vitest.full-coverage.config.ts",
|
||||
"test:watch": "vitest",
|
||||
"dev:lan": "next dev --hostname 0.0.0.0 --port 3000",
|
||||
"api:gen": "orval --config ./orval.config.ts",
|
||||
|
||||
67
frontend/src/components/molecules/TaskCard.test.tsx
Normal file
67
frontend/src/components/molecules/TaskCard.test.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { TaskCard } from "./TaskCard";
|
||||
|
||||
describe("TaskCard", () => {
|
||||
it("renders title, assignee, and due date", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
title="Fix flaky test"
|
||||
assignee="Zara"
|
||||
due="Feb 11"
|
||||
priority="high"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Fix flaky test")).toBeInTheDocument();
|
||||
expect(screen.getByText("Zara")).toBeInTheDocument();
|
||||
expect(screen.getByText("Feb 11")).toBeInTheDocument();
|
||||
expect(screen.getByText("HIGH")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows blocked state with count", () => {
|
||||
render(
|
||||
<TaskCard title="Blocked task" isBlocked blockedByCount={2} priority="low" />,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Blocked · 2/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows approvals pending indicator", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
title="Needs approval"
|
||||
approvalsPendingCount={3}
|
||||
priority="medium"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Approval needed · 3/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows lead review indicator when status is review with no approvals and not blocked", () => {
|
||||
render(<TaskCard title="Waiting" status="review" approvalsPendingCount={0} />);
|
||||
|
||||
expect(screen.getByText(/Waiting for lead review/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("invokes onClick for mouse and keyboard activation", () => {
|
||||
const onClick = vi.fn();
|
||||
|
||||
render(<TaskCard title="Clickable" onClick={onClick} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Clickable/i }));
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
|
||||
fireEvent.keyDown(screen.getByRole("button", { name: /Clickable/i }), {
|
||||
key: "Enter",
|
||||
});
|
||||
expect(onClick).toHaveBeenCalledTimes(2);
|
||||
|
||||
fireEvent.keyDown(screen.getByRole("button", { name: /Clickable/i }), {
|
||||
key: " ",
|
||||
});
|
||||
expect(onClick).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,31 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { TaskBoard } from "./TaskBoard";
|
||||
|
||||
type TaskStatus = "inbox" | "in_progress" | "review" | "done";
|
||||
|
||||
type Task = {
|
||||
id: string;
|
||||
title: string;
|
||||
status: TaskStatus;
|
||||
priority: string;
|
||||
approvals_pending_count?: number;
|
||||
blocked_by_task_ids?: string[];
|
||||
is_blocked?: boolean;
|
||||
};
|
||||
|
||||
const buildTask = (overrides: Partial<Task> = {}): Task => ({
|
||||
id: `task-${Math.random().toString(16).slice(2)}`,
|
||||
title: "Task",
|
||||
status: "inbox",
|
||||
priority: "medium",
|
||||
approvals_pending_count: 0,
|
||||
blocked_by_task_ids: [],
|
||||
is_blocked: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("TaskBoard", () => {
|
||||
it("uses a mobile-first stacked layout (no horizontal scroll) with responsive kanban columns on larger screens", () => {
|
||||
render(
|
||||
@@ -48,4 +71,130 @@ describe("TaskBoard", () => {
|
||||
// Ensure we didn't accidentally keep unscoped sticky behavior.
|
||||
expect(header?.className).not.toContain("sticky top-0");
|
||||
});
|
||||
|
||||
it("renders the 4 columns and shows per-column counts", () => {
|
||||
const tasks: Task[] = [
|
||||
buildTask({ id: "t1", title: "Inbox A", status: "inbox" }),
|
||||
buildTask({ id: "t2", title: "Doing A", status: "in_progress" }),
|
||||
buildTask({ id: "t3", title: "Review A", status: "review" }),
|
||||
buildTask({ id: "t4", title: "Done A", status: "done" }),
|
||||
buildTask({ id: "t5", title: "Inbox B", status: "inbox" }),
|
||||
];
|
||||
|
||||
render(<TaskBoard tasks={tasks} />);
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Inbox" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "In Progress" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Review" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("heading", { name: "Done" })).toBeInTheDocument();
|
||||
|
||||
// Column count badges are plain spans; easiest stable check is text occurrence.
|
||||
expect(screen.getAllByText("2").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("1").length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
expect(screen.getByText("Inbox A")).toBeInTheDocument();
|
||||
expect(screen.getByText("Inbox B")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("filters the review column by bucket", () => {
|
||||
const tasks: Task[] = [
|
||||
buildTask({
|
||||
id: "blocked",
|
||||
title: "Blocked Review",
|
||||
status: "review",
|
||||
is_blocked: true,
|
||||
blocked_by_task_ids: ["dep-1"],
|
||||
}),
|
||||
buildTask({
|
||||
id: "approval",
|
||||
title: "Needs Approval",
|
||||
status: "review",
|
||||
approvals_pending_count: 2,
|
||||
}),
|
||||
buildTask({
|
||||
id: "lead",
|
||||
title: "Lead Review",
|
||||
status: "review",
|
||||
}),
|
||||
];
|
||||
|
||||
render(<TaskBoard tasks={tasks} />);
|
||||
|
||||
const reviewHeading = screen.getByRole("heading", { name: "Review" });
|
||||
const reviewColumn = reviewHeading.closest(".kanban-column");
|
||||
expect(reviewColumn).toBeTruthy();
|
||||
if (!reviewColumn) return;
|
||||
|
||||
const header = reviewColumn.querySelector(".column-header");
|
||||
expect(header).toBeTruthy();
|
||||
if (!header) return;
|
||||
|
||||
const headerQueries = within(header);
|
||||
|
||||
expect(headerQueries.getByRole("button", { name: /All · 3/i })).toBeInTheDocument();
|
||||
expect(
|
||||
headerQueries.getByRole("button", { name: /Approval needed · 1/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
headerQueries.getByRole("button", { name: /Lead review · 1/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
headerQueries.getByRole("button", { name: /Blocked · 1/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(headerQueries.getByRole("button", { name: /Blocked · 1/i }));
|
||||
expect(screen.getByText("Blocked Review")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Needs Approval")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Lead Review")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(
|
||||
headerQueries.getByRole("button", { name: /Approval needed · 1/i }),
|
||||
);
|
||||
expect(screen.getByText("Needs Approval")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Blocked Review")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Lead Review")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(
|
||||
headerQueries.getByRole("button", { name: /Lead review · 1/i }),
|
||||
);
|
||||
expect(screen.getByText("Lead Review")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Blocked Review")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Needs Approval")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("invokes onTaskMove when a task is dropped onto a different column", () => {
|
||||
const onTaskMove = vi.fn();
|
||||
const tasks: Task[] = [
|
||||
buildTask({ id: "t1", title: "Inbox A", status: "inbox" }),
|
||||
];
|
||||
|
||||
render(<TaskBoard tasks={tasks} onTaskMove={onTaskMove} />);
|
||||
|
||||
const dropTarget = screen
|
||||
.getByRole("heading", { name: "Done" })
|
||||
.closest(".kanban-column");
|
||||
expect(dropTarget).toBeTruthy();
|
||||
if (!dropTarget) return;
|
||||
|
||||
fireEvent.drop(dropTarget, {
|
||||
dataTransfer: {
|
||||
getData: () => JSON.stringify({ taskId: "t1", status: "inbox" }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(onTaskMove).toHaveBeenCalledWith("t1", "done");
|
||||
});
|
||||
|
||||
it("does not allow dragging when readOnly is true", () => {
|
||||
const tasks: Task[] = [buildTask({ id: "t1", title: "Inbox A" })];
|
||||
|
||||
render(<TaskBoard tasks={tasks} readOnly />);
|
||||
|
||||
expect(screen.getByRole("button", { name: /Inbox A/i })).toHaveAttribute(
|
||||
"draggable",
|
||||
"false",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
37
frontend/vitest.full-coverage.config.ts
Normal file
37
frontend/vitest.full-coverage.config.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
// Broader coverage config used for gap analysis.
|
||||
// - Does NOT enforce 100% thresholds.
|
||||
// - Includes most source files (excluding generated artifacts and types).
|
||||
// Keep the default vitest.config.ts as the scoped coverage gate.
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "jsdom",
|
||||
setupFiles: ["./src/setupTests.ts"],
|
||||
globals: true,
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "lcov"],
|
||||
reportsDirectory: "./coverage-full",
|
||||
include: ["src/**/*.{ts,tsx}"],
|
||||
exclude: [
|
||||
"**/*.d.ts",
|
||||
"src/**/__generated__/**",
|
||||
"src/**/generated/**",
|
||||
],
|
||||
thresholds: {
|
||||
lines: 0,
|
||||
statements: 0,
|
||||
functions: 0,
|
||||
branches: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user