fix(FN-2444): polish node dashboard UX and discovery core handling

- Update node management UI components to use shared form/button classes, semantic color tokens, and accessible interactions
- Refine mesh topology rendering and related dashboard tests for improved readability and behavior coverage
- Adjust discovery routes to reuse injected centralCore instances without unnecessary init/close cycles
- Add/extend tests for shared CentralCore discovery flows and model settings scope save behavior
This commit is contained in:
Fusion
2026-04-24 09:38:16 -07:00
committed by gsxdsm
parent 4f633be65e
commit 8a723c86b5
19 changed files with 507 additions and 385 deletions

View File

@@ -64,6 +64,20 @@ describe("MeshTopology", () => {
expect(links).toHaveLength(1); // One line from local to remote
});
it("does not render fabricated peer links between remote nodes", () => {
const nodes = [
makeNode({ id: "local", name: "Local", type: "local" }),
makeNode({ id: "remote-1", name: "Remote 1", type: "remote" }),
makeNode({ id: "remote-2", name: "Remote 2", type: "remote" }),
makeNode({ id: "remote-3", name: "Remote 3", type: "remote" }),
];
render(<MeshTopology nodes={nodes} />);
expect(document.querySelectorAll(".mesh-topology__peer-line")).toHaveLength(0);
expect(screen.getByText("Peer-to-peer discovery data unavailable.")).toBeInTheDocument();
});
it("renders legend with status colors", () => {
render(<MeshTopology nodes={[makeNode()]} />);
@@ -123,6 +137,19 @@ describe("MeshTopology", () => {
expect(circles[0]).toHaveAttribute("fill", expect.stringContaining("var(--color-error"));
});
it("uses consistent node type badges instead of emoji glyphs", () => {
const nodes = [
makeNode({ id: "local", name: "Local", type: "local" }),
makeNode({ id: "remote", name: "Remote", type: "remote" }),
];
render(<MeshTopology nodes={nodes} />);
const typeBadges = document.querySelectorAll(".mesh-topology__node-type-badge");
expect(typeBadges).toHaveLength(2);
expect(screen.queryByText("🏠")).not.toBeInTheDocument();
expect(screen.queryByText("🌐")).not.toBeInTheDocument();
});
it("renders correct status color for connecting nodes", () => {
const nodes = [makeNode({ id: "connecting", name: "Connecting Node", status: "connecting" })];
render(<MeshTopology nodes={nodes} />);

View File

@@ -1063,11 +1063,11 @@ describe("SettingsModal", () => {
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
// Click on "Models" (global models section)
fireEvent.click(screen.getByText("Models"));
fireEvent.click((await screen.findAllByText("Models"))[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
// Select a model
const trigger = screen.getByLabelText("Default Model");
const trigger = await screen.findByLabelText("Default Model");
await user.click(trigger);
await user.click(screen.getByText("Claude Sonnet 4.5"));
@@ -1347,11 +1347,11 @@ describe("SettingsModal", () => {
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
// Models section has default model dropdown
fireEvent.click(screen.getByText("Models"));
fireEvent.click((await screen.findAllByText("Models"))[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
// Open dropdown and select "Use default"
const trigger = screen.getByLabelText("Default Model");
const trigger = await screen.findByLabelText("Default Model");
await user.click(trigger);
// Find and click the "Use default" option in the dropdown
@@ -2341,7 +2341,7 @@ describe("SettingsModal", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Notifications"));
fireEvent.click((await screen.findAllByText("Notifications"))[0]);
const checkbox = screen.getByLabelText("Enable ntfy.sh notifications");
expect(checkbox).toBeTruthy();
expect(checkbox.getAttribute("type")).toBe("checkbox");
@@ -2606,7 +2606,7 @@ describe("SettingsModal", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Notifications"));
fireEvent.click((await screen.findAllByText("Notifications"))[0]);
expect((screen.getByLabelText("Task completed (in-review)") as HTMLInputElement).checked).toBe(true);
expect((screen.getByLabelText("Task merged") as HTMLInputElement).checked).toBe(true);
expect((screen.getByLabelText("Task failed") as HTMLInputElement).checked).toBe(true);
@@ -2656,7 +2656,7 @@ describe("SettingsModal", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Notifications"));
fireEvent.click((await screen.findAllByText("Notifications"))[0]);
// Uncheck all six
fireEvent.click(screen.getByLabelText("Task completed (in-review)"));
@@ -2726,7 +2726,7 @@ describe("SettingsModal", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getByText("Node Sync"));
fireEvent.click((await screen.findAllByText("Node Sync"))[0]);
expect(screen.getByLabelText("Enable automatic settings sync")).toBeTruthy();
expect(screen.getByLabelText("Sync model auth credentials")).toBeTruthy();
expect(screen.getByLabelText("Sync interval")).toBeTruthy();
@@ -3582,7 +3582,7 @@ describe("Prompts section", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Prompts")[0]);
fireEvent.click((await screen.findAllByText("Prompts"))[0]);
// Templates tab should be active by default
expect(screen.getByTestId("tab-templates")).toHaveClass(/active/);
@@ -3612,7 +3612,7 @@ describe("Prompts section", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Prompts")[0]);
fireEvent.click((await screen.findAllByText("Prompts"))[0]);
// Click Overrides tab
fireEvent.click(screen.getByTestId("tab-overrides"));
@@ -3626,7 +3626,7 @@ describe("Prompts section", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Prompts")[0]);
fireEvent.click((await screen.findAllByText("Prompts"))[0]);
// Click Overrides tab
fireEvent.click(screen.getByTestId("tab-overrides"));
@@ -3897,8 +3897,8 @@ describe("Prompts section", () => {
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
// Verify default and fallback model dropdowns
expect(screen.getByLabelText("Default Model")).toBeTruthy();
expect(screen.getByLabelText("Fallback Model")).toBeTruthy();
expect(await screen.findByLabelText("Default Model")).toBeTruthy();
expect(await screen.findByLabelText("Fallback Model")).toBeTruthy();
});
it("renders global baseline lane dropdowns in Models section", async () => {
@@ -3922,7 +3922,7 @@ describe("Prompts section", () => {
fireEvent.click(screen.getAllByText("Models")[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
const executionTrigger = screen.getByLabelText("Execution Model");
const executionTrigger = await screen.findByLabelText("Execution Model");
await user.click(executionTrigger);
await user.click(screen.getByText("GPT-4o"));
@@ -3948,7 +3948,7 @@ describe("Prompts section", () => {
fireEvent.click(screen.getAllByText("Models")[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
const executionTrigger = screen.getByLabelText("Execution Model");
const executionTrigger = await screen.findByLabelText("Execution Model");
await user.click(executionTrigger);
const useDefaultOption = await screen.findByRole("option", { name: /use default/i });
await user.click(useDefaultOption);
@@ -3965,7 +3965,7 @@ describe("Prompts section", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Models")[0]);
fireEvent.click((await screen.findAllByText("Models"))[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
expect(screen.getByLabelText("Execution Model").textContent).toContain("Use default");
@@ -3978,7 +3978,7 @@ describe("Prompts section", () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
fireEvent.click(screen.getAllByText("Models")[0]);
fireEvent.click((await screen.findAllByText("Models"))[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
// Thinking Effort is a native select
@@ -4136,6 +4136,54 @@ describe("Prompts section", () => {
expect(globalPayload.planningModelId).toBeUndefined();
}
}, FN1712_SCOPE_TEST_TIMEOUT_MS);
it("mixed global and project changes call both endpoints with correct subsets", async () => {
render(<SettingsModal onClose={onClose} addToast={addToast} />);
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
// Change global model
fireEvent.click(screen.getAllByText("Models")[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
let user = userEvent.setup();
let dropdownBtn = screen.getByRole("button", { name: /default model/i });
await user.click(dropdownBtn);
let option = await screen.findByRole("option", { name: /gpt-4o/i });
await user.click(option);
// Change project model
fireEvent.click(screen.getAllByText("Project Models")[0]);
await waitFor(() => expect(fetchModels).toHaveBeenCalled());
dropdownBtn = screen.getByRole("button", { name: /planning model/i });
await user.click(dropdownBtn);
option = await screen.findByRole("option", { name: /claude/i });
await user.click(option);
// Save
fireEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(updateSettings).toHaveBeenCalled();
expect(updateGlobalSettings).toHaveBeenCalled();
});
const globalCalls = (updateGlobalSettings as ReturnType<typeof vi.fn>).mock.calls;
const projectCalls = (updateSettings as ReturnType<typeof vi.fn>).mock.calls;
const globalPayload = globalCalls[globalCalls.length - 1][0];
const projectPayload = projectCalls[projectCalls.length - 1][0];
// Verify global payload contains only global keys
expect(globalPayload).toHaveProperty("defaultProvider");
expect(globalPayload).toHaveProperty("defaultModelId");
expect(globalPayload.planningProvider).toBeUndefined();
expect(globalPayload.planningModelId).toBeUndefined();
// Verify project payload contains only project keys
expect(projectPayload).toHaveProperty("planningProvider");
expect(projectPayload).toHaveProperty("planningModelId");
expect(projectPayload.defaultProvider).toBeUndefined();
expect(projectPayload.defaultModelId).toBeUndefined();
}, FN1712_SCOPE_TEST_TIMEOUT_MS);
});
describe("Reset/clear null-as-delete semantics (FN-1712)", () => {

View File

@@ -3189,7 +3189,9 @@ describe("TerminalModal — FN-872 real-device keyboard overlap refinement", ()
});
// resize should have been called with xterm dimensions
expect(mockResize).toHaveBeenCalledWith(80, 24);
await waitFor(() => {
expect(mockResize).toHaveBeenCalledWith(80, 24);
});
});
it("clears --vv-height when keyboard closes", async () => {

View File

@@ -44,10 +44,10 @@ describe("nodes-view mobile CSS", () => {
expect(block).toContain("flex-wrap: wrap");
});
it("defines .nodes-view-title h2 with 16px font on mobile", () => {
it("defines .nodes-view-title h2 with tokenized font size on mobile", () => {
expect(mobileMediaBlock).toContain(".nodes-view-title h2");
const block = extractRuleBlock(mobileMediaBlock, ".nodes-view-title h2");
expect(block).toContain("font-size: 16px");
expect(block).toContain("font-size: calc(var(--space-md) + var(--space-xs));");
});
it("defines .nodes-view-title h2 svg with flex-shrink: 0 on mobile", () => {
@@ -56,17 +56,17 @@ describe("nodes-view mobile CSS", () => {
expect(block).toContain("flex-shrink: 0");
});
it("defines .nodes-view-count with smaller font on mobile", () => {
it("defines .nodes-view-count with tokenized smaller font on mobile", () => {
expect(mobileMediaBlock).toContain(".nodes-view-count");
const block = extractRuleBlock(mobileMediaBlock, ".nodes-view-count");
expect(block).toContain("font-size: 12px");
expect(block).toContain("font-size: calc(var(--space-sm) + var(--space-xs));");
});
it("defines .nodes-view-close with 36px touch target on mobile", () => {
it("defines .nodes-view-close with tokenized touch target on mobile", () => {
expect(mobileMediaBlock).toContain(".nodes-view-close");
const block = extractRuleBlock(mobileMediaBlock, ".nodes-view-close");
expect(block).toContain("min-height: 36px");
expect(block).toContain("min-width: 36px");
expect(block).toContain("min-height: calc(var(--space-xl) + var(--space-md));");
expect(block).toContain("min-width: calc(var(--space-xl) + var(--space-md));");
});
it("defines .nodes-view-actions with full width on mobile", () => {
@@ -77,10 +77,10 @@ describe("nodes-view mobile CSS", () => {
expect(block).toContain("justify-content: flex-end");
});
it("defines .nodes-view-actions .btn with min-height on mobile", () => {
it("defines .nodes-view-actions .btn with tokenized min-height on mobile", () => {
expect(mobileMediaBlock).toContain(".nodes-view-actions .btn");
const block = extractRuleBlock(mobileMediaBlock, ".nodes-view-actions .btn");
expect(block).toContain("min-height: 36px");
expect(block).toContain("min-height: calc(var(--space-xl) + var(--space-md));");
});
it("defines .nodes-view-stats with 2-column grid on mobile", () => {
@@ -96,16 +96,16 @@ describe("nodes-view mobile CSS", () => {
expect(block).toContain("padding: var(--space-xs) var(--space-sm)");
});
it("defines .nodes-view-stat span with smaller font on mobile", () => {
it("defines .nodes-view-stat span with tokenized smaller font on mobile", () => {
expect(mobileMediaBlock).toContain(".nodes-view-stat span");
const block = extractRuleBlock(mobileMediaBlock, ".nodes-view-stat span");
expect(block).toContain("font-size: 11px");
expect(block).toContain("font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);");
});
it("defines .nodes-view-stat strong with smaller font on mobile", () => {
it("defines .nodes-view-stat strong with tokenized smaller font on mobile", () => {
expect(mobileMediaBlock).toContain(".nodes-view-stat strong");
const block = extractRuleBlock(mobileMediaBlock, ".nodes-view-stat strong");
expect(block).toContain("font-size: 14px");
expect(block).toContain("font-size: calc(var(--space-md) + var(--space-xs) / 2);");
});
it("defines .nodes-view-empty with compact padding on mobile", () => {
@@ -128,9 +128,9 @@ describe("nodes-view mobile CSS", () => {
expect(block).toContain("padding: var(--space-sm) 0");
});
it("defines .nodes-view-section-title with smaller font on mobile", () => {
it("defines .nodes-view-section-title with tokenized smaller font on mobile", () => {
expect(mobileMediaBlock).toContain(".nodes-view-section-title");
const block = extractRuleBlock(mobileMediaBlock, ".nodes-view-section-title");
expect(block).toContain("font-size: 13px");
expect(block).toContain("font-size: calc(var(--space-md) + var(--space-xs) / 4);");
});
});
});

View File

@@ -770,7 +770,9 @@ describe("onboarding flow integration", () => {
expect(mockMarkOnboardingCompleted).toHaveBeenCalled();
expect(mockUpdateGlobalSettings).toHaveBeenCalledWith({ modelOnboardingComplete: true });
expect(screen.getByText("Your first task is ready!")).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("Your first task is ready!")).toBeInTheDocument();
});
});
it("task creation flow: View Task button navigates and completes onboarding", async () => {