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",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"test": "vitest run --passWithNoTests --coverage",
|
"test": "vitest run --passWithNoTests --coverage",
|
||||||
|
"test:full-coverage": "vitest run --passWithNoTests --coverage --config ./vitest.full-coverage.config.ts",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"dev:lan": "next dev --hostname 0.0.0.0 --port 3000",
|
"dev:lan": "next dev --hostname 0.0.0.0 --port 3000",
|
||||||
"api:gen": "orval --config ./orval.config.ts",
|
"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 { fireEvent, render, screen, within } from "@testing-library/react";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { TaskBoard } from "./TaskBoard";
|
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", () => {
|
describe("TaskBoard", () => {
|
||||||
it("uses a mobile-first stacked layout (no horizontal scroll) with responsive kanban columns on larger screens", () => {
|
it("uses a mobile-first stacked layout (no horizontal scroll) with responsive kanban columns on larger screens", () => {
|
||||||
render(
|
render(
|
||||||
@@ -48,4 +71,130 @@ describe("TaskBoard", () => {
|
|||||||
// Ensure we didn't accidentally keep unscoped sticky behavior.
|
// Ensure we didn't accidentally keep unscoped sticky behavior.
|
||||||
expect(header?.className).not.toContain("sticky top-0");
|
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