Skip to content

Commit 00e2e15

Browse files
hannahwestra25Copilothannahwestra25
authored
FEAT use ThemeProvider instead of boolean for theme (#1991)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: hannahwestra25 <hannahwestra@users.noreply.github.com>
1 parent 3ae078b commit 00e2e15

11 files changed

Lines changed: 650 additions & 165 deletions

File tree

frontend/e2e/accessibility.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ test.describe("Accessibility", () => {
5555
const configBtn = page.getByTitle("Configuration");
5656
await expect(configBtn).toBeVisible();
5757

58-
// Theme toggle button
59-
const themeBtn = page.getByTitle(/light mode|dark mode/i);
58+
// Theme toggle button (now a menu trigger with "Theme: <mode>" title)
59+
const themeBtn = page.getByTitle(/^Theme:/);
6060
await expect(themeBtn).toBeVisible();
6161
});
6262

frontend/e2e/chat.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,21 +175,21 @@ test.describe("Theme Toggle", () => {
175175
await page.goto("/");
176176
await expect(page.getByTitle("Chat")).toBeVisible({ timeout: 10000 });
177177

178-
// The app defaults to dark mode, so the toggle button title should say "Light Mode"
179-
const themeBtn = page.getByTitle("Light Mode");
178+
// The app defaults to system mode, so the toggle button title should say "Theme: System"
179+
const themeBtn = page.getByTitle("Theme: System");
180180
await expect(themeBtn).toBeVisible();
181181

182-
// Click to switch to light mode
182+
// Open the theme menu and select Light
183183
await themeBtn.click();
184+
await page.getByRole("menuitemradio", { name: "Light" }).click();
184185

185-
// Now the button title should change to "Dark Mode"
186-
await expect(page.getByTitle("Dark Mode")).toBeVisible({ timeout: 5000 });
187-
// The old title should no longer be present
188-
await expect(page.getByTitle("Light Mode")).not.toBeVisible();
186+
// Now the button title should say "Theme: Light"
187+
await expect(page.getByTitle("Theme: Light")).toBeVisible({ timeout: 5000 });
189188

190-
// Click again to switch back to dark mode
191-
await page.getByTitle("Dark Mode").click();
192-
await expect(page.getByTitle("Light Mode")).toBeVisible({ timeout: 5000 });
189+
// Open the menu again and select Dark
190+
await page.getByTitle("Theme: Light").click();
191+
await page.getByRole("menuitemradio", { name: "Dark" }).click();
192+
await expect(page.getByTitle("Theme: Dark")).toBeVisible({ timeout: 5000 });
193193
});
194194
});
195195

frontend/src/App.test.tsx

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
77
import { MemoryRouter } from "react-router-dom";
88
import App from "./App";
9+
import { ThemeProvider } from "./hooks/useTheme";
10+
911
import { attacksApi } from "./services/api";
1012

1113
const mockGetActiveAccount = jest.fn();
@@ -80,22 +82,15 @@ jest.mock("./components/Labels/LabelsBar", () => {
8082
jest.mock("./components/Layout/MainLayout", () => {
8183
const MockMainLayout = ({
8284
children,
83-
onToggleTheme,
84-
isDarkMode,
8585
currentView,
8686
onNavigate,
8787
}: {
8888
children: React.ReactNode;
89-
onToggleTheme: () => void;
90-
isDarkMode: boolean;
9189
currentView: string;
9290
onNavigate: (view: string) => void;
9391
}) => {
9492
return (
95-
<div data-testid="main-layout" data-dark-mode={isDarkMode} data-current-view={currentView}>
96-
<button onClick={onToggleTheme} data-testid="toggle-theme">
97-
Toggle Theme
98-
</button>
93+
<div data-testid="main-layout" data-current-view={currentView}>
9994
<button onClick={() => onNavigate("home")} data-testid="nav-home">
10095
Home
10196
</button>
@@ -289,15 +284,18 @@ describe("App", () => {
289284
// initialPath lets a test deep-link straight to a view (e.g. "/config").
290285
function renderApp(initialPath = "/") {
291286
return render(
292-
<MemoryRouter initialEntries={[initialPath]}>
293-
<App />
294-
</MemoryRouter>
287+
<ThemeProvider>
288+
<MemoryRouter initialEntries={[initialPath]}>
289+
<App />
290+
</MemoryRouter>
291+
</ThemeProvider>
295292
);
296293
}
297294

298295
beforeEach(() => {
299296
jest.clearAllMocks();
300297
mockGetActiveAccount.mockReturnValue(null);
298+
window.localStorage.clear();
301299
});
302300

303301
it("renders with FluentProvider and MainLayout", () => {
@@ -306,35 +304,6 @@ describe("App", () => {
306304
expect(screen.getByTestId("home-view")).toBeInTheDocument();
307305
});
308306

309-
it("starts in dark mode", () => {
310-
renderApp();
311-
expect(screen.getByTestId("main-layout")).toHaveAttribute(
312-
"data-dark-mode",
313-
"true"
314-
);
315-
});
316-
317-
it("toggles theme when onToggleTheme is called", () => {
318-
renderApp();
319-
320-
expect(screen.getByTestId("main-layout")).toHaveAttribute(
321-
"data-dark-mode",
322-
"true"
323-
);
324-
325-
fireEvent.click(screen.getByTestId("toggle-theme"));
326-
expect(screen.getByTestId("main-layout")).toHaveAttribute(
327-
"data-dark-mode",
328-
"false"
329-
);
330-
331-
fireEvent.click(screen.getByTestId("toggle-theme"));
332-
expect(screen.getByTestId("main-layout")).toHaveAttribute(
333-
"data-dark-mode",
334-
"true"
335-
);
336-
});
337-
338307
it("starts in home view", () => {
339308
renderApp();
340309

frontend/src/App.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { useState, useCallback, useEffect, useRef, useMemo } from 'react'
22
import { Routes, Route, Navigate, useNavigate, useLocation, useSearchParams, matchPath } from 'react-router-dom'
3-
import { FluentProvider, webLightTheme, webDarkTheme } from '@fluentui/react-components'
43
import { useMsal } from '@azure/msal-react'
54
import { Joyride } from 'react-joyride'
5+
import { useTheme } from './hooks/useTheme'
66
import MainLayout from './components/Layout/MainLayout'
77
import ChatWindow from './components/Chat/ChatWindow'
88
import AttackNotFound from './components/Chat/AttackNotFound'
@@ -94,7 +94,6 @@ function App() {
9494
const routeConversationId = conversationMatch?.params.conversationId ?? null
9595
const currentView: ViewName = routeAttackId !== null ? 'chat' : viewFromPath(location.pathname)
9696

97-
const [isDarkMode, setIsDarkMode] = useState(true)
9897
const [activeTarget, setActiveTarget] = useState<TargetInstance | null>(null)
9998
const [globalLabels, setGlobalLabels] = useState<Record<string, string>>({ ...DEFAULT_GLOBAL_LABELS })
10099

@@ -310,10 +309,6 @@ function App() {
310309
navigate(attackPath(openAttackResultId))
311310
}, [navigate])
312311

313-
const toggleTheme = () => {
314-
setIsDarkMode(!isDarkMode)
315-
}
316-
317312
const chatElement = isAttackNotFound || isAttackError ? (
318313
<AttackNotFound
319314
attackId={routeAttackId ?? ''}
@@ -341,7 +336,8 @@ function App() {
341336
)
342337

343338
// Onboarding tour — pass handleNavigate so the tour can switch views between steps
344-
const { startTour, hasCompletedTour, tourProps } = useTour(handleNavigate, isDarkMode, currentView)
339+
const { resolved } = useTheme()
340+
const { startTour, hasCompletedTour, tourProps } = useTour(handleNavigate, resolved === 'dark', currentView)
345341

346342
// Auto-start the tour on first visit
347343
useEffect(() => {
@@ -354,14 +350,11 @@ function App() {
354350
return (
355351
<ErrorBoundary>
356352
<ConnectionHealthProvider>
357-
<FluentProvider theme={isDarkMode ? webDarkTheme : webLightTheme}>
358353
<Joyride {...tourProps} />
359354
<ConnectionBannerContainer />
360355
<MainLayout
361356
currentView={currentView}
362357
onNavigate={handleNavigate}
363-
onToggleTheme={toggleTheme}
364-
isDarkMode={isDarkMode}
365358
onOpenFeedback={() => setFeedbackOpen(true)}
366359
onStartTour={startTour}
367360
>
@@ -423,7 +416,6 @@ function App() {
423416
}}
424417
/>
425418
)}
426-
</FluentProvider>
427419
</ConnectionHealthProvider>
428420
</ErrorBoundary>
429421
)

frontend/src/components/Layout/MainLayout.test.tsx

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,14 @@ jest.mock("../../services/api", () => ({
1717
// Mock Navigation to simplify testing
1818
jest.mock("../Sidebar/Navigation", () => {
1919
const MockNavigation = ({
20-
onToggleTheme,
21-
isDarkMode,
2220
currentView,
2321
onNavigate,
2422
}: {
25-
onToggleTheme: () => void;
26-
isDarkMode: boolean;
2723
currentView: string;
2824
onNavigate: (view: string) => void;
2925
}) => {
3026
return (
31-
<div data-testid="navigation" data-dark-mode={isDarkMode} data-current-view={currentView}>
32-
<button onClick={onToggleTheme}>Toggle</button>
27+
<div data-testid="navigation" data-current-view={currentView}>
3328
<button onClick={() => onNavigate("config")}>Config</button>
3429
</div>
3530
);
@@ -55,8 +50,6 @@ describe("MainLayout", () => {
5550
});
5651

5752
const defaultProps = {
58-
onToggleTheme: jest.fn(),
59-
isDarkMode: false,
6053
currentView: 'chat' as const,
6154
onNavigate: jest.fn(),
6255
};
@@ -145,21 +138,4 @@ describe("MainLayout", () => {
145138
expect(mockedVersionApi.getVersion).toHaveBeenCalled();
146139
});
147140
});
148-
149-
it("passes theme props to Navigation", async () => {
150-
mockedVersionApi.getVersion.mockResolvedValue({ version: "1.0.0" });
151-
152-
renderWithProvider(
153-
<MainLayout {...defaultProps} isDarkMode={true}>
154-
<div>Content</div>
155-
</MainLayout>
156-
);
157-
158-
const navigation = screen.getByTestId("navigation");
159-
expect(navigation).toHaveAttribute("data-dark-mode", "true");
160-
161-
await waitFor(() => {
162-
expect(mockedVersionApi.getVersion).toHaveBeenCalled();
163-
});
164-
});
165141
});

frontend/src/components/Layout/MainLayout.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ interface MainLayoutProps {
1212
children: React.ReactNode
1313
currentView: ViewName
1414
onNavigate: (view: ViewName) => void
15-
onToggleTheme: () => void
16-
isDarkMode: boolean
1715
onOpenFeedback: () => void
1816
onStartTour?: () => void
1917
}
@@ -22,8 +20,6 @@ export default function MainLayout({
2220
children,
2321
currentView,
2422
onNavigate,
25-
onToggleTheme,
26-
isDarkMode,
2723
onOpenFeedback,
2824
onStartTour,
2925
}: MainLayoutProps) {
@@ -59,8 +55,6 @@ export default function MainLayout({
5955
<Navigation
6056
currentView={currentView}
6157
onNavigate={onNavigate}
62-
onToggleTheme={onToggleTheme}
63-
isDarkMode={isDarkMode}
6458
onOpenFeedback={onOpenFeedback}
6559
onStartTour={onStartTour}
6660
/>

0 commit comments

Comments
 (0)