feat(FN-1705): merge fusion/fn-1705
This commit is contained in:
@@ -28,7 +28,7 @@ Like Trello, but your tasks get specified, executed, and delivered by AI — pow
|
|||||||
2. **GitHub (Optional)** — Connect GitHub for issue import and PR management
|
2. **GitHub (Optional)** — Connect GitHub for issue import and PR management
|
||||||
3. **First Task** — Create your first task or import from GitHub
|
3. **First Task** — Create your first task or import from GitHub
|
||||||
|
|
||||||
The wizard is **dismissible and non-blocking** — click **Skip for now** to dismiss it and use the dashboard immediately. After dismissing, a **Continue Setup** banner appears at the top of the dashboard, letting you resume from where you left off. You can also re-trigger onboarding later from Settings → Authentication, or by clearing the `modelOnboardingComplete` flag in global settings.
|
The wizard is **dismissible and non-blocking** — click **Skip for now** to dismiss it and use the dashboard immediately. After dismissing, a **Continue Setup** banner appears at the top of the dashboard, letting you resume from where you left off. You can also re-trigger onboarding later from **Settings → Authentication → Reopen onboarding guide**. When reopening, the wizard pre-populates your saved AI provider and default model for quick review.
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ On first launch, Fusion automatically opens the **onboarding wizard**. It guides
|
|||||||
**The onboarding wizard is dismissible and non-blocking.** If you skip setup, you can complete it later:
|
**The onboarding wizard is dismissible and non-blocking.** If you skip setup, you can complete it later:
|
||||||
- Click **Skip for now** to dismiss the wizard — the dashboard remains fully usable
|
- Click **Skip for now** to dismiss the wizard — the dashboard remains fully usable
|
||||||
- After dismissing, a **Continue Setup** banner appears at the top of the dashboard, letting you resume from where you left off
|
- After dismissing, a **Continue Setup** banner appears at the top of the dashboard, letting you resume from where you left off
|
||||||
- Re-open onboarding anytime from **Settings → Authentication**, or by clearing the `modelOnboardingComplete` flag in global settings
|
- Re-open onboarding anytime from **Settings → Authentication → Reopen onboarding guide**
|
||||||
|
|
||||||
|
When reopening onboarding, the wizard pre-populates your previously saved AI provider and default model, so you can quickly review or update your setup.
|
||||||
|
|
||||||
Onboarding completion is tracked by `modelOnboardingComplete` in global settings.
|
Onboarding completion is tracked by `modelOnboardingComplete` in global settings.
|
||||||
|
|
||||||
|
|||||||
@@ -565,6 +565,10 @@ function AppInner() {
|
|||||||
modalManager.closeSettings();
|
modalManager.closeSettings();
|
||||||
void refreshAppSettings();
|
void refreshAppSettings();
|
||||||
}}
|
}}
|
||||||
|
onReopenOnboarding={() => {
|
||||||
|
modalManager.closeSettings();
|
||||||
|
modalManager.openModelOnboarding();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ interface AppModalsProps {
|
|||||||
};
|
};
|
||||||
/** Optional override for the settings modal close handler. When provided, this is called instead of modalManager.closeSettings. */
|
/** Optional override for the settings modal close handler. When provided, this is called instead of modalManager.closeSettings. */
|
||||||
onSettingsClose?: () => void;
|
onSettingsClose?: () => void;
|
||||||
|
/** Optional callback to reopen the onboarding guide from Settings. Closes Settings and opens ModelOnboardingModal. */
|
||||||
|
onReopenOnboarding?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppModals({
|
export function AppModals({
|
||||||
@@ -72,6 +74,7 @@ export function AppModals({
|
|||||||
deepLink,
|
deepLink,
|
||||||
settings,
|
settings,
|
||||||
onSettingsClose,
|
onSettingsClose,
|
||||||
|
onReopenOnboarding,
|
||||||
}: AppModalsProps) {
|
}: AppModalsProps) {
|
||||||
// Use the override handler if provided, otherwise fall back to modalManager.closeSettings
|
// Use the override handler if provided, otherwise fall back to modalManager.closeSettings
|
||||||
const handleSettingsClose = onSettingsClose ?? modalManager.closeSettings;
|
const handleSettingsClose = onSettingsClose ?? modalManager.closeSettings;
|
||||||
@@ -110,6 +113,7 @@ export function AppModals({
|
|||||||
colorTheme={settings.colorTheme}
|
colorTheme={settings.colorTheme}
|
||||||
onThemeModeChange={settings.setThemeMode}
|
onThemeModeChange={settings.setThemeMode}
|
||||||
onColorThemeChange={settings.setColorTheme}
|
onColorThemeChange={settings.setColorTheme}
|
||||||
|
onReopenOnboarding={onReopenOnboarding}
|
||||||
/>
|
/>
|
||||||
</ModalErrorBoundary>
|
</ModalErrorBoundary>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -103,12 +103,26 @@ export function ModelOnboardingModal({
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Load global settings to hydrate saved default model (for reopen flow)
|
||||||
|
const loadGlobalSettings = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const globalSettings = await fetchGlobalSettings();
|
||||||
|
// If a default model is configured, pre-select it
|
||||||
|
if (globalSettings.defaultProvider && globalSettings.defaultModelId) {
|
||||||
|
const defaultModelValue = `${globalSettings.defaultProvider}/${globalSettings.defaultModelId}`;
|
||||||
|
setSelectedModel(defaultModelValue);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silently fail - onboarding still works without hydration
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Initial data load
|
// Initial data load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([loadAuthStatus(), loadModels()]).finally(() =>
|
Promise.all([loadAuthStatus(), loadModels(), loadGlobalSettings()]).finally(() =>
|
||||||
setAuthLoading(false),
|
setAuthLoading(false),
|
||||||
);
|
);
|
||||||
}, [loadAuthStatus, loadModels]);
|
}, [loadAuthStatus, loadModels, loadGlobalSettings]);
|
||||||
|
|
||||||
// Check if we have GitHub provider
|
// Check if we have GitHub provider
|
||||||
const githubProvider = authProviders.find((p) => p.id === "github");
|
const githubProvider = authProviders.find((p) => p.id === "github");
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ interface SettingsModalProps {
|
|||||||
onThemeModeChange?: (mode: ThemeMode) => void;
|
onThemeModeChange?: (mode: ThemeMode) => void;
|
||||||
/** Called when color theme changes */
|
/** Called when color theme changes */
|
||||||
onColorThemeChange?: (theme: ColorTheme) => void;
|
onColorThemeChange?: (theme: ColorTheme) => void;
|
||||||
|
/** Optional callback when user wants to reopen the onboarding guide */
|
||||||
|
onReopenOnboarding?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsModal({
|
export function SettingsModal({
|
||||||
@@ -103,6 +105,7 @@ export function SettingsModal({
|
|||||||
colorTheme = "default",
|
colorTheme = "default",
|
||||||
onThemeModeChange,
|
onThemeModeChange,
|
||||||
onColorThemeChange,
|
onColorThemeChange,
|
||||||
|
onReopenOnboarding,
|
||||||
}: SettingsModalProps) {
|
}: SettingsModalProps) {
|
||||||
const [form, setForm] = useState<Settings & { worktreeInitCommand?: string }>({ maxConcurrent: 2, maxWorktrees: 4, pollIntervalMs: 15000, groupOverlappingFiles: true, autoMerge: true, mergeStrategy: "direct", recycleWorktrees: false, worktreeNaming: "random", includeTaskIdInCommit: true, worktreeInitCommand: "", ntfyEnabled: false, ntfyTopic: undefined });
|
const [form, setForm] = useState<Settings & { worktreeInitCommand?: string }>({ maxConcurrent: 2, maxWorktrees: 4, pollIntervalMs: 15000, groupOverlappingFiles: true, autoMerge: true, mergeStrategy: "direct", recycleWorktrees: false, worktreeNaming: "random", includeTaskIdInCommit: true, worktreeInitCommand: "", ntfyEnabled: false, ntfyTopic: undefined });
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -2400,6 +2403,20 @@ export function SettingsModal({
|
|||||||
<small className="auth-hint">
|
<small className="auth-hint">
|
||||||
Authentication changes take effect immediately — no need to save.
|
Authentication changes take effect immediately — no need to save.
|
||||||
</small>
|
</small>
|
||||||
|
{onReopenOnboarding && (
|
||||||
|
<div className="form-group" style={{ marginTop: "var(--space-md)" }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={onReopenOnboarding}
|
||||||
|
>
|
||||||
|
Reopen onboarding guide
|
||||||
|
</button>
|
||||||
|
<small className="settings-muted">
|
||||||
|
Re-run the setup wizard to review or update your AI provider and model configuration.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,21 @@ vi.mock("../../components/model-onboarding-state", () => ({
|
|||||||
clearOnboardingState: (...args: unknown[]) => mockClearOnboardingState(...args),
|
clearOnboardingState: (...args: unknown[]) => mockClearOnboardingState(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock CustomModelDropdown for onboarding modal tests
|
||||||
|
vi.mock("../../components/CustomModelDropdown", () => ({
|
||||||
|
CustomModelDropdown: ({ value, onChange, placeholder }: { value: string; onChange: (v: string) => void; placeholder?: string }) => (
|
||||||
|
<select
|
||||||
|
data-testid="mock-model-dropdown"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">{placeholder ?? "Select…"}</option>
|
||||||
|
<option value="anthropic/claude-sonnet-4-5">Claude Sonnet 4.5</option>
|
||||||
|
<option value="openai/gpt-4o">GPT-4o</option>
|
||||||
|
</select>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
// Mock state holders for dynamic mocking
|
// Mock state holders for dynamic mocking
|
||||||
const mockProjectsState = {
|
const mockProjectsState = {
|
||||||
projects: [] as any[],
|
projects: [] as any[],
|
||||||
@@ -177,7 +192,7 @@ vi.mock("../../hooks/useNodes", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { App } from "../../App";
|
import { App } from "../../App";
|
||||||
import { fetchAuthStatus, fetchSettings, fetchGlobalSettings, fetchTaskDetail, updateSettings, runScript, fetchScripts } from "../../api";
|
import { fetchAuthStatus, fetchSettings, fetchGlobalSettings, fetchTaskDetail, updateSettings, runScript, fetchScripts, fetchModels } from "../../api";
|
||||||
import * as apiNodeModule from "../../hooks/useRemoteNodeData";
|
import * as apiNodeModule from "../../hooks/useRemoteNodeData";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -1983,3 +1998,174 @@ describe("App search query propagation to remote mode", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("App onboarding reopen", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset mocks before each test
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not auto-open onboarding when modelOnboardingComplete is true and setup is complete", async () => {
|
||||||
|
// Mock fetchGlobalSettings to return complete onboarding with default model
|
||||||
|
(fetchGlobalSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
modelOnboardingComplete: true,
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-sonnet-4-5",
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchSettings).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for initial load
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Onboarding modal should NOT be open
|
||||||
|
expect(screen.queryByText("Set Up AI")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens Settings → Authentication → Reopen onboarding guide opens onboarding modal", async () => {
|
||||||
|
// Mock fetchGlobalSettings to return complete onboarding (to avoid auto-open on first call)
|
||||||
|
// and hydrated settings on subsequent calls
|
||||||
|
(fetchGlobalSettings as ReturnType<typeof vi.fn>)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
modelOnboardingComplete: true,
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-sonnet-4-5",
|
||||||
|
})
|
||||||
|
.mockResolvedValue({
|
||||||
|
modelOnboardingComplete: true,
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-sonnet-4-5",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock Settings and auth
|
||||||
|
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
maxConcurrent: 2,
|
||||||
|
maxWorktrees: 4,
|
||||||
|
});
|
||||||
|
(fetchAuthStatus as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
providers: [
|
||||||
|
{ id: "anthropic", name: "Anthropic", authenticated: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
(fetchModels as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
models: [
|
||||||
|
{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: false, contextWindow: 200000 },
|
||||||
|
],
|
||||||
|
favoriteProviders: [],
|
||||||
|
favoriteModels: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchSettings).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for initial load
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Onboarding should NOT be open initially
|
||||||
|
expect(screen.queryByText("Set Up AI")).toBeNull();
|
||||||
|
|
||||||
|
// Open Settings via header
|
||||||
|
const settingsBtn = screen.getByRole("button", { name: /settings/i });
|
||||||
|
fireEvent.click(settingsBtn);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Settings")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigate to Authentication section (it should be default or click to ensure)
|
||||||
|
const authSection = screen.getAllByText("Authentication")[0];
|
||||||
|
fireEvent.click(authSection);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchAuthStatus).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click Reopen onboarding guide button
|
||||||
|
const reopenBtn = screen.getByText("Reopen onboarding guide");
|
||||||
|
fireEvent.click(reopenBtn);
|
||||||
|
|
||||||
|
// Onboarding modal should now be open
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reopened modal shows hydrated model state from global settings", async () => {
|
||||||
|
// Mock fetchGlobalSettings to return hydrated settings
|
||||||
|
(fetchGlobalSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
modelOnboardingComplete: true,
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-sonnet-4-5",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock Settings and auth
|
||||||
|
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
maxConcurrent: 2,
|
||||||
|
maxWorktrees: 4,
|
||||||
|
});
|
||||||
|
(fetchAuthStatus as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
providers: [
|
||||||
|
{ id: "anthropic", name: "Anthropic", authenticated: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
(fetchModels as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
models: [
|
||||||
|
{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: false, contextWindow: 200000 },
|
||||||
|
],
|
||||||
|
favoriteProviders: [],
|
||||||
|
favoriteModels: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchSettings).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for initial load
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open Settings via header
|
||||||
|
const settingsBtn = screen.getByRole("button", { name: /settings/i });
|
||||||
|
fireEvent.click(settingsBtn);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Settings")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigate to Authentication section
|
||||||
|
const authSection = screen.getAllByText("Authentication")[0];
|
||||||
|
fireEvent.click(authSection);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchAuthStatus).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click Reopen onboarding guide button
|
||||||
|
const reopenBtn = screen.getByText("Reopen onboarding guide");
|
||||||
|
fireEvent.click(reopenBtn);
|
||||||
|
|
||||||
|
// Wait for onboarding modal to open
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The model dropdown should be pre-populated with the saved default
|
||||||
|
// Check that the dropdown shows the saved model is selected
|
||||||
|
const dropdown = await screen.findByTestId("mock-model-dropdown");
|
||||||
|
expect((dropdown as HTMLSelectElement).value).toBe("anthropic/claude-sonnet-4-5");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const mockLogoutProvider = vi.fn();
|
|||||||
const mockSaveApiKey = vi.fn();
|
const mockSaveApiKey = vi.fn();
|
||||||
const mockClearApiKey = vi.fn();
|
const mockClearApiKey = vi.fn();
|
||||||
const mockFetchModels = vi.fn();
|
const mockFetchModels = vi.fn();
|
||||||
|
const mockFetchGlobalSettings = vi.fn();
|
||||||
const mockUpdateGlobalSettings = vi.fn();
|
const mockUpdateGlobalSettings = vi.fn();
|
||||||
|
|
||||||
vi.mock("../../api", () => ({
|
vi.mock("../../api", () => ({
|
||||||
@@ -19,6 +20,7 @@ vi.mock("../../api", () => ({
|
|||||||
saveApiKey: (...args: unknown[]) => mockSaveApiKey(...args),
|
saveApiKey: (...args: unknown[]) => mockSaveApiKey(...args),
|
||||||
clearApiKey: (...args: unknown[]) => mockClearApiKey(...args),
|
clearApiKey: (...args: unknown[]) => mockClearApiKey(...args),
|
||||||
fetchModels: (...args: unknown[]) => mockFetchModels(...args),
|
fetchModels: (...args: unknown[]) => mockFetchModels(...args),
|
||||||
|
fetchGlobalSettings: (...args: unknown[]) => mockFetchGlobalSettings(...args),
|
||||||
updateGlobalSettings: (...args: unknown[]) => mockUpdateGlobalSettings(...args),
|
updateGlobalSettings: (...args: unknown[]) => mockUpdateGlobalSettings(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -81,6 +83,7 @@ beforeEach(() => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockFetchAuthStatus.mockResolvedValue({ providers: defaultAuthProviders });
|
mockFetchAuthStatus.mockResolvedValue({ providers: defaultAuthProviders });
|
||||||
mockFetchModels.mockResolvedValue({ models: defaultModels, favoriteProviders: [], favoriteModels: [] });
|
mockFetchModels.mockResolvedValue({ models: defaultModels, favoriteProviders: [], favoriteModels: [] });
|
||||||
|
mockFetchGlobalSettings.mockResolvedValue({});
|
||||||
mockUpdateGlobalSettings.mockResolvedValue({});
|
mockUpdateGlobalSettings.mockResolvedValue({});
|
||||||
mockLoginProvider.mockResolvedValue({ url: "https://auth.example.com/login" });
|
mockLoginProvider.mockResolvedValue({ url: "https://auth.example.com/login" });
|
||||||
mockLogoutProvider.mockResolvedValue({ success: true });
|
mockLogoutProvider.mockResolvedValue({ success: true });
|
||||||
@@ -654,4 +657,57 @@ describe("ModelOnboardingModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("global settings hydration", () => {
|
||||||
|
it("pre-populates selectedModel from global settings defaultProvider/defaultModelId", async () => {
|
||||||
|
// Mock global settings with a saved default model
|
||||||
|
mockFetchGlobalSettings.mockResolvedValueOnce({
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-sonnet-4-5",
|
||||||
|
modelOnboardingComplete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The model dropdown should be pre-populated with the saved default
|
||||||
|
const dropdown = screen.getByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||||
|
expect(dropdown.value).toBe("anthropic/claude-sonnet-4-5");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves selectedModel empty when no default is configured in global settings", async () => {
|
||||||
|
// Mock global settings with no default model
|
||||||
|
mockFetchGlobalSettings.mockResolvedValueOnce({
|
||||||
|
modelOnboardingComplete: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The model dropdown should be empty
|
||||||
|
const dropdown = screen.getByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||||
|
expect(dropdown.value).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles fetchGlobalSettings failure gracefully", async () => {
|
||||||
|
// Mock global settings fetch to fail
|
||||||
|
mockFetchGlobalSettings.mockRejectedValueOnce(new Error("Network error"));
|
||||||
|
|
||||||
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Set Up AI")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The modal should still render with empty dropdown
|
||||||
|
const dropdown = screen.getByTestId("mock-model-dropdown") as HTMLSelectElement;
|
||||||
|
expect(dropdown.value).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3127,4 +3127,47 @@ describe("Prompts section", () => {
|
|||||||
expect(payload.agentPrompts.templates.length).toBe(1);
|
expect(payload.agentPrompts.templates.length).toBe(1);
|
||||||
expect(payload.agentPrompts.templates[0].name).toBe("My Custom Template");
|
expect(payload.agentPrompts.templates[0].name).toBe("My Custom Template");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Reopen onboarding guide", () => {
|
||||||
|
it("renders Reopen onboarding guide button in Authentication section when onReopenOnboarding is provided", async () => {
|
||||||
|
const onReopenOnboarding = vi.fn();
|
||||||
|
render(<SettingsModal onClose={onClose} addToast={addToast} onReopenOnboarding={onReopenOnboarding} />);
|
||||||
|
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
// Navigate to Authentication section
|
||||||
|
fireEvent.click(screen.getAllByText("Authentication")[0]);
|
||||||
|
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
|
||||||
|
|
||||||
|
// Check that the reopen button is rendered
|
||||||
|
expect(screen.getByText("Reopen onboarding guide")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render Reopen onboarding guide button when onReopenOnboarding is not provided", async () => {
|
||||||
|
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||||
|
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
// Navigate to Authentication section
|
||||||
|
fireEvent.click(screen.getAllByText("Authentication")[0]);
|
||||||
|
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
|
||||||
|
|
||||||
|
// Check that the reopen button is NOT rendered
|
||||||
|
expect(screen.queryByText("Reopen onboarding guide")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onReopenOnboarding when Reopen button is clicked", async () => {
|
||||||
|
const onReopenOnboarding = vi.fn();
|
||||||
|
render(<SettingsModal onClose={onClose} addToast={addToast} onReopenOnboarding={onReopenOnboarding} />);
|
||||||
|
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||||
|
|
||||||
|
// Navigate to Authentication section
|
||||||
|
fireEvent.click(screen.getAllByText("Authentication")[0]);
|
||||||
|
await waitFor(() => expect(fetchAuthStatus).toHaveBeenCalled());
|
||||||
|
|
||||||
|
// Click the reopen button
|
||||||
|
fireEvent.click(screen.getByText("Reopen onboarding guide"));
|
||||||
|
|
||||||
|
// Verify callback was called
|
||||||
|
expect(onReopenOnboarding).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user