test(FN-5160): complete Step 3 — add mobile layout contracts

Fusion-Task-Id: FN-5160
Fusion-Task-Lineage: 4817bb14-178e-4914-aa60-d0184fba653b
This commit is contained in:
Fusion (runfusion.ai)
2026-05-19 15:26:05 -07:00
committed by gsxdsm
parent 16c87c0722
commit 87c266d497
2 changed files with 163 additions and 1 deletions

View File

@@ -0,0 +1,162 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { loadAllAppCss, loadAllAppCssBaseOnly } from "../../test/cssFixture";
import { SecretsView } from "../SecretsView";
type JsonResponse = {
ok: boolean;
status?: number;
body: unknown;
};
function mockJsonResponse({ ok, status = ok ? 200 : 400, body }: JsonResponse): Response {
return {
ok,
status,
json: vi.fn().mockResolvedValue(body),
} as unknown as Response;
}
function extractRuleBlock(css: string, selector: string): string {
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = css.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\}`));
return match?.[1] ?? "";
}
function findRuleBodyContainingSelector(css: string, selector: string): string {
const ruleRegex = /([^{}]+)\{([^}]*)\}/g;
let match: RegExpExecArray | null;
while ((match = ruleRegex.exec(css)) !== null) {
const selectors = match[1]?.split(",").map((part) => part.trim()) ?? [];
if (selectors.includes(selector)) {
return match[2] ?? "";
}
}
return "";
}
function extractMobileMediaBlocks(content: string): string {
const blocks: string[] = [];
const regex = /@media\s*\(\s*max-width:\s*768px\s*\)\s*\{/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(content)) !== null) {
const startIdx = match.index + match[0].length;
let braceCount = 1;
let endIdx = startIdx;
while (braceCount > 0 && endIdx < content.length) {
if (content[endIdx] === "{") braceCount += 1;
if (content[endIdx] === "}") braceCount -= 1;
endIdx += 1;
}
if (braceCount === 0) {
blocks.push(content.slice(startIdx, endIdx - 1));
}
}
return blocks.join("\n");
}
function installCss(css: string) {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
return () => style.remove();
}
const secretsViewCss = readFileSync(resolve(__dirname, "../SecretsView.css"), "utf8");
const allCss = loadAllAppCss();
const baseCss = loadAllAppCssBaseOnly();
function seedFetchForSecretsList() {
const secrets = [
{
id: "secret-1",
scope: "project",
key: "LONG_SECRET_KEY_NAME_FOR_WRAP_TESTING",
description: null,
accessPolicy: "prompt",
envExportable: false,
envExportKey: null,
lastReadAt: null,
},
];
const fetchMock = vi
.fn()
.mockResolvedValueOnce(mockJsonResponse({ ok: true, body: { secrets } }))
.mockResolvedValueOnce(mockJsonResponse({ ok: true, body: { configured: false } }));
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
describe("SecretsView mobile layout contracts", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
it("keeps SecretsView.css token-only", () => {
const normalizedCss = secretsViewCss
.replace(/max-width:\s*768px/g, "max-width: MOBILE_BREAKPOINT")
.replace(/\b0px\b/g, "0");
expect(normalizedCss).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
expect(normalizedCss).not.toMatch(/rgba?\(/);
expect(normalizedCss).not.toMatch(/\b\d+px\b/);
});
it("keeps the mobile media block free of button and modal-close overrides", () => {
const mobileCss = extractMobileMediaBlocks(secretsViewCss);
expect(mobileCss).not.toMatch(/\.btn(?:\b|[-_])/);
expect(mobileCss).not.toMatch(/\.modal-close/);
});
it.each([".secrets-header", ".secrets-row", ".secrets-sync-header"])(
"mobile media rules stack %s as a column",
(selector) => {
const mobileCss = extractMobileMediaBlocks(secretsViewCss);
const ruleBlock = findRuleBodyContainingSelector(mobileCss, selector);
expect(ruleBlock).toMatch(/flex-direction\s*:\s*column/);
},
);
it("renders add-secret modal body with scoped spacing", async () => {
const removeCss = installCss(baseCss);
seedFetchForSecretsList();
render(<SecretsView addToast={vi.fn()} />);
await screen.findByText("LONG_SECRET_KEY_NAME_FOR_WRAP_TESTING");
await userEvent.click(screen.getByRole("button", { name: "Add Secret" }));
const modalBody = document.querySelector(".secrets-modal-body");
expect(modalBody).toBeTruthy();
expect(getComputedStyle(modalBody as Element).gap).not.toBe("");
removeCss();
});
});
describe("SecretsView revealed value styling", () => {
it("keeps revealed values scrollable within the card", () => {
const revealedBlock = extractRuleBlock(allCss, ".secrets-revealed");
expect(revealedBlock).toMatch(/overflow-x\s*:\s*auto/);
expect(revealedBlock).toMatch(/max-width\s*:\s*100%/);
});
});

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,SettingsModal,SettingsModal.worktrunk,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskChangesTab,TaskComments,TaskDetailModal,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-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.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}",