feat(FN-5210): wrap TaskCard footer chip cluster for mobile

Wraps the TaskCard footer chip cluster with CSS changes to prevent layout overflow, backed by a new regression test file and updated board-mobile tests registered in the curated dashboard test gate.

Fusion-Task-Id: FN-5210
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 20:58:30 -07:00
committed by gsxdsm
parent c047b25654
commit 35112e6070
4 changed files with 163 additions and 5 deletions

View File

@@ -656,9 +656,10 @@
display: flex;
align-items: center;
gap: var(--space-sm);
row-gap: var(--space-xs);
margin-top: var(--space-sm);
min-width: 0;
flex-wrap: nowrap;
flex-wrap: wrap;
}
.card-session-files {
@@ -708,9 +709,11 @@
display: inline-flex;
align-items: center;
gap: var(--space-sm);
row-gap: var(--space-xs);
justify-content: flex-end;
margin-left: auto;
min-width: 0;
flex-wrap: nowrap;
flex-wrap: wrap;
}
.card-source-provenance .provider-icon {
@@ -1318,6 +1321,7 @@
.card-footer-row {
margin-top: var(--space-xs);
gap: var(--space-xs);
row-gap: var(--space-xs);
}
.card-time-indicator,

View File

@@ -0,0 +1,153 @@
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 />,
}));
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");
});
});

View File

@@ -308,9 +308,10 @@ describe("TaskCard mobile", () => {
}
});
it("keeps TaskCard footer row on one line", () => {
it("wraps TaskCard footer chips instead of overflowing", () => {
const css = loadAllAppCss();
expectRuleToContain(css, ".card-footer-row", "flex-wrap: nowrap;");
expectRuleToContain(css, ".card-footer-row", "flex-wrap: wrap;");
expectRuleToContain(css, ".card-footer-row", "row-gap: var(--space-xs);");
});
it("keeps TaskCard footer-chip cluster anchored by first chip rules", () => {

View File

@@ -12,7 +12,7 @@ const qualityAppTests = [
"app/api/**/*.test.ts",
// Representative workflow/component coverage. Exhaustive modal/view suites
// stay available in the full `dashboard-app` project.
"app/components/__tests__/{ActiveAgentsPanel,ActivityLogModal,AgentMentionPopup,AgentMetricsBar,AgentOnboardingModal,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile-view-switch,ChatView,ChatView.autosize,ChatView.chat-input-autosize,ChatView.default-model-icon,ChatView.draft,ChatView.rooms,ChatView.swipe-back,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DevServerView.mobile,DirectoryPicker,DuplicateWarningModal,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,GitHubBadge,InlineCreateCard,LoginInstructions,MemoryView,MessageComposer,MessageComposer.autosize,MobileNavBar,NewTaskModal,NodeCard,NodeHealthDot,NodeStatusIndicator,PlanningModeModal.autosize,PrChecksList,PrCreateModal,PrCreateModal.layout,ProjectCard,ProjectSelector,ProviderIcon,PrPanel,PrPanel.merge,PrPanel.reviews,QuickChatFAB,ReliabilityView,ResearchView,SecretsView,SecretsView.mobile,SettingsModal,SettingsModal.worktrunk,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskChangesTab,TaskComments,TaskDetailModal,TaskDetailModal.create-pr-e2e,TaskDetailModal.create-pr-integration,TaskDetailModal.github-tracking-header,TaskDetailModal.github-tracking-stale,TaskDetailModal.rebind-banner,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,TrackingRepoSelect,WorkflowResultsTab,WorktrunkInstallApprovalDetails}.test.tsx",
"app/components/__tests__/{ActiveAgentsPanel,ActivityLogModal,AgentMentionPopup,AgentMetricsBar,AgentOnboardingModal,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile,board-mobile-view-switch,ChatView,ChatView.autosize,ChatView.chat-input-autosize,ChatView.default-model-icon,ChatView.draft,ChatView.rooms,ChatView.swipe-back,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DevServerView.mobile,DirectoryPicker,DuplicateWarningModal,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,GitHubBadge,InlineCreateCard,LoginInstructions,MemoryView,MessageComposer,MessageComposer.autosize,MobileNavBar,NewTaskModal,NodeCard,NodeHealthDot,NodeStatusIndicator,PlanningModeModal.autosize,PrChecksList,PrCreateModal,PrCreateModal.layout,ProjectCard,ProjectSelector,ProviderIcon,PrPanel,PrPanel.merge,PrPanel.reviews,QuickChatFAB,ReliabilityView,ResearchView,SecretsView,SecretsView.mobile,SettingsModal,SettingsModal.worktrunk,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskCard.footer-wrap,TaskChangesTab,TaskComments,TaskDetailModal,TaskDetailModal.create-pr-e2e,TaskDetailModal.create-pr-integration,TaskDetailModal.github-tracking-header,TaskDetailModal.github-tracking-stale,TaskDetailModal.rebind-banner,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,TrackingRepoSelect,WorkflowResultsTab,WorktrunkInstallApprovalDetails}.test.tsx",
// Hooks and utilities are fast, user-visible state/formatting behavior.
"app/context/**/*.test.tsx",
"app/hooks/__tests__/{useAgents,useAgentLogs,useAppSettings,useAuthOnboarding,useConfirm,useCurrentProject,useNodes,useNodeSettingsSync,useProjects,useQuickChat,useTasks,useTerminalSessions,useTheme,useToast,useUsageData,useViewState}.test.{ts,tsx}",