FN-6413: move task-card timer to footer cluster

Move the task timing badge out of the header/meta row and into the footer's bottom-right badge cluster.

- Render the time indicator with footer chips so it aligns with retry and GitHub badges.
- Reuse footer badge sizing for the timer across desktop, wrapping, and mobile layouts.
- Update TaskCard layout tests to assert footer placement and add a patch changeset.

Files changed:
 .changeset/fn-6413-task-card-timing-footer.md      |   5 +
 packages/dashboard/app/components/TaskCard.css     |   8 +-
 packages/dashboard/app/components/TaskCard.tsx     |  31 ++---
 .../__tests__/TaskCard.footer-alignment.test.tsx   |  13 +-
 .../__tests__/TaskCard.footer-wrap.test.tsx        |   1 +
 .../app/components/__tests__/TaskCard.test.tsx     | 138 ++++++++++++++-------
 .../app/components/__tests__/board-mobile.test.tsx |   5 +-
 7 files changed, 130 insertions(+), 71 deletions(-)

Fusion-Task-Id: FN-6413

Fusion-Task-Lineage: a82da5ab-20c6-4dfa-92a3-150306011fd6
This commit is contained in:
gsxdsm
2026-06-13 19:17:36 -07:00
parent 6a5d7ab48f
commit 67d4d51aeb
7 changed files with 130 additions and 71 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Move task-card timing badges from the top metadata cluster into the bottom-right footer chip cluster so timers align with retry and GitHub footer badges.

View File

@@ -760,10 +760,10 @@
align-items: center; align-items: center;
} }
.card-footer-row > .card-time-indicator:first-of-type { /*
margin-left: auto; FNXC:TaskCardTimingBadge 2026-06-13-17:26:
} The execution-time badge is part of the footer's bottom-right chip cluster, so it inherits the same height, padding, font, and responsive sizing as retry and GitHub tracking badges instead of using the old meta-row placement.
*/
.card-time-indicator, .card-time-indicator,
.card-github-tracking-chip, .card-github-tracking-chip,
.card-retry-badge, .card-retry-badge,

View File

@@ -1789,8 +1789,7 @@ function TaskCardComponent({
&& Boolean(githubTrackedIssue); && Boolean(githubTrackedIssue);
const hasCardMetaBadges = showPriorityBadge const hasCardMetaBadges = showPriorityBadge
|| task.executionMode === "fast" || task.executionMode === "fast"
|| isAgentCreated || isAgentCreated;
|| timeIndicator != null;
if (isEditing) { if (isEditing) {
return ( return (
@@ -2012,16 +2011,6 @@ function TaskCardComponent({
<span aria-hidden="true">{agentCreatedVisibleLabel}</span> <span aria-hidden="true">{agentCreatedVisibleLabel}</span>
</span> </span>
)} )}
{timeIndicator && (
<span
className="card-time-indicator"
title={timeIndicator.title}
aria-label={timeIndicator.ariaLabel}
>
<Clock size={12} />
<span>{timeIndicator.label}</span>
</span>
)}
</div> </div>
)} )}
{task.noCommitsExpected === true && ( {task.noCommitsExpected === true && (
@@ -2289,7 +2278,7 @@ function TaskCardComponent({
</> </>
); );
})()} })()}
{(filesChangedButton || isGitHubImportedTask || showNearDuplicateChip || ((showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue) || (task.retrySummary?.total ?? 0) > 0) && ( {(filesChangedButton || isGitHubImportedTask || timeIndicator || showNearDuplicateChip || ((showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue) || (task.retrySummary?.total ?? 0) > 0) && (
<div className={`card-footer-row${chipFarRight ? " card-footer-row--chip-far-right" : ""}`}> <div className={`card-footer-row${chipFarRight ? " card-footer-row--chip-far-right" : ""}`}>
{filesChangedButton} {filesChangedButton}
{isGitHubImportedTask && !showLinkedIssueChipForImport && ( {isGitHubImportedTask && !showLinkedIssueChipForImport && (
@@ -2301,7 +2290,7 @@ function TaskCardComponent({
<ProviderIcon provider="github" size="sm" /> <ProviderIcon provider="github" size="sm" />
</span> </span>
)} )}
{(showNearDuplicateChip || ((showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue) || (task.retrySummary?.total ?? 0) > 0) && ( {(timeIndicator || showNearDuplicateChip || ((showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue) || (task.retrySummary?.total ?? 0) > 0) && (
<div className="card-footer-row-right"> <div className="card-footer-row-right">
{showNearDuplicateChip && ( {showNearDuplicateChip && (
<> <>
@@ -2374,6 +2363,20 @@ function TaskCardComponent({
<span>{`#${githubTrackedIssue.number}`}</span> <span>{`#${githubTrackedIssue.number}`}</span>
</a> </a>
)} )}
{/*
FNXC:TaskCardTimingBadge 2026-06-13-17:20:
The execution-time badge belongs in the bottom-right footer cluster and must match sibling footer badge sizing while preserving its existing label, title, aria text, and live-update data.
*/}
{timeIndicator && (
<span
className="card-time-indicator"
title={timeIndicator.title}
aria-label={timeIndicator.ariaLabel}
>
<Clock size={12} />
<span>{timeIndicator.label}</span>
</span>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -125,7 +125,7 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
} }
}); });
it("keeps github and retry right-aligned while timer joins card meta badges", () => { it("keeps github, retry, and timer right-aligned in the footer cluster", () => {
const { container } = render( const { container } = render(
<TaskCard <TaskCard
task={{ task={{
@@ -134,6 +134,7 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
column: "in-review", column: "in-review",
retrySummary: { total: 2 }, retrySummary: { total: 2 },
executionStartedAt: "2026-05-12T00:00:00.000Z", executionStartedAt: "2026-05-12T00:00:00.000Z",
executionCompletedAt: "2026-05-12T00:05:00.000Z",
updatedAt: "2026-05-12T00:05:00.000Z", updatedAt: "2026-05-12T00:05:00.000Z",
}} }}
onOpenDetail={noop} onOpenDetail={noop}
@@ -146,14 +147,14 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
const rightCluster = footerRow.querySelector(":scope > .card-footer-row-right") as HTMLElement; const rightCluster = footerRow.querySelector(":scope > .card-footer-row-right") as HTMLElement;
const githubChip = rightCluster.querySelector(":scope > .card-github-tracking-chip") as HTMLElement; const githubChip = rightCluster.querySelector(":scope > .card-github-tracking-chip") as HTMLElement;
const retryChip = rightCluster.querySelector(":scope > .card-retry-badge") as HTMLElement; const retryChip = rightCluster.querySelector(":scope > .card-retry-badge") as HTMLElement;
const timerChip = container.querySelector(".card-meta-badges > .card-time-indicator") as HTMLElement; const timerChip = rightCluster.querySelector(":scope > .card-time-indicator") as HTMLElement;
expect(footerRow).toBeTruthy(); expect(footerRow).toBeTruthy();
expect(rightCluster).toBeTruthy(); expect(rightCluster).toBeTruthy();
expect(githubChip).toBeTruthy(); expect(githubChip).toBeTruthy();
expect(retryChip).toBeTruthy(); expect(retryChip).toBeTruthy();
expect(timerChip).toBeTruthy(); expect(timerChip).toBeTruthy();
expect(rightCluster.contains(timerChip)).toBe(false); expect(rightCluster.contains(timerChip)).toBe(true);
expect(getComputedStyle(rightCluster).marginLeft).toBe("auto"); expect(getComputedStyle(rightCluster).marginLeft).toBe("auto");
@@ -170,6 +171,7 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
sourceType: "github_import", sourceType: "github_import",
retrySummary: { total: 3 }, retrySummary: { total: 3 },
executionStartedAt: "2026-05-12T00:00:00.000Z", executionStartedAt: "2026-05-12T00:00:00.000Z",
executionCompletedAt: "2026-05-12T00:05:00.000Z",
updatedAt: "2026-05-12T00:05:00.000Z", updatedAt: "2026-05-12T00:05:00.000Z",
}} }}
onOpenDetail={noop} onOpenDetail={noop}
@@ -183,7 +185,7 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
const rightCluster = footerRow.querySelector(":scope > .card-footer-row-right") as HTMLElement; const rightCluster = footerRow.querySelector(":scope > .card-footer-row-right") as HTMLElement;
const retryChip = rightCluster.querySelector(":scope > .card-retry-badge") as HTMLElement; const retryChip = rightCluster.querySelector(":scope > .card-retry-badge") as HTMLElement;
const githubChip = rightCluster.querySelector(":scope > .card-github-tracking-chip") as HTMLElement; const githubChip = rightCluster.querySelector(":scope > .card-github-tracking-chip") as HTMLElement;
const timerChip = container.querySelector(".card-meta-badges > .card-time-indicator") as HTMLElement; const timerChip = rightCluster.querySelector(":scope > .card-time-indicator") as HTMLElement;
expect(footerRow).toBeTruthy(); expect(footerRow).toBeTruthy();
expect(sourceChip).toBeTruthy(); expect(sourceChip).toBeTruthy();
@@ -191,13 +193,14 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
expect(retryChip).toBeTruthy(); expect(retryChip).toBeTruthy();
expect(githubChip).toBeTruthy(); expect(githubChip).toBeTruthy();
expect(timerChip).toBeTruthy(); expect(timerChip).toBeTruthy();
expect(rightCluster.contains(timerChip)).toBe(false); expect(rightCluster.contains(timerChip)).toBe(true);
expect(getComputedStyle(sourceChip).marginLeft).not.toBe("auto"); expect(getComputedStyle(sourceChip).marginLeft).not.toBe("auto");
expect(getComputedStyle(rightCluster).marginLeft).toBe("auto"); expect(getComputedStyle(rightCluster).marginLeft).toBe("auto");
expect(Array.from(rightCluster.children).map((node) => (node as HTMLElement).className)).toEqual([ expect(Array.from(rightCluster.children).map((node) => (node as HTMLElement).className)).toEqual([
"card-github-tracking-chip card-github-tracking-link", "card-github-tracking-chip card-github-tracking-link",
expect.stringContaining("card-retry-badge"), expect.stringContaining("card-retry-badge"),
"card-time-indicator",
]); ]);
}); });
}); });

View File

@@ -124,6 +124,7 @@ describe("TaskCard footer wrapping (FN-5210)", () => {
expect(retryChip).toBeTruthy(); expect(retryChip).toBeTruthy();
expect(githubChip).toBeTruthy(); expect(githubChip).toBeTruthy();
expect(timeChip).toBeTruthy(); expect(timeChip).toBeTruthy();
expect(rightCluster.contains(timeChip)).toBe(true);
const footerStyles = getComputedStyle(footerRow); const footerStyles = getComputedStyle(footerRow);
expect(footerStyles.flexWrap).toBe("wrap"); expect(footerStyles.flexWrap).toBe("wrap");

View File

@@ -137,6 +137,16 @@ function mountCssForBadgeTests() {
}; };
} }
function expectTimerInFooterRight(container: HTMLElement) {
const timer = container.querySelector(".card-time-indicator");
const footerRow = container.querySelector(".card-footer-row");
const rightCluster = container.querySelector(".card-footer-row-right");
expect(timer).not.toBeNull();
expect(footerRow?.contains(timer)).toBe(true);
expect(timer?.closest(".card-footer-row-right")).toBe(rightCluster);
expect(timer?.closest(".card-meta-badges")).toBeNull();
}
const highFanout = { const highFanout = {
totalCount: 7, totalCount: 7,
activeTodoCount: 3, activeTodoCount: 3,
@@ -1602,7 +1612,9 @@ describe("TaskCard", () => {
expect(screen.getByTestId("icon-zap")).toBeDefined(); expect(screen.getByTestId("icon-zap")).toBeDefined();
}); });
it("groups priority, fast mode, agent-created, and time metadata in one badge row", () => {
it("keeps priority, fast mode, and agent-created in meta while time moves to footer", () => {
const { container } = render( const { container } = render(
<TaskCard <TaskCard
task={makeTask({ task={makeTask({
@@ -1621,22 +1633,30 @@ describe("TaskCard", () => {
const group = container.querySelector(".card-meta-badges"); const group = container.querySelector(".card-meta-badges");
expect(group).not.toBeNull(); expect(group).not.toBeNull();
const expectedSelectors = [
const expectedMetaSelectors = [
".card-priority-badge", ".card-priority-badge",
".card-execution-mode-badge", ".card-execution-mode-badge",
".card-agent-created-badge", ".card-agent-created-badge",
".card-time-indicator",
]; ];
expectedSelectors.forEach((selector) => { expectedMetaSelectors.forEach((selector) => {
const badge = container.querySelector(selector); const badge = container.querySelector(selector);
expect(badge).not.toBeNull(); expect(badge).not.toBeNull();
expect(badge?.closest(".card-meta-badges")).toBe(group); expect(badge?.closest(".card-meta-badges")).toBe(group);
}); });
const timer = container.querySelector(".card-time-indicator");
expect(timer).not.toBeNull();
expect(timer?.closest(".card-meta-badges")).toBeNull();
expect(timer?.closest(".card-footer-row-right")).not.toBeNull();
expect(Array.from(group?.children ?? []).map((child) => child.className)).toEqual([ expect(Array.from(group?.children ?? []).map((child) => child.className)).toEqual([
"card-priority-badge card-priority-badge--high", "card-priority-badge card-priority-badge--high",
"card-execution-mode-badge card-execution-mode-badge--fast", "card-execution-mode-badge card-execution-mode-badge--fast",
"card-agent-created-badge", "card-agent-created-badge",
"card-time-indicator",
]); ]);
}); });
@@ -1665,7 +1685,9 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-footer-row-right")).toBeNull(); expect(container.querySelector(".card-footer-row-right")).toBeNull();
}); });
it("moves a lone time chip into card meta badges without rendering an empty footer", () => {
it("moves a lone time chip into the footer without rendering empty meta badges", () => {
vi.useFakeTimers(); vi.useFakeTimers();
vi.setSystemTime(new Date("2026-04-25T12:05:00.000Z")); vi.setSystemTime(new Date("2026-04-25T12:05:00.000Z"));
@@ -1683,14 +1705,19 @@ describe("TaskCard", () => {
const group = container.querySelector(".card-meta-badges"); const group = container.querySelector(".card-meta-badges");
const timer = container.querySelector(".card-time-indicator"); const timer = container.querySelector(".card-time-indicator");
expect(group).not.toBeNull();
const footerRow = container.querySelector(".card-footer-row");
const rightCluster = container.querySelector(".card-footer-row-right");
expect(group).toBeNull();
expect(timer).not.toBeNull(); expect(timer).not.toBeNull();
expect(timer?.closest(".card-meta-badges")).toBe(group); expect(footerRow).not.toBeNull();
expect(rightCluster).not.toBeNull();
expect(timer?.closest(".card-footer-row-right")).toBe(rightCluster);
expect(Array.from(rightCluster?.children ?? [])).toEqual([timer]);
expect(container.querySelector(".card-priority-badge")).toBeNull(); expect(container.querySelector(".card-priority-badge")).toBeNull();
expect(container.querySelector(".card-execution-mode-badge")).toBeNull(); expect(container.querySelector(".card-execution-mode-badge")).toBeNull();
expect(container.querySelector(".card-agent-created-badge")).toBeNull(); expect(container.querySelector(".card-agent-created-badge")).toBeNull();
expect(container.querySelector(".card-footer-row")).toBeNull();
expect(container.querySelector(".card-footer-row-right")).toBeNull();
}); });
it("does not render card meta badge shells when all grouped affordances are absent", () => { it("does not render card meta badge shells when all grouped affordances are absent", () => {
@@ -2335,7 +2362,7 @@ describe("TaskCard", () => {
); );
const timer = container.querySelector(".card-time-indicator"); const timer = container.querySelector(".card-time-indicator");
expect(timer).not.toBeNull(); expectTimerInFooterRight(container);
// 8m workflow + 4m timed = 12m // 8m workflow + 4m timed = 12m
expect(timer?.textContent).toContain("12m"); expect(timer?.textContent).toContain("12m");
expect(timer?.getAttribute("title")).toContain("In progress 12m"); expect(timer?.getAttribute("title")).toContain("In progress 12m");
@@ -2354,6 +2381,7 @@ describe("TaskCard", () => {
); );
expect(container.querySelector(".card-time-indicator")?.textContent).toContain("1m"); expect(container.querySelector(".card-time-indicator")?.textContent).toContain("1m");
expectTimerInFooterRight(container);
rerender( rerender(
<TaskCard <TaskCard
@@ -2367,6 +2395,7 @@ describe("TaskCard", () => {
); );
expect(container.querySelector(".card-time-indicator")?.textContent).toContain("2m"); expect(container.querySelector(".card-time-indicator")?.textContent).toContain("2m");
expectTimerInFooterRight(container);
}); });
it("shows timer chip for done cards summing workflow runtime + timed events", () => { it("shows timer chip for done cards summing workflow runtime + timed events", () => {
@@ -2401,7 +2430,7 @@ describe("TaskCard", () => {
); );
const timer = container.querySelector(".card-time-indicator"); const timer = container.querySelector(".card-time-indicator");
expect(timer).not.toBeNull(); expectTimerInFooterRight(container);
// 1h workflow + 1h timed = 2h // 1h workflow + 1h timed = 2h
expect(timer?.textContent).toContain("2h"); expect(timer?.textContent).toContain("2h");
expect(timer?.getAttribute("title")).toContain("Execution time 2h"); expect(timer?.getAttribute("title")).toContain("Execution time 2h");
@@ -2512,7 +2541,9 @@ describe("TaskCard", () => {
expect(queuedBadge?.compareDocumentPosition(footerRow as Node) & Node.DOCUMENT_POSITION_PRECEDING).toBeTruthy(); expect(queuedBadge?.compareDocumentPosition(footerRow as Node) & Node.DOCUMENT_POSITION_PRECEDING).toBeTruthy();
}); });
it("renders tracking and retry in the footer while timer joins the card meta badges", () => {
it("renders tracking, retry, and timer in the footer right cluster", () => {
const { container } = render( const { container } = render(
<TaskCard <TaskCard
task={makeTask({ task={makeTask({
@@ -2539,16 +2570,23 @@ describe("TaskCard", () => {
const footerRow = container.querySelector(".card-footer-row"); const footerRow = container.querySelector(".card-footer-row");
const metaBadges = container.querySelector(".card-meta-badges"); const metaBadges = container.querySelector(".card-meta-badges");
const rightCluster = container.querySelector(".card-footer-row-right");
const trackingLink = container.querySelector(".card-github-tracking-chip"); const trackingLink = container.querySelector(".card-github-tracking-chip");
const retryChip = container.querySelector(".card-retry-badge"); const retryChip = container.querySelector(".card-retry-badge");
const timerChip = container.querySelector(".card-time-indicator"); const timerChip = container.querySelector(".card-time-indicator");
expect(footerRow).not.toBeNull(); expect(footerRow).not.toBeNull();
expect(metaBadges).not.toBeNull();
expect(metaBadges).toBeNull();
expect(rightCluster).not.toBeNull();
expect(footerRow?.contains(trackingLink)).toBe(true); expect(footerRow?.contains(trackingLink)).toBe(true);
expect(footerRow?.contains(retryChip)).toBe(true); expect(footerRow?.contains(retryChip)).toBe(true);
expect(footerRow?.contains(timerChip)).toBe(false); expect(footerRow?.contains(timerChip)).toBe(true);
expect(metaBadges?.contains(timerChip)).toBe(true); expect(rightCluster?.contains(timerChip)).toBe(true);
expect(Array.from(rightCluster?.children ?? [])).toContain(timerChip);
expect(container.querySelector(".card-bottom-right-row")).toBeNull(); expect(container.querySelector(".card-bottom-right-row")).toBeNull();
}); });
@@ -2745,7 +2783,9 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-footer-row > .card-source-provenance")).toBeNull(); expect(container.querySelector(".card-footer-row > .card-source-provenance")).toBeNull();
}); });
it("keeps github badges before retry while time chip joins card meta badges", () => {
it("keeps github badges before retry while time chip ends the footer cluster", () => {
const { container } = render( const { container } = render(
<TaskCard <TaskCard
task={makeTask({ task={makeTask({
@@ -2779,10 +2819,13 @@ describe("TaskCard", () => {
const timerChip = container.querySelector(".card-time-indicator"); const timerChip = container.querySelector(".card-time-indicator");
expect(sourceNode).not.toBeNull(); expect(sourceNode).not.toBeNull();
expect(rightCluster).not.toBeNull(); expect(rightCluster).not.toBeNull();
expect(timerChip?.closest(".card-meta-badges")).not.toBeNull();
expect(timerChip?.closest(".card-footer-row-right")).toBe(rightCluster);
const orderedNodes = [ const orderedNodes = [
rightCluster?.querySelector(".card-github-tracking-chip"), rightCluster?.querySelector(".card-github-tracking-chip"),
rightCluster?.querySelector(".card-retry-badge"), rightCluster?.querySelector(".card-retry-badge"),
timerChip,
]; ];
orderedNodes.forEach((node) => expect(node).not.toBeNull()); orderedNodes.forEach((node) => expect(node).not.toBeNull());
expect(Array.from((rightCluster as Element).children)).toEqual(orderedNodes); expect(Array.from((rightCluster as Element).children)).toEqual(orderedNodes);
@@ -2838,17 +2881,15 @@ describe("TaskCard", () => {
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null; const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(rightCluster).not.toBeNull(); expect(rightCluster).not.toBeNull();
const children = Array.from((rightCluster as HTMLElement).children); const children = Array.from((rightCluster as HTMLElement).children);
if (rightSideChip?.classList.contains("card-time-indicator")) {
expect(children.at(-1)).toBe(trackingChip); expect(children).toContain(trackingChip as HTMLElement);
expect(rightSideChip.closest(".card-meta-badges")).not.toBeNull(); expect(children).toContain(rightSideChip as HTMLElement);
} else { expect(children.indexOf(rightSideChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
expect(children.at(-1)).toBe(rightSideChip);
expect(children.indexOf(rightSideChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
}
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto"); expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
}); });
it.each(["in-progress", "in-review"] as const)("renders time indicator in meta badges beside footer tracking chip for %s", (column) => { it.each(["in-progress", "in-review"] as const)("renders time indicator in footer right cluster beside tracking chip for %s", (column) => {
const { container } = render( const { container } = render(
<TaskCard <TaskCard
task={makeTask({ task={makeTask({
@@ -2875,8 +2916,10 @@ describe("TaskCard", () => {
expect(rightCluster).not.toBeNull(); expect(rightCluster).not.toBeNull();
const children = Array.from((rightCluster as HTMLElement).children); const children = Array.from((rightCluster as HTMLElement).children);
expect(children).toContain(trackingChip); expect(children).toContain(trackingChip);
expect(children).not.toContain(timeChip);
expect(timeChip?.closest(".card-meta-badges")).not.toBeNull(); expect(children).toContain(timeChip);
expect(timeChip?.closest(".card-footer-row-right")).toBe(rightCluster);
}); });
it("does not force far-right modifier when in-progress card has files changed", () => { it("does not force far-right modifier when in-progress card has files changed", () => {
@@ -2940,13 +2983,11 @@ describe("TaskCard", () => {
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null; const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(rightCluster).not.toBeNull(); expect(rightCluster).not.toBeNull();
const children = Array.from((rightCluster as HTMLElement).children); const children = Array.from((rightCluster as HTMLElement).children);
if (rightSideChip?.classList.contains("card-time-indicator")) {
expect(children.at(-1)).toBe(trackingChip); expect(children).toContain(trackingChip as HTMLElement);
expect(rightSideChip.closest(".card-meta-badges")).not.toBeNull(); expect(children).toContain(rightSideChip as HTMLElement);
} else { expect(children.indexOf(rightSideChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
expect(children.at(-1)).toBe(rightSideChip);
expect(children.indexOf(rightSideChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
}
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto"); expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
}); });
@@ -3010,8 +3051,10 @@ describe("TaskCard", () => {
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null; const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(rightCluster).not.toBeNull(); expect(rightCluster).not.toBeNull();
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto"); expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
expect((trackingChip as HTMLElement).nextElementSibling).toBeNull();
expect(timerChip?.closest(".card-meta-badges")).not.toBeNull(); expect((trackingChip as HTMLElement).nextElementSibling).toBe(timerChip);
expect(timerChip?.closest(".card-footer-row-right")).toBe(rightCluster);
} finally { } finally {
cleanupCss(); cleanupCss();
} }
@@ -3798,12 +3841,15 @@ describe("TaskCard", () => {
expect(footerRow).not.toBeNull(); expect(footerRow).not.toBeNull();
expect(filesChanged).not.toBeNull(); expect(filesChanged).not.toBeNull();
expect(timer).not.toBeNull(); expect(timer).not.toBeNull();
const rightCluster = container.querySelector(".card-footer-row-right");
expect(footerRow?.contains(filesChanged)).toBe(true); expect(footerRow?.contains(filesChanged)).toBe(true);
expect(footerRow?.contains(timer)).toBe(false);
expect(header?.contains(timer)).toBe(true); expect(footerRow?.contains(timer)).toBe(true);
expect(timer?.closest(".card-meta-badges")).not.toBeNull(); expect(header?.contains(timer)).toBe(false);
expect(container.querySelector(".card-footer-row-right")).toBeNull(); expect(timer?.closest(".card-footer-row-right")).toBe(rightCluster);
expect(Array.from(footerRow?.children ?? [])).toEqual([filesChanged]); expect(rightCluster).not.toBeNull();
expect(Array.from(footerRow?.children ?? [])).toEqual([filesChanged, rightCluster]);
}); });
it("shows timer chip for in-review cards", () => { it("shows timer chip for in-review cards", () => {
@@ -3835,7 +3881,7 @@ describe("TaskCard", () => {
); );
const timer = container.querySelector(".card-time-indicator"); const timer = container.querySelector(".card-time-indicator");
expect(timer).not.toBeNull(); expectTimerInFooterRight(container);
expect(timer?.textContent).toContain("12m"); expect(timer?.textContent).toContain("12m");
expect(timer?.getAttribute("title")).toContain("Execution time 12m"); expect(timer?.getAttribute("title")).toContain("Execution time 12m");
expect(timer?.getAttribute("title")).not.toContain("Completed"); expect(timer?.getAttribute("title")).not.toContain("Completed");
@@ -3859,7 +3905,7 @@ describe("TaskCard", () => {
); );
const timer = container.querySelector(".card-time-indicator"); const timer = container.querySelector(".card-time-indicator");
expect(timer).not.toBeNull(); expectTimerInFooterRight(container);
expect(timer?.textContent).toContain("30m"); expect(timer?.textContent).toContain("30m");
expect(timer?.getAttribute("title")).toBe("Execution time 30m"); expect(timer?.getAttribute("title")).toBe("Execution time 30m");
@@ -3891,7 +3937,7 @@ describe("TaskCard", () => {
); );
const timer = container.querySelector(".card-time-indicator"); const timer = container.querySelector(".card-time-indicator");
expect(timer).not.toBeNull(); expectTimerInFooterRight(container);
expect(timer?.textContent).toContain("6m"); expect(timer?.textContent).toContain("6m");
expect(timer?.getAttribute("title")).toBe("Execution time 6m"); expect(timer?.getAttribute("title")).toBe("Execution time 6m");
}); });
@@ -3945,7 +3991,7 @@ describe("TaskCard", () => {
); );
const timer = container.querySelector(".card-time-indicator"); const timer = container.querySelector(".card-time-indicator");
expect(timer).not.toBeNull(); expectTimerInFooterRight(container);
expect(timer?.textContent).toContain("45m"); expect(timer?.textContent).toContain("45m");
expect(timer?.getAttribute("title")).toBe("Execution time 45m. Merge phase <1m"); expect(timer?.getAttribute("title")).toBe("Execution time 45m. Merge phase <1m");
} finally { } finally {

View File

@@ -339,9 +339,10 @@ describe("TaskCard mobile", () => {
expectRuleToContain(css, ".card-footer-row", "row-gap: var(--space-xs);"); expectRuleToContain(css, ".card-footer-row", "row-gap: var(--space-xs);");
}); });
it("keeps TaskCard footer-chip cluster anchored by first chip rules", () => { it("keeps TaskCard footer-chip cluster anchored by the right wrapper", () => {
const css = loadAllAppCss(); const css = loadAllAppCss();
expectRuleToContain(css, ".card-footer-row > .card-time-indicator:first-of-type", "margin-left: auto;"); expectRuleToContain(css, ".card-footer-row-right", "margin-left: auto;");
expect(css).not.toContain(".card-footer-row > .card-time-indicator:first-of-type");
const timeIndicatorRule = css.match(/\.card-time-indicator\s*\{[^}]*\}/)?.[0] ?? ""; const timeIndicatorRule = css.match(/\.card-time-indicator\s*\{[^}]*\}/)?.[0] ?? "";
expect(timeIndicatorRule).not.toContain("margin-left: auto;"); expect(timeIndicatorRule).not.toContain("margin-left: auto;");