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:
Fusion
2026-04-24 16:08:18 -07:00
committed by gsxdsm
parent bb75e73e24
commit 5ca0df728f
5 changed files with 133 additions and 28 deletions

View File

@@ -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 };
}

View File

@@ -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 (
<>

View File

@@ -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();

View File

@@ -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" },

View File

@@ -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" },