fix(FN-2484): limit progress visibility to active execution
- Gate unified progress display in ListView to tasks that are executing or in-progress - Gate TaskCard progress bar and steps toggle behind the same active-execution visibility rule - Update ListView and mobile list-card tests to cover hidden progress for non-executing todo tasks - Expand TaskCard and mobile board tests to assert progress/toggle visibility for executing vs queued states
This commit is contained in:
@@ -133,9 +133,13 @@ interface ListViewProps {
|
||||
lastFetchTimeMs?: number;
|
||||
}
|
||||
|
||||
function shouldShowTaskProgress(task: Task): boolean {
|
||||
return task.status === "executing" || task.column === "in-progress";
|
||||
}
|
||||
|
||||
function getTaskProgress(task: Task): { label: string; percent: number; hasProgress: boolean } {
|
||||
const progress = getUnifiedTaskProgress(task);
|
||||
if (progress.total === 0) {
|
||||
if (progress.total === 0 || !shouldShowTaskProgress(task)) {
|
||||
return { label: "-", percent: 0, hasProgress: false };
|
||||
}
|
||||
|
||||
|
||||
@@ -559,6 +559,8 @@ function TaskCardComponent({
|
||||
() => getUnifiedTaskProgress(task, workflowStepNameLookup),
|
||||
[task.steps, task.enabledWorkflowSteps, task.workflowStepResults, workflowStepNameLookup],
|
||||
);
|
||||
const showProgressSection =
|
||||
unifiedProgress.total > 0 && (task.status === "executing" || task.column === "in-progress");
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasGitHubBadge || !isInViewport) {
|
||||
@@ -1029,7 +1031,7 @@ function TaskCardComponent({
|
||||
<div className="card-title" title={task.title || task.description || undefined}>
|
||||
{truncate(task.title, MAX_TITLE_LENGTH) || truncate(task.description, MAX_TITLE_LENGTH) || task.id}
|
||||
</div>
|
||||
{unifiedProgress.total > 0 && (() => {
|
||||
{showProgressSection && (() => {
|
||||
const progressPercent = (unifiedProgress.completed / unifiedProgress.total) * 100;
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -418,10 +418,12 @@ describe("ListView", () => {
|
||||
expect(table?.textContent).toContain("Done");
|
||||
});
|
||||
|
||||
it("renders unified progress bar for implementation steps + workflow checks", () => {
|
||||
it("renders unified progress bar for actively executing tasks", () => {
|
||||
const tasks = [
|
||||
createMockTask({
|
||||
id: "FN-001",
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "Step 1", status: "done" },
|
||||
{ name: "Step 2", status: "done" },
|
||||
@@ -447,6 +449,8 @@ describe("ListView", () => {
|
||||
const tasks = [
|
||||
createMockTask({
|
||||
id: "FN-001",
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [],
|
||||
enabledWorkflowSteps: ["WS-001"],
|
||||
workflowStepResults: [
|
||||
@@ -477,6 +481,23 @@ describe("ListView", () => {
|
||||
expect(progressCell?.textContent).toBe("-");
|
||||
});
|
||||
|
||||
it("hides progress for todo tasks that are not executing", () => {
|
||||
const tasks = [
|
||||
createMockTask({
|
||||
id: "FN-002",
|
||||
column: "todo",
|
||||
status: "pending",
|
||||
steps: [{ name: "Step 1", status: "done" }],
|
||||
}),
|
||||
];
|
||||
|
||||
renderListView({ tasks });
|
||||
|
||||
const row = screen.getByText("FN-002").closest("tr")!;
|
||||
const progressCell = row.querySelector(".list-cell-progress");
|
||||
expect(progressCell?.textContent).toBe("-");
|
||||
});
|
||||
|
||||
it("renders dependency count with icon", () => {
|
||||
const tasks = [
|
||||
createMockTask({
|
||||
@@ -2231,7 +2252,7 @@ describe("ListView - Bulk Selection", () => {
|
||||
expect(within(card as HTMLElement).getByText("executing")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows unified progress bar for cards with steps and workflow checks", () => {
|
||||
it("shows unified progress bar for executing mobile cards with steps and workflow checks", () => {
|
||||
mockMobileViewport();
|
||||
|
||||
const { container } = renderListView({
|
||||
@@ -2239,6 +2260,8 @@ describe("ListView - Bulk Selection", () => {
|
||||
createMockTask({
|
||||
id: "FN-001",
|
||||
title: "Progress task",
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "Step 1", status: "done" },
|
||||
{ name: "Step 2", status: "pending" },
|
||||
@@ -2260,6 +2283,25 @@ describe("ListView - Bulk Selection", () => {
|
||||
expect(within(card).getByText("2/3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides mobile card progress for non-executing todo tasks", () => {
|
||||
mockMobileViewport();
|
||||
|
||||
const { container } = renderListView({
|
||||
tasks: [
|
||||
createMockTask({
|
||||
id: "FN-002",
|
||||
title: "Todo pending task",
|
||||
column: "todo",
|
||||
status: "pending",
|
||||
steps: [{ name: "Step 1", status: "done" }],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const card = container.querySelector('.list-card[data-id="FN-002"]') as HTMLElement;
|
||||
expect(card.querySelector(".list-progress-bar")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows dependency badge for cards with dependencies", () => {
|
||||
mockMobileViewport();
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ describe("TaskCard memoization", () => {
|
||||
|
||||
it("re-renders workflow labels when workflowStepNameLookup prop changes", () => {
|
||||
const task = createTask({
|
||||
status: "executing",
|
||||
enabledWorkflowSteps: ["WS-003"],
|
||||
workflowStepResults: [],
|
||||
steps: [],
|
||||
@@ -1249,10 +1250,30 @@ describe("TaskCard steps toggle", () => {
|
||||
expect(toggle).toBeNull();
|
||||
});
|
||||
|
||||
it("shows steps toggle with count when task has steps", () => {
|
||||
// Use 'todo' column to test default collapsed behavior
|
||||
it("hides progress for todo tasks that are not executing", () => {
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "queued",
|
||||
steps: [{ name: "Step 1", status: "pending" }],
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={task}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={noopToast}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(container.querySelector(".card-progress-bar")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: /steps/i })).toBeNull();
|
||||
});
|
||||
|
||||
it("shows steps toggle with count when todo task is executing", () => {
|
||||
// Use 'todo' + executing status to keep default collapsed behavior
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "Step 1", status: "done" },
|
||||
{ name: "Step 2", status: "pending" },
|
||||
@@ -1275,6 +1296,7 @@ describe("TaskCard steps toggle", () => {
|
||||
it("shows singular step label for one-step tasks", () => {
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [{ name: "Only step", status: "pending" }],
|
||||
});
|
||||
|
||||
@@ -1292,9 +1314,10 @@ describe("TaskCard steps toggle", () => {
|
||||
});
|
||||
|
||||
it("clicking toggle expands and shows step list", () => {
|
||||
// Use 'todo' column to test default collapsed behavior
|
||||
// Use 'todo' + executing status to test default collapsed behavior
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "First step", status: "done" },
|
||||
{ name: "Second step", status: "in-progress" },
|
||||
@@ -1318,9 +1341,10 @@ describe("TaskCard steps toggle", () => {
|
||||
});
|
||||
|
||||
it("clicking toggle again collapses step list", () => {
|
||||
// Use 'todo' column to test default collapsed behavior
|
||||
// Use 'todo' + executing status to test default collapsed behavior
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [{ name: "Single step", status: "pending" }],
|
||||
});
|
||||
|
||||
@@ -1344,9 +1368,10 @@ describe("TaskCard steps toggle", () => {
|
||||
});
|
||||
|
||||
it("step list renders correct number of steps", () => {
|
||||
// Use 'todo' column to test default collapsed behavior
|
||||
// Use 'todo' + executing status to test default collapsed behavior
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "Step 1", status: "done" },
|
||||
{ name: "Step 2", status: "in-progress" },
|
||||
@@ -1372,9 +1397,10 @@ describe("TaskCard steps toggle", () => {
|
||||
});
|
||||
|
||||
it("completed steps have strikethrough style", () => {
|
||||
// Use 'todo' column to test default collapsed behavior
|
||||
// Use 'todo' + executing status to test default collapsed behavior
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "Done step", status: "done" },
|
||||
{ name: "Pending step", status: "pending" },
|
||||
@@ -1400,9 +1426,10 @@ describe("TaskCard steps toggle", () => {
|
||||
});
|
||||
|
||||
it("toggle does not trigger card click when clicked", () => {
|
||||
// Use 'todo' column to test default collapsed behavior
|
||||
// Use 'todo' + executing status to test default collapsed behavior
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [{ name: "Test step", status: "pending" }],
|
||||
});
|
||||
const onOpenDetail = vi.fn();
|
||||
@@ -1423,9 +1450,10 @@ describe("TaskCard steps toggle", () => {
|
||||
});
|
||||
|
||||
it("aria-expanded reflects toggle state", () => {
|
||||
// Use 'todo' column to test default collapsed behavior
|
||||
// Use 'todo' + executing status to test default collapsed behavior
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [{ name: "Test step", status: "pending" }],
|
||||
});
|
||||
|
||||
@@ -1445,9 +1473,10 @@ describe("TaskCard steps toggle", () => {
|
||||
});
|
||||
|
||||
it("chevron icon rotates when expanded", () => {
|
||||
// Use 'todo' column to test default collapsed behavior
|
||||
// Use 'todo' + executing status to test default collapsed behavior
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [{ name: "Test step", status: "pending" }],
|
||||
});
|
||||
|
||||
@@ -1493,9 +1522,10 @@ describe("TaskCard steps toggle", () => {
|
||||
});
|
||||
|
||||
it("step list renders skipped status with correct CSS class", () => {
|
||||
// Use 'todo' column to test default collapsed behavior
|
||||
// Use 'todo' + executing status to test default collapsed behavior
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "Done step", status: "done" },
|
||||
{ name: "Skipped step", status: "skipped" },
|
||||
@@ -1567,18 +1597,8 @@ describe("TaskCard steps auto-expand", () => {
|
||||
true,
|
||||
],
|
||||
[
|
||||
"fresh triage (all pending): collapsed",
|
||||
{ column: "triage", steps: [{ name: "Step 1", status: "pending" }, { name: "Step 2", status: "pending" }] },
|
||||
false,
|
||||
],
|
||||
[
|
||||
"re-triaged (has completed or skipped steps): expanded",
|
||||
{ column: "triage", steps: [{ name: "Step 1", status: "done" }, { name: "Step 2", status: "pending" }] },
|
||||
true,
|
||||
],
|
||||
[
|
||||
"todo / in-review / done: collapsed even with completed steps",
|
||||
{ column: "done", steps: [{ name: "Step 1", status: "done" }, { name: "Step 2", status: "done" }] },
|
||||
"todo + executing: visible but collapsed by default",
|
||||
{ column: "todo", status: "executing", steps: [{ name: "Step 1", status: "done" }, { name: "Step 2", status: "pending" }] },
|
||||
false,
|
||||
],
|
||||
])("default expansion — %s", (_label, overrides, expanded) => {
|
||||
@@ -1596,6 +1616,21 @@ describe("TaskCard steps auto-expand", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("hides steps toggle for non-executing non-in-progress tasks", () => {
|
||||
const task = makeTask({
|
||||
column: "triage",
|
||||
status: "queued",
|
||||
steps: [
|
||||
{ name: "Step 1", status: "done" },
|
||||
{ name: "Step 2", status: "pending" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<TaskCard task={task} onOpenDetail={vi.fn()} addToast={noopToast} />);
|
||||
|
||||
expect(screen.queryByRole("button", { name: /steps/i })).toBeNull();
|
||||
});
|
||||
|
||||
it("toggle button works to collapse steps on in-progress cards", () => {
|
||||
const task = makeTask({
|
||||
column: "in-progress",
|
||||
@@ -1629,9 +1664,10 @@ describe("TaskCard steps auto-expand", () => {
|
||||
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
||||
});
|
||||
|
||||
it("toggle button works to expand steps on non-in-progress cards", () => {
|
||||
it("toggle button works to expand steps on executing todo cards", () => {
|
||||
const task = makeTask({
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "Step 1", status: "done" },
|
||||
{ name: "Step 2", status: "pending" },
|
||||
|
||||
@@ -304,6 +304,7 @@ describe("TaskCard mobile", () => {
|
||||
const task = createTask({
|
||||
id: "FN-205",
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "Step 1", status: "done" },
|
||||
{ name: "Step 2", status: "in-progress" },
|
||||
@@ -330,6 +331,7 @@ describe("TaskCard mobile", () => {
|
||||
const task = createTask({
|
||||
id: "FN-206",
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "Step 1", status: "done" },
|
||||
{ name: "Step 2", status: "in-progress" },
|
||||
@@ -370,10 +372,29 @@ describe("TaskCard mobile", () => {
|
||||
expect(screen.getByRole("button", { name: "Edit task" })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders the progress bar when task has steps", () => {
|
||||
it("hides the progress bar for todo cards that are not executing", () => {
|
||||
const task = createTask({
|
||||
id: "FN-203",
|
||||
column: "todo",
|
||||
status: "queued",
|
||||
steps: [
|
||||
{ name: "Step 1", status: "done" },
|
||||
{ name: "Step 2", status: "in-progress" },
|
||||
],
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<TaskCard task={task} onOpenDetail={vi.fn()} addToast={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".card-progress-bar")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders the progress bar when a todo card is executing", () => {
|
||||
const task = createTask({
|
||||
id: "FN-207",
|
||||
column: "todo",
|
||||
status: "executing",
|
||||
steps: [
|
||||
{ name: "Step 1", status: "done" },
|
||||
{ name: "Step 2", status: "in-progress" },
|
||||
|
||||
Reference in New Issue
Block a user