Skip to content
Merged
62 changes: 31 additions & 31 deletions ui/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* @vitest-environment jsdom
*/
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { screen, render, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import App from './App';
import { AppInner } from './App';

// Set up globals for jsdom
Object.defineProperty(window, 'location', {
Expand Down Expand Up @@ -161,13 +161,19 @@ vi.mock('./components/AnnouncementModal', () => ({
default: () => null,
}));

// Create mocks that we can track and configure per test
const mockNavigate = vi.fn();
const mockSearchParams = new URLSearchParams();
const mockSetSearchParams = vi.fn();

// Mock react-router-dom to avoid HashRouter issues in tests
vi.mock('react-router-dom', () => ({
HashRouter: ({ children }: { children: React.ReactNode }) => <>{children}</>,
Routes: ({ children }: { children: React.ReactNode }) => <>{children}</>,
Route: ({ element }: { element: React.ReactNode }) => element,
useNavigate: () => vi.fn(),
useNavigate: () => mockNavigate,
useLocation: () => ({ state: null, pathname: '/' }),
useSearchParams: () => [mockSearchParams, mockSetSearchParams],
Outlet: () => null,
}));

Expand Down Expand Up @@ -216,6 +222,14 @@ Object.defineProperty(window, 'matchMedia', {
describe('App Component - Brand New State', () => {
beforeEach(() => {
vi.clearAllMocks();
mockNavigate.mockClear();
mockSetSearchParams.mockClear();

// Reset search params
mockSearchParams.forEach((_, key) => {
mockSearchParams.delete(key);
});

window.location.hash = '';
window.location.search = '';
window.location.pathname = '/';
Expand All @@ -235,21 +249,16 @@ describe('App Component - Brand New State', () => {
GOOSE_ALLOWLIST_WARNING: false,
});

render(<App />);
render(<AppInner />);

// Wait for initialization
await waitFor(() => {
expect(mockElectron.reactReady).toHaveBeenCalled();
});

// Check that we navigated to "/" not "/welcome"
await waitFor(() => {
// In some environments, the hash might be empty or just "#"
expect(window.location.hash).toMatch(/^(#\/?|)$/);
});

// History should have been updated to "/"
expect(window.history.replaceState).toHaveBeenCalledWith({}, '', '#/');
// The app should initialize without any navigation calls since we're already at "/"
// No navigate calls should be made when no provider is configured
expect(mockNavigate).not.toHaveBeenCalled();
});

it('should handle deep links correctly when app is brand new', async () => {
Expand All @@ -260,20 +269,17 @@ describe('App Component - Brand New State', () => {
GOOSE_ALLOWLIST_WARNING: false,
});

// Simulate a deep link
window.location.search = '?view=settings';
// Set up search params to simulate view=settings deep link
mockSearchParams.set('view', 'settings');

render(<App />);
render(<AppInner />);

// Wait for initialization
await waitFor(() => {
expect(mockElectron.reactReady).toHaveBeenCalled();
});

// Should redirect to settings route via hash
await waitFor(() => {
expect(window.location.hash).toBe('#/settings');
});
expect(screen.getByText(/^Select an AI model provider/)).toBeInTheDocument();
});

it('should not redirect to /welcome when provider is configured', async () => {
Expand All @@ -284,18 +290,15 @@ describe('App Component - Brand New State', () => {
GOOSE_ALLOWLIST_WARNING: false,
});

render(<App />);
render(<AppInner />);

// Wait for initialization
await waitFor(() => {
expect(mockElectron.reactReady).toHaveBeenCalled();
});

// Should stay at "/" since provider is configured
await waitFor(() => {
// In some environments, the hash might be empty or just "#"
expect(window.location.hash).toMatch(/^(#\/?|)$/);
});
// Should not navigate anywhere since provider is configured and we're already at "/"
expect(mockNavigate).not.toHaveBeenCalled();
});

it('should handle config recovery gracefully', async () => {
Expand All @@ -310,17 +313,14 @@ describe('App Component - Brand New State', () => {
GOOSE_ALLOWLIST_WARNING: false,
});

render(<App />);
render(<AppInner />);

// Wait for initialization and recovery
await waitFor(() => {
expect(mockElectron.reactReady).toHaveBeenCalled();
});

// App should still initialize and navigate to "/"
await waitFor(() => {
// In some environments, the hash might be empty or just "#"
expect(window.location.hash).toMatch(/^(#\/?|)$/);
});
// App should still initialize without any navigation calls
expect(mockNavigate).not.toHaveBeenCalled();
});
});
Loading
Loading