feat(FN-2875): add done-task elapsed time ceiling rounding
- Add formatElapsedDurationDone to render done task elapsed time with ceiling-based minute/hour/day rounding - Keep in-progress time indicator logic on existing floor-based formatElapsedDuration behavior - Route done-card time indicator rendering through the new done-specific formatter - Expand TaskCard tests to cover done rounding semantics and preserve in-progress rounding expectations
This commit is contained in:
@@ -169,6 +169,20 @@ function formatElapsedDuration(elapsedMs: number): string {
|
||||
return `${elapsedDays}d`;
|
||||
}
|
||||
|
||||
export function formatElapsedDurationDone(elapsedMs: number): string {
|
||||
if (!Number.isFinite(elapsedMs) || elapsedMs < 0) return "";
|
||||
if (elapsedMs === 0) return "";
|
||||
|
||||
const elapsedMinutes = Math.ceil(elapsedMs / 60_000);
|
||||
if (elapsedMinutes < 59) return `${elapsedMinutes}m`;
|
||||
|
||||
const elapsedHours = Math.ceil(elapsedMs / 3_600_000);
|
||||
if (elapsedHours < 24) return `${elapsedHours}h`;
|
||||
|
||||
const elapsedDays = Math.ceil(elapsedMs / 86_400_000);
|
||||
return `${elapsedDays}d`;
|
||||
}
|
||||
|
||||
|
||||
interface TaskCardProps {
|
||||
task: Task;
|
||||
@@ -692,12 +706,12 @@ function TaskCardComponent({
|
||||
return null;
|
||||
}
|
||||
|
||||
const elapsedLabel = formatElapsedDuration(instrumentedMs);
|
||||
if (!elapsedLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (task.column === "in-progress") {
|
||||
const elapsedLabel = formatElapsedDuration(instrumentedMs);
|
||||
if (!elapsedLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: elapsedLabel,
|
||||
title: `Execution time ${elapsedLabel}`,
|
||||
@@ -705,6 +719,11 @@ function TaskCardComponent({
|
||||
};
|
||||
}
|
||||
|
||||
const elapsedLabel = formatElapsedDurationDone(instrumentedMs);
|
||||
if (!elapsedLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const completionMs = getDoneCompletionMs(task);
|
||||
if (completionMs == null) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
|
||||
import { TaskCard } from "../TaskCard";
|
||||
import { TaskCard, formatElapsedDurationDone } from "../TaskCard";
|
||||
import type { Task } from "@fusion/core";
|
||||
|
||||
// Mock lucide-react to avoid SVG rendering issues in test env
|
||||
@@ -712,6 +712,72 @@ describe("TaskCard", () => {
|
||||
expect(container.querySelector(".card-time-indicator")).toBeNull();
|
||||
});
|
||||
|
||||
describe("formatElapsedDuration rounding for done tasks", () => {
|
||||
it.each([
|
||||
[59_999, "1m"],
|
||||
[60_000, "1m"],
|
||||
[90_000, "2m"],
|
||||
[3_540_000, "1h"],
|
||||
[3_600_000, "1h"],
|
||||
[86_400_000, "1d"],
|
||||
])("formats %dms as %s for done tasks", (elapsedMs, expected) => {
|
||||
expect(formatElapsedDurationDone(elapsedMs)).toBe(expected);
|
||||
});
|
||||
|
||||
it("keeps in-progress rounding with floor semantics", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-04-25T12:01:30.000Z"));
|
||||
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({
|
||||
column: "in-progress",
|
||||
workflowStepResults: [
|
||||
{
|
||||
workflowStepId: "step-1",
|
||||
workflowStepName: "Plan",
|
||||
phase: "pre-merge" as const,
|
||||
status: "pending" as const,
|
||||
startedAt: "2026-04-25T12:00:00.000Z",
|
||||
},
|
||||
],
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".card-time-indicator")?.textContent).toContain("1m");
|
||||
});
|
||||
|
||||
it("renders done-card timer with ceiling rounding for fractional minutes", () => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({
|
||||
column: "done",
|
||||
createdAt: "2026-04-25T12:00:00.000Z",
|
||||
columnMovedAt: "2026-04-25T12:04:30.000Z",
|
||||
updatedAt: "2026-04-25T12:04:30.000Z",
|
||||
workflowStepResults: [
|
||||
{
|
||||
workflowStepId: "step-1",
|
||||
workflowStepName: "Plan",
|
||||
phase: "pre-merge" as const,
|
||||
status: "passed" as const,
|
||||
startedAt: "2026-04-25T12:00:00.000Z",
|
||||
completedAt: "2026-04-25T12:04:30.000Z",
|
||||
},
|
||||
],
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".card-time-indicator")?.textContent).toContain("5m");
|
||||
});
|
||||
});
|
||||
|
||||
it("live-ticks workflow runtime for in-progress steps", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-04-25T12:00:30.000Z"));
|
||||
|
||||
Reference in New Issue
Block a user