feat(FN-5099): complete Step 4 — pass verification gates

Fusion-Task-Id: FN-5099
Fusion-Task-Lineage: 26353f25-da61-4b61-8fd2-69915a984cc4
This commit is contained in:
Fusion (runfusion.ai)
2026-05-18 22:33:24 -07:00
committed by gsxdsm
parent 39fee83654
commit d8a04ff5a7
5 changed files with 135 additions and 123 deletions

View File

@@ -693,30 +693,13 @@
color: var(--text-muted);
}
.card-footer-row > .card-source-provenance:first-of-type,
.card-footer-row > .card-github-tracking-chip:first-of-type,
.card-footer-row > .card-retry-badge:first-of-type,
.card-footer-row > .card-time-indicator:first-of-type {
margin-left: auto;
}
.card-source-provenance + .card-source-provenance,
.card-footer-row > .card-source-provenance + .card-github-tracking-chip:first-of-type,
.card-footer-row > .card-source-provenance + .card-retry-badge,
.card-footer-row > .card-source-provenance + .card-time-indicator,
.card-footer-row > .card-github-tracking-chip + .card-retry-badge,
.card-footer-row > .card-github-tracking-chip + .card-time-indicator,
.card-footer-row > .card-retry-badge + .card-time-indicator {
margin-left: 0;
}
.card-footer-row.card-footer-row--chip-far-right > .card-source-provenance:first-of-type,
.card-footer-row.card-footer-row--chip-far-right > .card-source-provenance + .card-github-tracking-chip:first-of-type {
margin-left: 0;
}
.card-footer-row.card-footer-row--chip-far-right > .card-github-tracking-chip {
.card-footer-row-right {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
margin-left: auto;
min-width: 0;
flex-wrap: nowrap;
}
.card-source-provenance .provider-icon {
@@ -1322,10 +1305,6 @@
gap: var(--space-xs);
}
.card-footer-row.card-footer-row--chip-far-right > .card-github-tracking-chip {
margin-left: auto;
}
.card-time-indicator,
.card-github-tracking-chip,
.card-retry-badge,

View File

@@ -1847,49 +1847,68 @@ function TaskCardComponent({
<ProviderIcon provider="github" size="sm" />
</span>
)}
{(task.retrySummary?.total ?? 0) > 0 && (
<span
className={`card-retry-badge${(retryWarningThreshold != null && (task.retrySummary?.total ?? 0) >= retryWarningThreshold) ? " card-retry-badge--error" : " card-retry-badge--warning"}`}
onClick={handleOpenRetries}
role="button"
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
onOpenDetailWithTab?.(task, "retries");
}
}}
aria-label={`${task.retrySummary?.total ?? 0} retries`}
title="Open retry breakdown"
>
<RotateCw size={11} />
<span>{task.retrySummary?.total ?? 0}</span>
</span>
)}
{(showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue && (
<a
className="card-github-tracking-chip card-github-tracking-link"
href={githubTrackedIssue.url}
target="_blank"
rel="noopener noreferrer"
title={`Linked GitHub issue: ${githubTrackedIssue.owner}/${githubTrackedIssue.repo}#${githubTrackedIssue.number}`}
aria-label={`Linked GitHub issue #${githubTrackedIssue.number}`}
onClick={(e) => e.stopPropagation()}
>
<ProviderIcon provider="github" size="sm" />
<span>{`#${githubTrackedIssue.number}`}</span>
</a>
)}
{timeIndicator && (
<span
className="card-time-indicator"
title={timeIndicator.title}
aria-label={timeIndicator.ariaLabel}
>
<Clock size={12} />
<span>{timeIndicator.label}</span>
</span>
{(((showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue) || (task.retrySummary?.total ?? 0) > 0 || timeIndicator) && (
<div className="card-footer-row-right">
{chipFarRight && (showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue && (
<a
className="card-github-tracking-chip card-github-tracking-link"
href={githubTrackedIssue.url}
target="_blank"
rel="noopener noreferrer"
title={`Linked GitHub issue: ${githubTrackedIssue.owner}/${githubTrackedIssue.repo}#${githubTrackedIssue.number}`}
aria-label={`Linked GitHub issue #${githubTrackedIssue.number}`}
onClick={(e) => e.stopPropagation()}
>
<ProviderIcon provider="github" size="sm" />
<span>{`#${githubTrackedIssue.number}`}</span>
</a>
)}
{(task.retrySummary?.total ?? 0) > 0 && (
<span
className={`card-retry-badge${(retryWarningThreshold != null && (task.retrySummary?.total ?? 0) >= retryWarningThreshold) ? " card-retry-badge--error" : " card-retry-badge--warning"}`}
onClick={handleOpenRetries}
role="button"
tabIndex={0}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
onOpenDetailWithTab?.(task, "retries");
}
}}
aria-label={`${task.retrySummary?.total ?? 0} retries`}
title="Open retry breakdown"
>
<RotateCw size={11} />
<span>{task.retrySummary?.total ?? 0}</span>
</span>
)}
{(!chipFarRight || !((showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue))
&& (showTrackingIndicator || showLinkedIssueChipForImport) && githubTrackedIssue && (
<a
className="card-github-tracking-chip card-github-tracking-link"
href={githubTrackedIssue.url}
target="_blank"
rel="noopener noreferrer"
title={`Linked GitHub issue: ${githubTrackedIssue.owner}/${githubTrackedIssue.repo}#${githubTrackedIssue.number}`}
aria-label={`Linked GitHub issue #${githubTrackedIssue.number}`}
onClick={(e) => e.stopPropagation()}
>
<ProviderIcon provider="github" size="sm" />
<span>{`#${githubTrackedIssue.number}`}</span>
</a>
)}
{timeIndicator && (
<span
className="card-time-indicator"
title={timeIndicator.title}
aria-label={timeIndicator.ariaLabel}
>
<Clock size={12} />
<span>{timeIndicator.label}</span>
</span>
)}
</div>
)}
</div>
)}

View File

@@ -142,18 +142,18 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
);
const footerRow = container.querySelector(".card-footer-row") as HTMLElement;
const githubChip = footerRow.querySelector(":scope > .card-github-tracking-chip") as HTMLElement;
const retryChip = footerRow.querySelector(":scope > .card-retry-badge") as HTMLElement;
const timerChip = footerRow.querySelector(":scope > .card-time-indicator") 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 retryChip = rightCluster.querySelector(":scope > .card-retry-badge") as HTMLElement;
const timerChip = rightCluster.querySelector(":scope > .card-time-indicator") as HTMLElement;
expect(footerRow).toBeTruthy();
expect(rightCluster).toBeTruthy();
expect(githubChip).toBeTruthy();
expect(retryChip).toBeTruthy();
expect(timerChip).toBeTruthy();
expect(getComputedStyle(githubChip).marginLeft).toBe("auto");
expect(getComputedStyle(retryChip).marginLeft).toBe("0px");
expect(getComputedStyle(timerChip).marginLeft).toBe("0px");
expect(getComputedStyle(rightCluster).marginLeft).toBe("auto");
const footerStyle = getComputedStyle(footerRow);
expect(footerStyle.gap).toBe("var(--space-sm)");
@@ -176,21 +176,26 @@ describe("FN-4598 TaskCard footer chip alignment", () => {
/>,
);
const footerRow = container.querySelector(".card-footer-row.card-footer-row--chip-far-right") as HTMLElement;
const footerRow = container.querySelector(".card-footer-row") as HTMLElement;
const sourceChip = footerRow.querySelector(":scope > .card-source-provenance") as HTMLElement;
const retryChip = footerRow.querySelector(":scope > .card-retry-badge") as HTMLElement;
const githubChip = footerRow.querySelector(":scope > .card-github-tracking-chip") as HTMLElement;
const timerChip = footerRow.querySelector(":scope > .card-time-indicator") as HTMLElement;
const rightCluster = footerRow.querySelector(":scope > .card-footer-row-right") as HTMLElement;
const retryChip = rightCluster.querySelector(":scope > .card-retry-badge") as HTMLElement;
const githubChip = rightCluster.querySelector(":scope > .card-github-tracking-chip") as HTMLElement;
const timerChip = rightCluster.querySelector(":scope > .card-time-indicator") as HTMLElement;
expect(footerRow).toBeTruthy();
expect(sourceChip).toBeTruthy();
expect(rightCluster).toBeTruthy();
expect(retryChip).toBeTruthy();
expect(githubChip).toBeTruthy();
expect(timerChip).toBeTruthy();
expect(getComputedStyle(sourceChip).marginLeft).toBe("0px");
expect(getComputedStyle(githubChip).marginLeft).toBe("auto");
expect(getComputedStyle(retryChip).marginLeft).toBe("0px");
expect(getComputedStyle(timerChip).marginLeft).toBe("0px");
expect(getComputedStyle(sourceChip).marginLeft).not.toBe("auto");
expect(getComputedStyle(rightCluster).marginLeft).toBe("auto");
expect(Array.from(rightCluster.children).map((node) => (node as HTMLElement).className)).toEqual([
"card-github-tracking-chip card-github-tracking-link",
expect.stringContaining("card-retry-badge"),
"card-time-indicator",
]);
});
});

View File

@@ -2191,8 +2191,7 @@ describe("TaskCard", () => {
expect(trackingLink).not.toBeNull();
const css = loadAllAppCssBaseOnly();
expect(css).toMatch(/\.card-footer-row\s*>\s*\.card-source-provenance:first-of-type[\s\S]*\.card-footer-row\s*>\s*\.card-time-indicator:first-of-type\s*\{[^}]*margin-left:\s*auto;[^}]*\}/);
expect(css).toMatch(/\.card-source-provenance\s*\+\s*\.card-source-provenance[\s\S]*\.card-footer-row\s*>\s*\.card-retry-badge\s*\+\s*\.card-time-indicator\s*\{[^}]*margin-left:\s*0;[^}]*\}/);
expect(css).toMatch(/\.card-footer-row-right\s*\{[^}]*margin-left:\s*auto;[^}]*\}/);
const provenanceRule = css.match(/\.card-source-provenance\s*\{[^}]*\}/)?.[0] ?? "";
expect(provenanceRule).not.toMatch(/margin-left\s*:\s*auto/);
});
@@ -2222,20 +2221,21 @@ describe("TaskCard", () => {
const footerRow = container.querySelector(".card-footer-row");
const sourceBadge = container.querySelector(".card-footer-row > .card-source-provenance");
const trackingChip = container.querySelector(".card-footer-row > .card-github-tracking-chip");
const trackingChip = container.querySelector(".card-footer-row-right > .card-github-tracking-chip");
expect(footerRow).not.toBeNull();
expect(sourceBadge).not.toBeNull();
expect(trackingChip).not.toBeNull();
expect((sourceBadge as Element).nextElementSibling).toBe(trackingChip);
const rightCluster = container.querySelector(".card-footer-row > .card-footer-row-right");
expect(rightCluster).not.toBeNull();
expect((sourceBadge as Element).nextElementSibling).toBe(rightCluster);
const css = loadAllAppCssBaseOnly();
expect(css).toMatch(/\.card-footer-row\s*>\s*\.card-source-provenance:first-of-type[\s\S]*\.card-footer-row\s*>\s*\.card-time-indicator:first-of-type\s*\{[^}]*margin-left:\s*auto;[^}]*\}/);
expect(css).toMatch(/\.card-footer-row\s*>\s*\.card-source-provenance\s*\+\s*\.card-github-tracking-chip:first-of-type[\s\S]*\.card-footer-row\s*>\s*\.card-retry-badge\s*\+\s*\.card-time-indicator\s*\{[^}]*margin-left:\s*0;[^}]*\}/);
expect(css).toMatch(/\.card-footer-row-right\s*\{[^}]*margin-left:\s*auto;[^}]*\}/);
});
it("applies right-alignment rule when only tracking chip is rendered", () => {
const css = loadAllAppCssBaseOnly();
expect(css).toMatch(/\.card-footer-row\s*>\s*\.card-source-provenance:first-of-type[\s\S]*\.card-footer-row\s*>\s*\.card-github-tracking-chip:first-of-type[\s\S]*\{[^}]*margin-left:\s*auto;[^}]*\}/);
expect(css).toMatch(/\.card-footer-row-right\s*\{[^}]*margin-left:\s*auto;[^}]*\}/);
const { container } = render(
<TaskCard
@@ -2258,7 +2258,7 @@ describe("TaskCard", () => {
);
const footerRow = container.querySelector(".card-footer-row");
const trackingChip = container.querySelector(".card-footer-row > .card-github-tracking-chip");
const trackingChip = container.querySelector(".card-footer-row-right > .card-github-tracking-chip");
expect(footerRow).not.toBeNull();
expect(trackingChip).not.toBeNull();
expect(container.querySelector(".card-footer-row > .card-source-provenance")).toBeNull();
@@ -2293,20 +2293,17 @@ describe("TaskCard", () => {
const footerRow = container.querySelector(".card-footer-row");
expect(footerRow).not.toBeNull();
const orderedSelectors = [
".card-source-provenance",
".card-retry-badge",
".card-github-tracking-chip",
".card-time-indicator",
const sourceNode = footerRow?.querySelector(".card-source-provenance");
const rightCluster = footerRow?.querySelector(".card-footer-row-right");
expect(sourceNode).not.toBeNull();
expect(rightCluster).not.toBeNull();
const orderedNodes = [
rightCluster?.querySelector(".card-github-tracking-chip"),
rightCluster?.querySelector(".card-retry-badge"),
rightCluster?.querySelector(".card-time-indicator"),
];
const orderedNodes = orderedSelectors.map((selector) => footerRow?.querySelector(selector));
orderedNodes.forEach((node) => expect(node).not.toBeNull());
const elementChildren = Array.from((footerRow as Element).children);
const orderedIndexes = orderedNodes.map((node) => elementChildren.indexOf(node as Element));
expect(orderedIndexes[0]).toBeLessThan(orderedIndexes[1]);
expect(orderedIndexes[1]).toBeLessThan(orderedIndexes[2]);
expect(orderedIndexes[2]).toBeLessThan(orderedIndexes[3]);
expect(Array.from((rightCluster as Element).children)).toEqual(orderedNodes);
});
});
@@ -2356,15 +2353,17 @@ describe("TaskCard", () => {
expect(footerRow).toHaveClass("card-footer-row--chip-far-right");
expect(trackingChip).not.toBeNull();
expect(rightSideChip).not.toBeNull();
const children = Array.from((footerRow as HTMLElement).children);
const expectedLastChip = rightSideChip?.classList.contains("card-time-indicator") ? rightSideChip : trackingChip;
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(rightCluster).not.toBeNull();
const children = Array.from((rightCluster as HTMLElement).children);
const expectedLastChip = rightSideChip;
expect(children.at(-1)).toBe(expectedLastChip);
if (rightSideChip?.classList.contains("card-retry-badge")) {
expect(children.indexOf(rightSideChip as HTMLElement)).toBeLessThan(children.indexOf(trackingChip as HTMLElement));
expect(children.indexOf(rightSideChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
} else {
expect(children.indexOf(trackingChip as HTMLElement)).toBeLessThan(children.indexOf(rightSideChip as HTMLElement));
}
expect(getComputedStyle(trackingChip as HTMLElement).marginLeft).toBe("auto");
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
});
it.each(["in-progress", "in-review"] as const)("renders time indicator to the right of tracking chip in %s", (column) => {
@@ -2390,7 +2389,9 @@ describe("TaskCard", () => {
expect(footerRow).not.toBeNull();
expect(trackingChip).not.toBeNull();
expect(timeChip).not.toBeNull();
const children = Array.from((footerRow as HTMLElement).children);
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(rightCluster).not.toBeNull();
const children = Array.from((rightCluster as HTMLElement).children);
expect(children.indexOf(timeChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
expect(children.at(-1)).toBe(timeChip);
});
@@ -2453,15 +2454,17 @@ describe("TaskCard", () => {
expect(footerRow).toHaveClass("card-footer-row--chip-far-right");
expect(trackingChip).not.toBeNull();
expect(rightSideChip).not.toBeNull();
const children = Array.from((footerRow as HTMLElement).children);
const expectedLastChip = rightSideChip?.classList.contains("card-time-indicator") ? rightSideChip : trackingChip;
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(rightCluster).not.toBeNull();
const children = Array.from((rightCluster as HTMLElement).children);
const expectedLastChip = rightSideChip;
expect(children.at(-1)).toBe(expectedLastChip);
if (rightSideChip?.classList.contains("card-retry-badge")) {
expect(children.indexOf(rightSideChip as HTMLElement)).toBeLessThan(children.indexOf(trackingChip as HTMLElement));
expect(children.indexOf(rightSideChip as HTMLElement)).toBeGreaterThan(children.indexOf(trackingChip as HTMLElement));
} else {
expect(children.indexOf(trackingChip as HTMLElement)).toBeLessThan(children.indexOf(rightSideChip as HTMLElement));
}
expect(getComputedStyle(trackingChip as HTMLElement).marginLeft).toBe("auto");
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
});
it("does not force far-right modifier when in-review card has files changed", () => {
@@ -2521,7 +2524,9 @@ describe("TaskCard", () => {
expect(footerRow).toHaveClass("card-footer-row--chip-far-right");
expect(trackingChip).not.toBeNull();
expect(timerChip).not.toBeNull();
expect(getComputedStyle(trackingChip as HTMLElement).marginLeft).toBe("auto");
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(rightCluster).not.toBeNull();
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
expect((trackingChip as HTMLElement).nextElementSibling).toBe(timerChip);
} finally {
cleanupCss();
@@ -2556,8 +2561,10 @@ describe("TaskCard", () => {
);
const trackingChip = container.querySelector(".card-github-tracking-chip") as HTMLElement | null;
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(trackingChip).not.toBeNull();
expect(getComputedStyle(trackingChip as HTMLElement).marginLeft).toBe("auto");
expect(rightCluster).not.toBeNull();
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
} finally {
cleanupCss();
}
@@ -2598,9 +2605,11 @@ describe("TaskCard", () => {
expect(footerRow).not.toHaveClass("card-footer-row--chip-far-right");
expect(trackingChip).not.toBeNull();
expect(timerChip).not.toBeNull();
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement | null;
expect(filesChangedButton.compareDocumentPosition(trackingChip as HTMLElement)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(getComputedStyle(trackingChip as HTMLElement).marginLeft).toBe("auto");
expect(getComputedStyle(timerChip as HTMLElement).marginLeft).toBe("0px");
expect(rightCluster).not.toBeNull();
expect(getComputedStyle(rightCluster as HTMLElement).marginLeft).toBe("auto");
expect(getComputedStyle(timerChip as HTMLElement).marginLeft).not.toBe("auto");
} finally {
cleanupCss();
}
@@ -3189,7 +3198,9 @@ describe("TaskCard", () => {
expect(footerRow?.contains(filesChanged)).toBe(true);
expect(footerRow?.contains(timer)).toBe(true);
expect(header?.contains(timer)).toBe(false);
expect(Array.from(footerRow?.children ?? [])).toEqual([filesChanged, timer]);
const rightCluster = container.querySelector(".card-footer-row-right");
expect(Array.from(footerRow?.children ?? [])).toEqual([filesChanged, rightCluster]);
expect(Array.from((rightCluster as HTMLElement | null)?.children ?? [])).toEqual([timer]);
});
it("shows timer chip for in-review cards", () => {