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:
5
.changeset/fn-6413-task-card-timing-footer.md
Normal file
5
.changeset/fn-6413-task-card-timing-footer.md
Normal 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.
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;");
|
||||||
|
|||||||
Reference in New Issue
Block a user