Files
fusion/packages/dashboard/app/components/__tests__/TaskCard.badge-wrap.test.tsx
gsxdsm e24980ae46 FN-7997: keep task-card agent badge text visible on mobile and narrow cards
Stop hiding agent badge labels as icon-only pills on mobile and narrow task cards; rely on existing ellipsis truncation instead.

- Remove @container and max-width media rules that set .card-agent-badge-text { display: none }
- Document the no-icon-only-label requirement with an FNXC comment
- Add regression coverage that label text stays visible with ellipsis and that CSS no longer hides it

Files changed:
 packages/dashboard/app/components/TaskCard.css     | 15 +++-------
 .../__tests__/TaskCard.badge-wrap.test.tsx         | 33 ++++++++++++++++++++++
 2 files changed, 37 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-7997

Fusion-Task-Lineage: 3add9a91-adef-4299-a638-d71257d26633

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 14:31:21 -07:00

669 lines
27 KiB
TypeScript

import React from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { render } from "@testing-library/react";
import { TaskCard } from "../TaskCard";
import type { Task } from "@fusion/core";
import { loadAllAppCss } from "../../test/cssFixture";
vi.mock("lucide-react", () => ({
Link: () => null,
GitBranch: () => null,
Clock: () => null,
Pencil: () => null,
Layers: () => null,
ChevronDown: () => null,
Folder: () => null,
GitPullRequest: () => null,
CircleDot: () => null,
CheckCircle2: () => null,
XCircle: () => null,
Target: () => null,
Bot: () => null,
Trash2: () => null,
RotateCw: () => null,
Zap: () => null,
AlertTriangle: () => null,
ArrowDown: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-arrow-down" style={style} />,
Flag: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-flag" style={style} />,
ArrowUp: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-arrow-up" style={style} />,
TriangleAlert: ({ style }: { style?: React.CSSProperties }) => <svg className="lucide-triangle-alert" style={style} />,
Eye: () => null,
MoreHorizontal: () => null,
}));
vi.mock("../ProviderIcon", () => ({
ProviderIcon: () => null,
}));
vi.mock("../PluginSlot", () => ({
PluginSlot: () => null,
}));
vi.mock("../../hooks/useTaskDiffStats", () => ({
useTaskDiffStats: () => ({ stats: null, loading: false }),
}));
vi.mock("../../hooks/useAgentsMapCache", () => ({
useAgentsMapCache: () => ({
agentsMap: new Map([["agent-ci", { name: "CI Engineer with a very long display name" }]]),
agents: [],
loading: false,
refresh: vi.fn(),
}),
}));
vi.mock("../../hooks/useToast", () => ({
useToast: () => ({
addToast: vi.fn(),
removeToast: vi.fn(),
toasts: [],
}),
}));
const noop = () => {};
const resolvedChipHeightPattern = /^(var\(--card-chip-height\)|22px)$/;
const centeredIdNudgePattern = /^translateY\(calc\(var\(--space-xs\) \/ 4\)\)$/;
function expectSharedHeaderBaseline(container: HTMLElement) {
const header = container.querySelector(".card-header") as HTMLElement;
const cardId = container.querySelector(".card-id") as HTMLElement;
const actions = container.querySelector(".card-header-actions") as HTMLElement;
expect(header).toBeTruthy();
expect(cardId).toBeTruthy();
expect(actions).toBeTruthy();
const headerStyles = getComputedStyle(header);
const idStyles = getComputedStyle(cardId);
const actionsStyles = getComputedStyle(actions);
expect(headerStyles.alignItems).toBe("flex-start");
expect(headerStyles.flexWrap).toBe("nowrap");
expect(idStyles.display).toBe("inline-flex");
expect(idStyles.alignItems).toBe("center");
expect(idStyles.lineHeight).toBe("1");
expect(idStyles.minHeight).toMatch(resolvedChipHeightPattern);
expect(idStyles.height).toMatch(resolvedChipHeightPattern);
expect(idStyles.maxHeight).toMatch(resolvedChipHeightPattern);
expect(idStyles.transform).toMatch(centeredIdNudgePattern);
expect(actionsStyles.display).toBe("flex");
expect(actionsStyles.alignItems).toBe("center");
expect(actionsStyles.transform).toBe(idStyles.transform);
expect(actionsStyles.minHeight).toMatch(resolvedChipHeightPattern);
// Locked chip-height row so a taller ⋯ touch target cannot sink the right cluster below the task id.
expect(actionsStyles.height).toMatch(resolvedChipHeightPattern);
expect(actionsStyles.maxHeight).toMatch(resolvedChipHeightPattern);
expect(actionsStyles.overflow).toBe("visible");
expect(actionsStyles.marginLeft).toBe("auto");
expect(actionsStyles.flex).toBe("0 0 auto");
}
function getCssBlocks(css: string, atRuleFragment: string): string[] {
const re = /@media[^{}]*\{/g;
const blocks: string[] = [];
for (const match of css.matchAll(re)) {
if (!match[0].includes(atRuleFragment)) continue;
const start = match.index! + match[0].length;
let depth = 1;
let i = start;
while (i < css.length && depth > 0) {
const ch = css[i];
if (ch === "{") depth++;
else if (ch === "}") depth--;
i++;
}
blocks.push(css.slice(start, i - 1));
}
expect(blocks.length).toBeGreaterThan(0);
return blocks;
}
function getCssRuleBodies(section: string, selectorFragment: string): string[] {
const bodies: string[] = [];
const pattern = /([^{}]+)\{([\s\S]*?)\}/g;
for (const match of section.matchAll(pattern)) {
if (match[1].includes(selectorFragment)) {
bodies.push(match[2]);
}
}
expect(bodies.length, `Expected CSS rule for ${selectorFragment}`).toBeGreaterThan(0);
return bodies;
}
function expectCssRuleToContain(section: string, selectorFragment: string, declaration: string): void {
const bodies = getCssRuleBodies(section, selectorFragment);
expect(bodies.some((body) => body.includes(declaration)), `${selectorFragment} should include ${declaration}`).toBe(true);
}
function expectCssRuleNotToContain(section: string, selectorFragment: string, declaration: string): void {
const bodies = getCssRuleBodies(section, selectorFragment);
for (const body of bodies) {
expect(body, `${selectorFragment} should not include ${declaration}`).not.toContain(declaration);
}
}
function expectHeaderActionsControlCenterline(container: HTMLElement, expected: {
sendBack?: boolean;
menu?: boolean;
size?: boolean;
}) {
const actions = container.querySelector(".card-header-actions") as HTMLElement;
expect(actions).toBeTruthy();
expect(getComputedStyle(actions).alignItems).toBe("center");
const sendBack = actions.querySelector(".card-send-back-btn") as HTMLElement | null;
const menu = actions.querySelector(".card-menu-btn") as HTMLElement | null;
const sizeBadge = actions.querySelector(".card-size-badge") as HTMLElement | null;
if (expected.sendBack) {
expect(sendBack).toBeTruthy();
const sendBackStyles = getComputedStyle(sendBack!);
expect(sendBackStyles.display).toBe("inline-flex");
expect(sendBackStyles.alignItems).toBe("center");
expect(sendBackStyles.lineHeight).toBe("1");
expect(sendBackStyles.minHeight).toBe("");
// Text+chevron Actions chip reads optically low vs ⋯ / size; tokenized 1px raise keeps the three on one centerline.
expect(sendBackStyles.transform).toMatch(/^translateY\(calc\(var\(--space-xs\) \/ -4\)\)$/);
} else {
expect(sendBack).toBeNull();
}
if (expected.menu) {
expect(menu).toBeTruthy();
const menuStyles = getComputedStyle(menu!);
expect(menuStyles.display).toBe("flex");
expect(menuStyles.alignItems).toBe("center");
expect(menuStyles.justifyContent).toBe("center");
expect(menuStyles.lineHeight).toBe("1");
expect(menuStyles.minHeight).toBe("");
} else {
expect(menu).toBeNull();
}
if (expected.size) {
expect(sizeBadge).toBeTruthy();
const sizeStyles = getComputedStyle(sizeBadge!);
expect(sizeStyles.display).toBe("inline-flex");
expect(sizeStyles.alignItems).toBe("center");
expect(sizeStyles.lineHeight).toBe("1");
expect(actions.contains(sizeBadge)).toBe(true);
expect(sizeBadge!.closest(".card-header-badges")).toBeNull();
} else {
expect(sizeBadge).toBeNull();
}
}
function makeTask(overrides: Partial<Task> = {}): Task {
return {
id: "FN-5162",
title: "Wrap this very long task title without letting the badge row overflow the card boundary",
column: "in-progress",
status: "planning" as Task["status"],
steps: [],
dependencies: [],
description: "",
...overrides,
} as Task;
}
describe("TaskCard badge wrapping (FN-5162)", () => {
let cleanupCss: (() => void) | undefined;
let container: HTMLElement;
let loadedCss = "";
beforeEach(async () => {
const style = document.createElement("style");
loadedCss = await Promise.resolve(loadAllAppCss());
style.textContent = loadedCss;
document.head.appendChild(style);
cleanupCss = () => style.remove();
container = render(
<TaskCard
task={makeTask({
priority: "urgent" as Task["priority"],
executionMode: "fast",
noCommitsExpected: true,
sourceType: "agent_heartbeat",
sourceAgentId: "agent-badge-wrap",
issueInfo: {
owner: "owner",
repo: "repo",
number: 42,
state: "open",
title: "Tracked issue with a long badge label",
url: "https://github.com/owner/repo/issues/42",
} as Task["issueInfo"],
})}
onOpenDetail={noop}
addToast={noop}
workflowBadge={{ workflowId: "wf-badge-wrap", workflowName: "Long workflow badge label" }}
/>,
).container;
});
afterEach(() => {
cleanupCss?.();
cleanupCss = undefined;
});
it("keeps the outer header row non-wrapping while badges wrap inside their own group", () => {
const header = container.querySelector(".card-header");
const headerBadges = container.querySelector(".card-header-badges");
expect(header).toBeTruthy();
expect(headerBadges).toBeTruthy();
const headerStyles = getComputedStyle(header!);
expect(headerStyles.flexWrap).toBe("nowrap");
expect(headerStyles.rowGap).toMatch(/^(var\(--space-xs\)|(?!0px$)\d+(?:\.\d+)?px)$/);
const badgeStyles = getComputedStyle(headerBadges!);
expect(badgeStyles.display).toBe("flex");
expect(badgeStyles.flexWrap).toBe("wrap");
expect(badgeStyles.minWidth).toBe("0px");
expect(header?.contains(headerBadges)).toBe(true);
expect(container.querySelector(".card-header-actions")).toBeNull();
});
it("keeps a fast-mode size badge in the right-aligned header actions instead of an orphaned wrapped row", () => {
const { container: sizedContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7832",
column: "done",
status: "done" as Task["status"],
size: "S",
priority: "urgent" as Task["priority"],
executionMode: "fast",
noCommitsExpected: true,
issueInfo: {
owner: "owner",
repo: "repo",
number: 7832,
state: "open",
title: "Fast-mode done card with extra header badges",
url: "https://github.com/owner/repo/issues/7832",
} as Task["issueInfo"],
})}
onOpenDetail={noop}
addToast={noop}
onArchiveTask={async () => makeTask()}
workflowBadge={{ workflowId: "wf-fast-size", workflowName: "Fast size workflow" }}
/>,
);
const header = sizedContainer.querySelector(".card-header") as HTMLElement;
const headerBadges = sizedContainer.querySelector(".card-header-badges") as HTMLElement;
const actions = sizedContainer.querySelector(".card-header-actions") as HTMLElement;
const sizeBadge = sizedContainer.querySelector(".card-size-badge") as HTMLElement;
const fastBadge = sizedContainer.querySelector(".card-execution-mode-badge") as HTMLElement;
expect(header).toBeTruthy();
expect(headerBadges).toBeTruthy();
expect(actions).toBeTruthy();
expect(sizeBadge).toBeTruthy();
expect(fastBadge).toBeTruthy();
expect(actions.contains(sizeBadge)).toBe(true);
expect(headerBadges.contains(fastBadge)).toBe(true);
expect(sizeBadge.closest(".card-header-badges")).toBeNull();
expect(actions.parentElement).toBe(header);
expect(headerBadges.parentElement).toBe(header);
const headerStyles = getComputedStyle(header);
const actionsStyles = getComputedStyle(actions);
expect(headerStyles.flexWrap).toBe("nowrap");
expect(actionsStyles.marginLeft).toBe("auto");
expect(actionsStyles.flexShrink).toBe("0");
expect(actionsStyles.alignSelf).toBe("flex-start");
expectSharedHeaderBaseline(sizedContainer);
});
it("aligns an in-progress card id with Send back and size actions while badges are present", () => {
const { container: alignedContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7862",
column: "in-progress",
status: "planning" as Task["status"],
size: "M",
priority: "urgent" as Task["priority"],
executionMode: "fast",
plannerOverseerState: { state: "monitoring" },
})}
onOpenDetail={noop}
addToast={noop}
onMoveTask={async () => makeTask()}
/>,
);
const headerBadges = alignedContainer.querySelector(".card-header-badges") as HTMLElement;
const actions = alignedContainer.querySelector(".card-header-actions") as HTMLElement;
const sizeBadge = alignedContainer.querySelector(".card-size-badge") as HTMLElement;
const sendBack = alignedContainer.querySelector(".card-send-back") as HTMLElement;
expect(headerBadges).toBeTruthy();
expect(getComputedStyle(headerBadges).alignItems).toBe("center");
expect(getComputedStyle(headerBadges).minHeight).toMatch(resolvedChipHeightPattern);
expect(sendBack).toBeTruthy();
expect(actions.contains(sendBack)).toBe(true);
expect(actions.contains(sizeBadge)).toBe(true);
expect(sizeBadge.closest(".card-header-badges")).toBeNull();
expectSharedHeaderBaseline(alignedContainer);
});
it("aligns a no-badges triage card id with edit/delete actions without shifting the id", () => {
const { container: triageContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7862-NO-BADGES",
column: "triage",
status: undefined,
size: "M",
priority: "normal" as Task["priority"],
executionMode: "standard",
plannerOversightLevel: "off",
})}
onOpenDetail={noop}
addToast={noop}
onUpdateTask={async () => makeTask()}
onDeleteTask={async () => makeTask()}
/>,
);
const actions = triageContainer.querySelector(".card-header-actions") as HTMLElement;
const sizeBadge = triageContainer.querySelector(".card-size-badge") as HTMLElement;
expect(triageContainer.querySelector(".card-header-badges")).toBeNull();
expect(actions.querySelector(".card-edit-btn")).toBeTruthy();
expect(actions.querySelector(".card-delete-btn")).toBeTruthy();
expect(actions.contains(sizeBadge)).toBe(true);
expect(sizeBadge.closest(".card-header-badges")).toBeNull();
expectSharedHeaderBaseline(triageContainer);
});
it("keeps Send back, menu, and size controls on one header-actions centerline across card states", () => {
const { container: inProgressContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7928-IN-PROGRESS",
column: "in-progress",
status: "running" as Task["status"],
size: "M",
})}
onOpenDetail={noop}
addToast={noop}
onMoveTask={async () => makeTask()}
/>,
);
expectSharedHeaderBaseline(inProgressContainer);
expectHeaderActionsControlCenterline(inProgressContainer, { sendBack: true, menu: true, size: true });
const { container: doneContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7928-DONE",
column: "done",
status: "done" as Task["status"],
size: "S",
})}
onOpenDetail={noop}
addToast={noop}
onArchiveTask={async () => makeTask()}
/>,
);
expectSharedHeaderBaseline(doneContainer);
expectHeaderActionsControlCenterline(doneContainer, { sendBack: true, menu: true, size: true });
const { container: triageContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7928-TRIAGE",
column: "triage",
status: undefined,
size: "L",
})}
onOpenDetail={noop}
addToast={noop}
onUpdateTask={async () => makeTask()}
onDeleteTask={async () => makeTask()}
/>,
);
expectSharedHeaderBaseline(triageContainer);
expectHeaderActionsControlCenterline(triageContainer, { menu: true, size: true });
const { container: menuAbsentContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7928-NO-MENU",
column: "todo",
status: "pending" as Task["status"],
size: "M",
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expectSharedHeaderBaseline(menuAbsentContainer);
expectHeaderActionsControlCenterline(menuAbsentContainer, { size: true });
const { container: sizeAbsentContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7928-NO-SIZE",
column: "in-progress",
status: "running" as Task["status"],
size: undefined,
})}
onOpenDetail={noop}
addToast={noop}
onMoveTask={async () => makeTask()}
/>,
);
expectSharedHeaderBaseline(sizeAbsentContainer);
expectHeaderActionsControlCenterline(sizeAbsentContainer, { sendBack: true, menu: true });
const { container: awaitingInputContainer } = render(
<TaskCard
task={makeTask({
id: "FN-7933-AWAITING-INPUT",
column: "in-progress",
status: "awaiting-user-input" as Task["status"],
size: "M",
})}
onOpenDetail={noop}
onOpenDetailWithTab={noop}
addToast={noop}
onMoveTask={async () => makeTask()}
/>,
);
expect(awaitingInputContainer.querySelector(".card-answer-questions-btn")).toBeTruthy();
expectSharedHeaderBaseline(awaitingInputContainer);
expectHeaderActionsControlCenterline(awaitingInputContainer, { sendBack: true, menu: true, size: true });
});
it("keeps the centered-id nudge and mobile header rhythm tokenized with the badge-wrap contract", () => {
const cardHeaderRule = loadedCss.match(/\.card-header\s*\{(?<body>[^}]*)\}/)?.groups?.body ?? "";
const cardIdRule = loadedCss.match(/\.card-id\s*\{(?<body>[^}]*)\}/)?.groups?.body ?? "";
const actionsRule = loadedCss.match(/\.card-header-actions\s*\{(?<body>[^}]*)\}/)?.groups?.body ?? "";
expect(cardHeaderRule).toContain("align-items: flex-start;");
expect(cardIdRule).toContain("min-height: var(--card-chip-height);");
expect(cardIdRule).toContain("height: var(--card-chip-height);");
expect(cardIdRule).toContain("max-height: var(--card-chip-height);");
expect(cardIdRule).toContain("line-height: 1;");
expect(cardIdRule).toContain("transform: translateY(calc(var(--space-xs) / 4));");
expect(cardIdRule).not.toMatch(/translateY\(\d/);
expect(actionsRule).toContain("align-items: center;");
expect(actionsRule).toContain("height: var(--card-chip-height);");
expect(actionsRule).toContain("max-height: var(--card-chip-height);");
expect(actionsRule).toContain("overflow: visible;");
expect(actionsRule).toContain("transform: translateY(calc(var(--space-xs) / 4));");
expect(actionsRule).not.toMatch(/translateY\(\d/);
expect(loadedCss).toContain(".card-id,\n .card-header-badges,\n .card-header-actions");
expect(loadedCss).toContain("min-height: var(--card-chip-height-mobile);");
});
it("locks the mobile Send back, menu, and size controls to one header-actions centerline", () => {
const mobileSection = getCssBlocks(loadedCss, "max-width: 768px").join("\n");
const menuTouchSection = getCssBlocks(loadedCss, "max-height: 480px").join("\n");
expectCssRuleToContain(mobileSection, ".card-header-actions", "min-height: var(--card-chip-height-mobile);");
expectCssRuleToContain(mobileSection, ".card-header-actions", "height: var(--card-chip-height-mobile);");
expectCssRuleToContain(mobileSection, ".card-header-actions", "max-height: var(--card-chip-height-mobile);");
expectCssRuleToContain(mobileSection, ".card-header-actions", "overflow: visible;");
expectCssRuleToContain(mobileSection, ".card-header-actions", "align-items: center;");
expectCssRuleToContain(mobileSection, ".card-header-actions", "gap: calc(var(--space-xs) / 2);");
// Task id and right cluster share the same locked mobile chip row so Actions/⋯/size sit on the FN-#### baseline.
expectCssRuleToContain(mobileSection, ".card-id", "height: var(--card-chip-height-mobile);");
expectCssRuleToContain(mobileSection, ".card-id", "max-height: var(--card-chip-height-mobile);");
expectCssRuleToContain(mobileSection, ".card-send-back", "height: 100%;");
expectCssRuleToContain(mobileSection, ".card-send-back", "align-items: center;");
expectCssRuleToContain(mobileSection, ".card-send-back-btn", "line-height: 1;");
expectCssRuleToContain(mobileSection, ".card-send-back-btn", "transform: translateY(calc(var(--space-xs) / -4));");
expectCssRuleToContain(mobileSection, ".card-menu-btn", "line-height: 1;");
expectCssRuleToContain(mobileSection, ".card-size-badge", "line-height: 1;");
expectCssRuleToContain(mobileSection, ".card-size-badge", "font-size: 0.5625rem;");
expectCssRuleToContain(mobileSection, ".card-size-badge", "padding: calc(var(--space-xs) / 4) calc((var(--space-xs) * 3) / 2);");
expectCssRuleToContain(mobileSection, ".card-size-badge", "padding-block: calc((var(--space-xs) / 4) + var(--btn-border-width));");
expectCssRuleNotToContain(mobileSection, ".card-send-back-btn", "min-height:");
expectCssRuleNotToContain(mobileSection, ".card-menu-btn", "min-height:");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "width: 28px;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "height: 28px;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "line-height: 1;");
// Negative vertical margin cancels residual 28px layout contribution (same pattern as .card-edit-btn/.card-delete-btn).
expectCssRuleToContain(menuTouchSection, ".card-menu-btn", "margin: -6px 0;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn svg", "width: 16px;");
expectCssRuleToContain(menuTouchSection, ".card-menu-btn svg", "height: 16px;");
});
it.each([
".card-status-badge",
".card-priority-badge",
".card-agent-created-badge",
".card-no-commits-expected-badge",
".card-github-badge",
".card-workflow-badge",
])("applies truncation constraints to %s when rendered", (selector) => {
const badge = container.querySelector(selector);
expect(badge, `${selector} should render for the fixture`).toBeTruthy();
const styles = getComputedStyle(badge as Element);
expect(styles.maxWidth).not.toBe("none");
expect(styles.whiteSpace).toBe("nowrap");
});
it("keeps assigned-agent badge text visible and truncated in mobile and narrow-card CSS", () => {
const { container: assignedAgentContainer } = render(
<TaskCard
task={makeTask({ assignedAgentId: "agent-ci" })}
onOpenDetail={noop}
addToast={noop}
/>,
);
const label = assignedAgentContainer.querySelector(".card-agent-badge-text") as HTMLElement;
expect(label).toBeTruthy();
expect(label.textContent).toBe("CI Engineer ...");
const labelStyles = getComputedStyle(label);
expect(labelStyles.display).not.toBe("none");
expect(labelStyles.overflow).toBe("hidden");
expect(labelStyles.textOverflow).toBe("ellipsis");
expect(labelStyles.whiteSpace).toBe("nowrap");
// jsdom does not apply media or container queries, so lock the source declaration that would otherwise hide this visible label.
const mobileSection = getCssBlocks(loadedCss, "max-width: 768px").join("\n");
expect(mobileSection).not.toMatch(/\.card-agent-badge-text\s*\{[^}]*display\s*:\s*none/);
expect(loadedCss).not.toMatch(/@container\s+task-card\s*\(max-width:\s*240px\)\s*\{\s*\.card-agent-badge-text\s*\{[^}]*display\s*:\s*none/);
});
it("places the agent badge in a left-aligned bottom row outside the header badge cluster", () => {
const agentRow = container.querySelector(".card-agent-badge-row") as HTMLElement;
const agentBadge = container.querySelector(".card-agent-created-badge") as HTMLElement;
const header = container.querySelector(".card-header") as HTMLElement;
const metaBadges = container.querySelector(".card-meta-badges") as HTMLElement;
expect(agentRow).toBeTruthy();
expect(agentBadge).toBeTruthy();
expect(agentRow.contains(agentBadge)).toBe(true);
expect(header.contains(agentBadge)).toBe(false);
expect(metaBadges.contains(agentBadge)).toBe(false);
expect(agentBadge.getAttribute("title")).toBe("Created by agent: agent-badge-wrap");
expect(agentBadge.getAttribute("aria-label")).toBe("Created by agent: agent-badge-wrap");
expect(agentBadge.querySelector(".visually-hidden")?.textContent).toBe("Created by agent: agent-badge-wrap");
expect(agentBadge.querySelector("span[aria-hidden='true']")?.textContent).toBe("agent-badge-...");
const styles = getComputedStyle(agentRow);
expect(styles.display).toBe("flex");
expect(styles.justifyContent).toBe("flex-start");
expect(styles.minWidth).toBe("0px");
});
it("does not render an empty header badge shell when the agent badge is the only grouped affordance", () => {
const { container: agentOnlyContainer } = render(
<TaskCard
task={makeTask({
priority: "normal" as Task["priority"],
executionMode: "standard",
sourceType: "agent_heartbeat",
sourceAgentId: "agent-only",
plannerOversightLevel: "off",
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
const agentBadge = agentOnlyContainer.querySelector(".card-agent-created-badge");
expect(agentBadge).not.toBeNull();
expect(agentBadge?.closest(".card-agent-badge-row")).not.toBeNull();
expect(agentBadge?.closest(".card-header")).toBeNull();
expect(agentOnlyContainer.querySelector(".card-meta-badges")).toBeNull();
});
it("omits the agent bottom row for non-agent-created tasks", () => {
const { container: nonAgentContainer } = render(
<TaskCard
task={makeTask({
priority: "normal" as Task["priority"],
executionMode: "standard",
sourceType: "dashboard_ui",
sourceAgentId: undefined,
plannerOversightLevel: "off",
})}
onOpenDetail={noop}
addToast={noop}
/>,
);
expect(nonAgentContainer.querySelector(".card-agent-created-badge")).toBeNull();
expect(nonAgentContainer.querySelector(".card-agent-badge-row")).toBeNull();
expect(nonAgentContainer.querySelector(".card-meta-badges")).toBeNull();
});
it("places the workflow badge in a left-aligned bottom row outside the header badge cluster", () => {
const workflowRow = container.querySelector(".card-workflow-badge-row") as HTMLElement;
const workflowBadge = container.querySelector(".card-workflow-badge") as HTMLElement;
const metaBadges = container.querySelector(".card-meta-badges") as HTMLElement;
const agentRow = container.querySelector(".card-agent-badge-row") as HTMLElement;
expect(workflowRow).toBeTruthy();
expect(workflowBadge).toBeTruthy();
expect(workflowRow.contains(workflowBadge)).toBe(true);
expect(metaBadges.contains(workflowBadge)).toBe(false);
expect(agentRow.compareDocumentPosition(workflowRow) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
const styles = getComputedStyle(workflowRow);
expect(styles.display).toBe("flex");
expect(styles.justifyContent).toBe("flex-start");
expect(styles.minWidth).toBe("0px");
});
});