Files
fusion/packages/dashboard/app/components/__tests__/TaskCard.footer-wrap.test.tsx
Fusion (runfusion.ai) c9ff2078b5 feat(FN-5314): add broad-scope advisory chip to TaskCard and banner to Task
Adds broad-scope advisory chips to TaskCard and banner to TaskDetailModal with comprehensive test coverage and a dashboard guide entry. TaskCard gains an inline advisory chip (with styles), TaskDetailModal gets a matching banner, and both components have dedicated test suites covering the new UI ele

Fusion-Task-Id: FN-5314

Fusion-Task-Lineage: cbc88f36-d029-4e4b-ac9c-32a77560b8b9
2026-05-20 09:42:40 -07:00

155 lines
4.7 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { render } from "@testing-library/react";
import type { Task } from "@fusion/core";
import { TaskCard } from "../TaskCard";
import { loadAllAppCss } from "../../test/cssFixture";
vi.mock("lucide-react", () => ({
Link: () => <svg />,
GitBranch: () => <svg />,
Clock: () => <svg />,
Pencil: () => <svg />,
Layers: () => <svg />,
ChevronDown: () => <svg />,
Folder: () => <svg />,
GitPullRequest: () => <svg />,
CircleDot: () => <svg />,
CheckCircle2: () => <svg />,
XCircle: () => <svg />,
Target: () => <svg />,
Bot: () => <svg />,
Trash2: () => <svg />,
RotateCw: () => <svg />,
Zap: () => <svg />,
AlertTriangle: () => <svg />,
}));
vi.mock("../ProviderIcon", () => ({
ProviderIcon: () => <span data-testid="provider-icon" />,
}));
vi.mock("../PluginSlot", () => ({
PluginSlot: () => null,
}));
vi.mock("../../hooks/useTaskDiffStats", () => ({
useTaskDiffStats: () => ({ stats: null, loading: false }),
}));
vi.mock("../../hooks/useBadgeWebSocket", () => ({
useBadgeWebSocket: () => ({
badgeUpdates: new Map(),
isConnected: true,
subscribeToBadge: vi.fn(),
unsubscribeFromBadge: vi.fn(),
}),
}));
vi.mock("../../hooks/useBatchBadgeFetch", () => ({
getFreshBatchData: vi.fn(() => null),
}));
vi.mock("../../api", () => ({
fetchTaskDetail: vi.fn(),
uploadAttachment: vi.fn(),
fetchMission: vi.fn(),
fetchAgent: vi.fn(),
}));
vi.mock("../../hooks/useConfirm", () => ({
useConfirm: () => ({ confirm: vi.fn(async () => true) }),
}));
const noop = () => {};
function makeTask(overrides: Partial<Task> = {}): Task {
return {
id: "FN-5210",
title: "Wrap footer chips when the card gets narrow",
description: "",
column: "in-progress",
status: "executing" as Task["status"],
steps: [],
dependencies: [],
sourceType: "dashboard_ui",
githubTracking: {
enabled: true,
issue: {
owner: "runfusion",
repo: "fusion",
number: 5210,
url: "https://github.com/runfusion/fusion/issues/5210",
createdAt: "2026-05-19T12:00:00.000Z",
},
},
retrySummary: { total: 3 } as Task["retrySummary"],
executionStartedAt: "2026-05-19T12:00:00.000Z",
updatedAt: "2026-05-19T12:05:00.000Z",
...overrides,
} as Task;
}
describe("TaskCard footer wrapping (FN-5210)", () => {
let cleanupCss: (() => void) | undefined;
beforeEach(async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-19T12:10:00.000Z"));
const style = document.createElement("style");
style.textContent = await Promise.resolve(loadAllAppCss());
document.head.appendChild(style);
cleanupCss = () => style.remove();
});
afterEach(() => {
cleanupCss?.();
cleanupCss = undefined;
vi.useRealTimers();
});
it("FN-5210 wraps the footer row and right chip cluster with a non-zero row gap", () => {
const { container } = render(
<TaskCard task={makeTask()} onOpenDetail={noop} addToast={noop} onOpenDetailWithTab={noop} />,
);
const footerRow = container.querySelector(".card-footer-row") as HTMLElement;
const rightCluster = container.querySelector(".card-footer-row-right") as HTMLElement;
const retryChip = container.querySelector(".card-retry-badge") as HTMLElement;
const githubChip = container.querySelector(".card-github-tracking-chip") as HTMLElement;
const timeChip = container.querySelector(".card-time-indicator") as HTMLElement;
expect(footerRow).toBeTruthy();
expect(rightCluster).toBeTruthy();
expect(retryChip).toBeTruthy();
expect(githubChip).toBeTruthy();
expect(timeChip).toBeTruthy();
const footerStyles = getComputedStyle(footerRow);
expect(footerStyles.flexWrap).toBe("wrap");
expect(footerStyles.rowGap).toMatch(/^(var\(--space-xs\)|(?!0(?:px)?$)\d+(?:\.\d+)?px)$/);
const rightClusterStyles = getComputedStyle(rightCluster);
expect(rightClusterStyles.flexWrap).toBe("wrap");
expect(rightClusterStyles.rowGap).toMatch(/^(var\(--space-xs\)|(?!0(?:px)?$)\d+(?:\.\d+)?px)$/);
expect(rightClusterStyles.justifyContent).toBe("flex-end");
});
it.each([
".card-retry-badge",
".card-github-tracking-chip",
".card-time-indicator",
])("FN-5210 keeps %s internally nowrap so wrapping happens at chip boundaries", (selector) => {
const { container } = render(
<TaskCard task={makeTask()} onOpenDetail={noop} addToast={noop} onOpenDetailWithTab={noop} />,
);
const chip = container.querySelector(selector) as HTMLElement;
expect(chip, `${selector} should render for the FN-5210 fixture`).toBeTruthy();
const styles = getComputedStyle(chip);
expect(styles.whiteSpace).toBe("nowrap");
expect(styles.flexShrink).toBe("0");
});
});