fix(FN-000): polish onboarding auth and planning modal

This commit is contained in:
gsxdsm
2026-04-22 19:24:34 -07:00
parent 9ca6b9174b
commit 0da498a0b9
7 changed files with 231 additions and 3 deletions

View File

@@ -430,6 +430,38 @@ describe("PlanningModeModal", () => {
});
});
describe("modal height constraint regression", () => {
it("desktop planning modal max-height accounts for overlay padding", async () => {
const { container } = render(
<PlanningModeModal
isOpen={true}
onClose={mockOnClose}
onTaskCreated={mockOnTaskCreated}
tasks={mockTasks}
/>
);
const modal = container.querySelector(".planning-modal");
expect(modal).toBeTruthy();
const fs = await import("fs");
const path = await import("path");
const cssPath = path.resolve(__dirname, "../styles.css");
const css = fs.readFileSync(cssPath, "utf-8");
const blockMatch = css.match(
/\.planning-modal\s*\{[^}]*max-height:\s*([^;]+);/,
);
expect(blockMatch).toBeTruthy();
const maxHeightValue = blockMatch![1].trim();
expect(maxHeightValue).toContain("min(");
expect(maxHeightValue).toContain("calc(");
expect(maxHeightValue).toContain("100dvh");
expect(maxHeightValue).toContain("--overlay-padding-top");
});
});
describe("Planning flow", () => {
it("starts planning and shows question view", async () => {
render(

View File

@@ -2,6 +2,7 @@ import { useState, useCallback } from "react";
import { X, Loader2, Sparkles, CheckCircle, ChevronRight } from "lucide-react";
import type { ProjectInfo, ProjectCreateInput } from "../api";
import { registerProject } from "../api";
import { getAuthToken, setAuthToken, clearAuthToken } from "../auth";
import { DirectoryPicker } from "./DirectoryPicker";
import { suggestProjectName } from "../utils/projectDetection";
import { useNodes } from "../hooks/useNodes";
@@ -47,6 +48,8 @@ export function SetupWizardModal({
error: null,
});
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
const [authTokenInput, setAuthTokenInput] = useState("");
const [storedAuthToken, setStoredAuthToken] = useState(() => getAuthToken());
const { nodes, loading: nodesLoading } = useNodes();
const localNodeId = nodes.find((n) => n.type === "local")?.id;
@@ -97,6 +100,20 @@ export function SetupWizardModal({
}
}, [state.manualPath, state.manualName, state.manualIsolationMode, state.manualNodeId, onProjectRegistered]);
const handleSetAuthToken = useCallback(() => {
const token = authTokenInput.trim();
if (!token) return;
setAuthToken(token);
window.location.reload();
}, [authTokenInput]);
const handleResetAuthToken = useCallback(() => {
clearAuthToken();
setStoredAuthToken(undefined);
setAuthTokenInput("");
window.location.reload();
}, []);
if (!isOpen) return null;
return (
@@ -254,6 +271,45 @@ export function SetupWizardModal({
</label>
</div>
</div>
<div className="form-group">
<label htmlFor="setup-auth-token">Browser Auth Token</label>
<div className="setup-wizard-auth-token">
<input
id="setup-auth-token"
type="password"
value={authTokenInput}
onChange={(e) => setAuthTokenInput(e.target.value)}
placeholder={storedAuthToken ? "Enter a new token to replace the stored one" : "Paste the auth token for this browser"}
autoComplete="off"
spellCheck={false}
/>
<div className="setup-wizard-auth-token-actions">
<button
type="button"
className="btn"
onClick={handleSetAuthToken}
disabled={authTokenInput.trim().length === 0}
>
{storedAuthToken ? "Update token" : "Set token"}
</button>
{storedAuthToken && (
<button
type="button"
className="btn"
onClick={handleResetAuthToken}
>
Reset token
</button>
)}
</div>
</div>
<p className="form-hint">
{storedAuthToken
? "A token is already stored in this browser. Updating or resetting it will reload the page."
: "Store a token in this browser for authenticated dashboard requests, then reload the page."}
</p>
</div>
</div>
)}
</div>

View File

@@ -45,15 +45,30 @@ vi.mock("../../api", () => ({
}),
}));
vi.mock("../../auth", () => ({
getAuthToken: vi.fn(() => undefined),
setAuthToken: vi.fn(),
clearAuthToken: vi.fn(),
}));
import { registerProject } from "../../api";
import { getAuthToken, setAuthToken, clearAuthToken } from "../../auth";
import { useNodes } from "../../hooks/useNodes";
const mockRegisterProject = vi.mocked(registerProject);
const mockGetAuthToken = vi.mocked(getAuthToken);
const mockSetAuthToken = vi.mocked(setAuthToken);
const mockClearAuthToken = vi.mocked(clearAuthToken);
const mockUseNodes = vi.mocked(useNodes);
describe("SetupWizardModal", () => {
let reloadMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
mockGetAuthToken.mockReturnValue(undefined);
reloadMock = vi.fn();
vi.stubGlobal("location", { ...window.location, reload: reloadMock });
});
it("renders with welcome message", () => {
@@ -248,6 +263,72 @@ describe("SetupWizardModal", () => {
expect(childProcessRadio.checked).toBe(true);
});
it("shows a set token action when no browser auth token is stored", () => {
render(
<SetupWizardModal
onProjectRegistered={vi.fn()}
onClose={vi.fn()}
/>
);
fireEvent.click(screen.getByText("Advanced settings"));
expect(screen.getByLabelText("Browser Auth Token")).toBeDefined();
expect(screen.getByRole("button", { name: "Set token" })).toBeDefined();
expect(screen.queryByRole("button", { name: "Reset token" })).toBeNull();
});
it("stores a browser auth token and reloads the page", () => {
render(
<SetupWizardModal
onProjectRegistered={vi.fn()}
onClose={vi.fn()}
/>
);
fireEvent.click(screen.getByText("Advanced settings"));
fireEvent.change(screen.getByLabelText("Browser Auth Token"), {
target: { value: "daemon-token" },
});
fireEvent.click(screen.getByRole("button", { name: "Set token" }));
expect(mockSetAuthToken).toHaveBeenCalledWith("daemon-token");
expect(reloadMock).toHaveBeenCalledTimes(1);
});
it("shows reset when a browser auth token is already stored", () => {
mockGetAuthToken.mockReturnValue("stored-token");
render(
<SetupWizardModal
onProjectRegistered={vi.fn()}
onClose={vi.fn()}
/>
);
fireEvent.click(screen.getByText("Advanced settings"));
expect(screen.getByRole("button", { name: "Update token" })).toBeDefined();
expect(screen.getByRole("button", { name: "Reset token" })).toBeDefined();
});
it("resets the stored browser auth token and reloads the page", () => {
mockGetAuthToken.mockReturnValue("stored-token");
render(
<SetupWizardModal
onProjectRegistered={vi.fn()}
onClose={vi.fn()}
/>
);
fireEvent.click(screen.getByText("Advanced settings"));
fireEvent.click(screen.getByRole("button", { name: "Reset token" }));
expect(mockClearAuthToken).toHaveBeenCalledTimes(1);
expect(reloadMock).toHaveBeenCalledTimes(1);
});
describe("node selector", () => {
const localNode = {
id: "local-1",