feat(FN-3398): update desktop README with multi-project setup and troublesh
The merge lands Step 7 of FN-3398, adding documentation and delivery artifacts across the mobile and desktop packages with updated READMEs, mobile-specific docs, and architecture references. Fusion-Task-Id: FN-3398
This commit is contained in:
@@ -2,12 +2,14 @@ interface BackendConnectionErrorPageProps {
|
||||
errorMessage: string;
|
||||
isRetrying: boolean;
|
||||
onRetry: () => void;
|
||||
onManageConnection?: () => void;
|
||||
}
|
||||
|
||||
export function BackendConnectionErrorPage({
|
||||
errorMessage,
|
||||
isRetrying,
|
||||
onRetry,
|
||||
onManageConnection,
|
||||
}: BackendConnectionErrorPageProps) {
|
||||
return (
|
||||
<div className="project-overview-empty" role="alert" aria-live="polite">
|
||||
@@ -16,9 +18,16 @@ export function BackendConnectionErrorPage({
|
||||
Fusion couldn't load your projects right now. Please make sure the backend is running and try again.
|
||||
</p>
|
||||
<p className="settings-muted">Error: {errorMessage}</p>
|
||||
<button type="button" className="btn btn-primary" onClick={onRetry} disabled={isRetrying}>
|
||||
{isRetrying ? "Retrying…" : "Retry Connection"}
|
||||
</button>
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn btn-primary" onClick={onRetry} disabled={isRetrying}>
|
||||
{isRetrying ? "Retrying…" : "Retry Connection"}
|
||||
</button>
|
||||
{onManageConnection && (
|
||||
<button type="button" className="btn" onClick={onManageConnection}>
|
||||
Manage Connection
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useCallback, useMemo, type KeyboardEvent as ReactKeyboardEvent } from "react";
|
||||
import { useState, useEffect, useRef, useCallback, useMemo, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from "react";
|
||||
import { Settings, Pause, Play, Square, LayoutGrid, List, Terminal, Lightbulb, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Server, Workflow, Bot, Target, ChevronRight, FileCode, Loader2, Grid3X3, Mail, MessageSquare, ChevronDown, Check, Zap, Sparkles, FileText, Brain, CheckSquare } from "lucide-react";
|
||||
import "./Header.css";
|
||||
// Header renders an inline ProjectSelector dropdown using project-selector-* classes.
|
||||
@@ -228,6 +228,7 @@ export interface HeaderProps {
|
||||
/** Experimental feature flags controlling visibility of nav items. */
|
||||
experimentalFeatures?: { insights?: boolean; roadmap?: boolean; memoryView?: boolean; devServer?: boolean; devServerView?: boolean; researchView?: boolean };
|
||||
pluginDashboardViews?: PluginDashboardViewEntry[];
|
||||
shellConnectionControl?: ReactNode;
|
||||
}
|
||||
|
||||
export function Header({
|
||||
@@ -277,6 +278,7 @@ export function Header({
|
||||
isRemote = false,
|
||||
experimentalFeatures,
|
||||
pluginDashboardViews = [],
|
||||
shellConnectionControl,
|
||||
}: HeaderProps) {
|
||||
const mode: ViewportMode = useViewportMode();
|
||||
const isMobile = mode === "mobile";
|
||||
@@ -992,6 +994,7 @@ export function Header({
|
||||
</div>
|
||||
|
||||
<div className="header-actions">
|
||||
{shellConnectionControl}
|
||||
{/* Mobile View Toggle - compact board/list switcher in header when mobile nav is active */}
|
||||
{hideFullNav && onChangeView && (view === "board" || view === "list") && (
|
||||
<div className="view-toggle" data-testid="mobile-view-toggle">
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
.native-shell-connection-manager {
|
||||
width: min(100%, 42rem);
|
||||
}
|
||||
|
||||
.native-shell-connection-manager__mode-row {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
padding: 0 var(--space-xl) var(--space-md);
|
||||
}
|
||||
|
||||
.native-shell-connection-manager__profiles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
padding: 0 var(--space-xl);
|
||||
max-height: 16rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.native-shell-connection-manager__profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.native-shell-connection-manager__profile-actions {
|
||||
display: inline-flex;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.native-shell-connection-manager__editor {
|
||||
padding-top: var(--space-lg);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.native-shell-connection-manager__profile {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import type { FusionShellApi, ShellConnectionProfile, ShellConnectionState } from "../types/native-shell";
|
||||
import "./NativeShellConnectionManager.css";
|
||||
|
||||
interface NativeShellConnectionManagerProps {
|
||||
open: boolean;
|
||||
shellApi: FusionShellApi;
|
||||
shellState: ShellConnectionState;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function NativeShellConnectionManager({ open, shellApi, shellState, onClose }: NativeShellConnectionManagerProps) {
|
||||
const activeProfile = useMemo(
|
||||
() => shellState.profiles.find((profile) => profile.id === shellState.activeProfileId) ?? null,
|
||||
[shellState.activeProfileId, shellState.profiles],
|
||||
);
|
||||
const [draft, setDraft] = useState<Partial<ShellConnectionProfile>>({});
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const workingName = draft.name ?? activeProfile?.name ?? "";
|
||||
const workingUrl = draft.serverUrl ?? activeProfile?.serverUrl ?? "";
|
||||
const workingToken = draft.authToken ?? activeProfile?.authToken ?? "";
|
||||
|
||||
const saveCurrent = async () => {
|
||||
setError(null);
|
||||
try {
|
||||
const saved = await shellApi.saveProfile({
|
||||
id: activeProfile?.id,
|
||||
name: workingName || "Remote Server",
|
||||
serverUrl: workingUrl,
|
||||
authToken: workingToken || null,
|
||||
});
|
||||
await shellApi.setActiveProfile(saved.id);
|
||||
setDraft({});
|
||||
} catch (nextError) {
|
||||
setError((nextError as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay open">
|
||||
<div className="modal native-shell-connection-manager" role="dialog" aria-label="Connection Manager">
|
||||
<div className="modal-header">
|
||||
<h2>Connection Manager</h2>
|
||||
<button type="button" className="modal-close" onClick={onClose} aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{shellState.host === "desktop-shell" && (
|
||||
<div className="native-shell-connection-manager__mode-row">
|
||||
<button type="button" className={`btn ${shellState.desktopMode === "local" ? "btn-primary" : ""}`} onClick={() => void shellApi.setDesktopMode("local")}>Local</button>
|
||||
<button type="button" className={`btn ${shellState.desktopMode !== "local" ? "btn-primary" : ""}`} onClick={() => void shellApi.setDesktopMode("remote")}>Remote</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="native-shell-connection-manager__profiles">
|
||||
{shellState.profiles.map((profile) => (
|
||||
<div className="card native-shell-connection-manager__profile" key={profile.id}>
|
||||
<div>
|
||||
<strong>{profile.name}</strong>
|
||||
<div className="settings-muted">{profile.serverUrl}</div>
|
||||
</div>
|
||||
<div className="native-shell-connection-manager__profile-actions">
|
||||
<button type="button" className="btn btn-sm" onClick={() => setDraft(profile)}>Edit</button>
|
||||
<button type="button" className="btn btn-sm" onClick={() => void shellApi.setActiveProfile(profile.id)}>Use</button>
|
||||
<button type="button" className="btn btn-sm btn-danger" onClick={() => void shellApi.deleteProfile(profile.id)}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="form-group native-shell-connection-manager__editor">
|
||||
<label htmlFor="native-shell-connection-manager-name">Name</label>
|
||||
<input id="native-shell-connection-manager-name" className="input" value={workingName} onChange={(event) => setDraft((value) => ({ ...value, name: event.target.value }))} />
|
||||
<label htmlFor="native-shell-connection-manager-url">Server URL</label>
|
||||
<input id="native-shell-connection-manager-url" className="input" value={workingUrl} onChange={(event) => setDraft((value) => ({ ...value, serverUrl: event.target.value }))} />
|
||||
<label htmlFor="native-shell-connection-manager-token">Auth token (optional)</label>
|
||||
<input id="native-shell-connection-manager-token" className="input" value={workingToken ?? ""} onChange={(event) => setDraft((value) => ({ ...value, authToken: event.target.value }))} />
|
||||
{error && <p className="form-error">{error}</p>}
|
||||
</div>
|
||||
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn" onClick={onClose}>Close</button>
|
||||
<button type="button" className="btn btn-primary" onClick={() => void saveCurrent()} disabled={!workingUrl.trim()}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.native-shell-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.native-shell-status__label {
|
||||
max-width: 18ch;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.native-shell-status__label {
|
||||
max-width: 12ch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { ShellConnectionState } from "../types/native-shell";
|
||||
import "./NativeShellConnectionStatus.css";
|
||||
|
||||
interface NativeShellConnectionStatusProps {
|
||||
state: ShellConnectionState;
|
||||
onManage: () => void;
|
||||
}
|
||||
|
||||
export function NativeShellConnectionStatus({ state, onManage }: NativeShellConnectionStatusProps) {
|
||||
const activeProfile = state.profiles.find((profile) => profile.id === state.activeProfileId) ?? null;
|
||||
const label =
|
||||
state.host === "desktop-shell" && state.desktopMode === "local"
|
||||
? "Local Fusion"
|
||||
: activeProfile?.name ?? "Disconnected";
|
||||
|
||||
return (
|
||||
<button type="button" className="btn native-shell-status" onClick={onManage} data-testid="native-shell-status-btn">
|
||||
<span className={`status-dot ${activeProfile || state.desktopMode === "local" ? "status-dot--online" : "status-dot--error"}`} aria-hidden="true" />
|
||||
<span className="native-shell-status__label">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
.native-shell-onboarding-modal {
|
||||
width: min(100%, calc(var(--space-2xl) * 20));
|
||||
}
|
||||
|
||||
.native-shell-onboarding-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
padding: 0 var(--space-xl) var(--space-xl);
|
||||
}
|
||||
|
||||
.native-shell-onboarding-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.native-shell-onboarding-mode-row {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.native-shell-onboarding-modal {
|
||||
width: calc(100% - var(--space-lg));
|
||||
}
|
||||
|
||||
.native-shell-onboarding-mode-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
127
packages/dashboard/app/components/NativeShellOnboardingModal.tsx
Normal file
127
packages/dashboard/app/components/NativeShellOnboardingModal.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { FusionShellApi, ShellConnectionState } from "../types/native-shell";
|
||||
import "./NativeShellOnboardingModal.css";
|
||||
|
||||
function buildRemoteDashboardUrl(serverUrl: string, authToken?: string | null): string {
|
||||
const url = new URL(serverUrl);
|
||||
if (authToken) {
|
||||
url.searchParams.set("token", authToken);
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
interface NativeShellOnboardingModalProps {
|
||||
open: boolean;
|
||||
shellApi: FusionShellApi;
|
||||
shellState: ShellConnectionState;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export function NativeShellOnboardingModal({ open, shellApi, shellState, onComplete }: NativeShellOnboardingModalProps) {
|
||||
const [mode, setMode] = useState<"local" | "remote">(shellState.desktopMode ?? "remote");
|
||||
const [name, setName] = useState("Remote Server");
|
||||
const [serverUrl, setServerUrl] = useState("");
|
||||
const [authToken, setAuthToken] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const isDesktop = shellState.host === "desktop-shell";
|
||||
|
||||
useEffect(() => {
|
||||
if (isDesktop) {
|
||||
setMode(shellState.desktopMode ?? "remote");
|
||||
}
|
||||
}, [isDesktop, shellState.desktopMode]);
|
||||
const canSubmit = useMemo(() => {
|
||||
if (isDesktop && mode === "local") return true;
|
||||
return serverUrl.trim().length > 0;
|
||||
}, [isDesktop, mode, serverUrl]);
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-overlay open">
|
||||
<div className="modal native-shell-onboarding-modal">
|
||||
<div className="modal-header">
|
||||
<h2>Welcome to Fusion</h2>
|
||||
</div>
|
||||
<div className="native-shell-onboarding-body">
|
||||
<p>Fusion helps you plan, run, and review AI-assisted engineering work.</p>
|
||||
{isDesktop && (
|
||||
<div className="native-shell-onboarding-mode-row">
|
||||
<button type="button" className={`btn ${mode === "local" ? "btn-primary" : ""}`} onClick={() => setMode("local")}>Local Fusion</button>
|
||||
<button type="button" className={`btn ${mode === "remote" ? "btn-primary" : ""}`} onClick={() => setMode("remote")}>Remote Server</button>
|
||||
</div>
|
||||
)}
|
||||
{(!isDesktop || mode === "remote") && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
onClick={async () => {
|
||||
setError(null);
|
||||
try {
|
||||
const result = await shellApi.startQrScan();
|
||||
setServerUrl(result.serverUrl);
|
||||
setAuthToken(result.authToken ?? "");
|
||||
} catch (scanError) {
|
||||
setError((scanError as Error).message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Scan QR
|
||||
</button>
|
||||
<label className="native-shell-onboarding-label" htmlFor="native-shell-onboarding-profile-name">Profile name</label>
|
||||
<input id="native-shell-onboarding-profile-name" className="input" value={name} onChange={(event) => setName(event.target.value)} />
|
||||
<label className="native-shell-onboarding-label" htmlFor="native-shell-onboarding-server-url">Server URL</label>
|
||||
<input id="native-shell-onboarding-server-url" className="input" value={serverUrl} onChange={(event) => setServerUrl(event.target.value)} placeholder="https://your-fusion-host" />
|
||||
<label className="native-shell-onboarding-label" htmlFor="native-shell-onboarding-auth-token">Auth token (optional)</label>
|
||||
<input id="native-shell-onboarding-auth-token" className="input" value={authToken} onChange={(event) => setAuthToken(event.target.value)} />
|
||||
</>
|
||||
)}
|
||||
{error && <p className="form-error">{error}</p>}
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={!canSubmit}
|
||||
onClick={async () => {
|
||||
setError(null);
|
||||
try {
|
||||
if (isDesktop && mode === "local") {
|
||||
await shellApi.setDesktopMode("local");
|
||||
onComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
const saved = await shellApi.saveProfile({
|
||||
name: name.trim() || "Remote Server",
|
||||
serverUrl,
|
||||
authToken: authToken || null,
|
||||
});
|
||||
|
||||
if (isDesktop) {
|
||||
await shellApi.setDesktopMode("remote");
|
||||
}
|
||||
await shellApi.setActiveProfile(saved.id);
|
||||
|
||||
if (typeof window !== "undefined" && shellState.host !== "web") {
|
||||
window.location.href = buildRemoteDashboardUrl(saved.serverUrl, saved.authToken ?? null);
|
||||
return;
|
||||
}
|
||||
|
||||
onComplete();
|
||||
} catch (submitError) {
|
||||
setError((submitError as Error).message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { NativeShellConnectionManager } from "../NativeShellConnectionManager";
|
||||
|
||||
function createShellApi() {
|
||||
return {
|
||||
getState: vi.fn(),
|
||||
listProfiles: vi.fn(),
|
||||
saveProfile: vi.fn(async () => ({ id: "p1", name: "Prod", serverUrl: "https://fusion.example.com", createdAt: "", updatedAt: "" })),
|
||||
deleteProfile: vi.fn(async () => undefined),
|
||||
setActiveProfile: vi.fn(async () => ({ host: "mobile-shell", activeProfileId: "p1", profiles: [] })),
|
||||
setDesktopMode: vi.fn(async () => ({ host: "desktop-shell", desktopMode: "remote", activeProfileId: null, profiles: [] })),
|
||||
startQrScan: vi.fn(),
|
||||
openConnectionManager: vi.fn(),
|
||||
subscribe: vi.fn(() => () => undefined),
|
||||
};
|
||||
}
|
||||
|
||||
describe("NativeShellConnectionManager", () => {
|
||||
it("switches desktop mode", async () => {
|
||||
const shellApi = createShellApi();
|
||||
render(
|
||||
<NativeShellConnectionManager
|
||||
open={true}
|
||||
shellApi={shellApi}
|
||||
shellState={{ host: "desktop-shell", desktopMode: "remote", activeProfileId: null, profiles: [] }}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Local"));
|
||||
await waitFor(() => expect(shellApi.setDesktopMode).toHaveBeenCalledWith("local"));
|
||||
});
|
||||
|
||||
it("edits and saves active profile", async () => {
|
||||
const shellApi = createShellApi();
|
||||
render(
|
||||
<NativeShellConnectionManager
|
||||
open={true}
|
||||
shellApi={shellApi}
|
||||
shellState={{ host: "mobile-shell", activeProfileId: "p1", profiles: [{ id: "p1", name: "Prod", serverUrl: "https://fusion.example.com", authToken: null, createdAt: "", updatedAt: "" }] }}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue("https://fusion.example.com"), { target: { value: "https://next.example.com" } });
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(shellApi.saveProfile).toHaveBeenCalled();
|
||||
expect(shellApi.setActiveProfile).toHaveBeenCalledWith("p1");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { NativeShellConnectionStatus } from "../NativeShellConnectionStatus";
|
||||
|
||||
describe("NativeShellConnectionStatus", () => {
|
||||
it("shows local label for desktop local mode", () => {
|
||||
render(
|
||||
<NativeShellConnectionStatus
|
||||
state={{ host: "desktop-shell", desktopMode: "local", activeProfileId: null, profiles: [] }}
|
||||
onManage={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Local Fusion")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens manager when clicked", () => {
|
||||
const onManage = vi.fn();
|
||||
render(
|
||||
<NativeShellConnectionStatus
|
||||
state={{ host: "mobile-shell", activeProfileId: "p1", profiles: [{ id: "p1", name: "Prod", serverUrl: "https://fusion.example.com", createdAt: "", updatedAt: "" }] }}
|
||||
onManage={onManage}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId("native-shell-status-btn"));
|
||||
expect(onManage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { NativeShellOnboardingModal } from "../NativeShellOnboardingModal";
|
||||
|
||||
describe("NativeShellOnboardingModal", () => {
|
||||
it("shows desktop mode options", () => {
|
||||
render(
|
||||
<NativeShellOnboardingModal
|
||||
open={true}
|
||||
shellApi={{
|
||||
getState: vi.fn(),
|
||||
listProfiles: vi.fn(),
|
||||
saveProfile: vi.fn(),
|
||||
deleteProfile: vi.fn(),
|
||||
setActiveProfile: vi.fn(),
|
||||
setDesktopMode: vi.fn(),
|
||||
startQrScan: vi.fn(),
|
||||
openConnectionManager: vi.fn(),
|
||||
subscribe: vi.fn(() => () => undefined),
|
||||
}}
|
||||
shellState={{ host: "desktop-shell", desktopMode: "remote", activeProfileId: null, profiles: [] }}
|
||||
onComplete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Local Fusion")).toBeInTheDocument();
|
||||
expect(screen.getByText("Remote Server")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("saves remote profile and redirects to remote dashboard", async () => {
|
||||
const saveProfile = vi.fn(async () => ({ id: "p1", serverUrl: "https://fusion.example.com", authToken: null }));
|
||||
const setActiveProfile = vi.fn(async () => ({ host: "mobile-shell", activeProfileId: "p1", profiles: [] }));
|
||||
const onComplete = vi.fn();
|
||||
const originalLocation = window.location;
|
||||
Object.defineProperty(window, "location", {
|
||||
configurable: true,
|
||||
value: { ...originalLocation, href: "http://localhost" },
|
||||
});
|
||||
|
||||
render(
|
||||
<NativeShellOnboardingModal
|
||||
open={true}
|
||||
shellApi={{
|
||||
getState: vi.fn(),
|
||||
listProfiles: vi.fn(),
|
||||
saveProfile,
|
||||
deleteProfile: vi.fn(),
|
||||
setActiveProfile,
|
||||
setDesktopMode: vi.fn(),
|
||||
startQrScan: vi.fn(),
|
||||
openConnectionManager: vi.fn(),
|
||||
subscribe: vi.fn(() => () => undefined),
|
||||
}}
|
||||
shellState={{ host: "mobile-shell", activeProfileId: null, profiles: [] }}
|
||||
onComplete={onComplete}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("https://your-fusion-host"), { target: { value: "https://fusion.example.com" } });
|
||||
fireEvent.click(screen.getByText("Continue"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(saveProfile).toHaveBeenCalled();
|
||||
expect(setActiveProfile).toHaveBeenCalledWith("p1");
|
||||
expect(window.location.href).toContain("https://fusion.example.com");
|
||||
});
|
||||
|
||||
Object.defineProperty(window, "location", { configurable: true, value: originalLocation });
|
||||
expect(onComplete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user