Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 29 additions & 56 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import App from "./App";
import { ThemeProvider } from "./hooks/useTheme";
import { attacksApi } from "./services/api";

const mockGetActiveAccount = jest.fn();
Expand Down Expand Up @@ -45,22 +46,15 @@ jest.mock("./components/Labels/LabelsBar", () => {
jest.mock("./components/Layout/MainLayout", () => {
const MockMainLayout = ({
children,
onToggleTheme,
isDarkMode,
currentView,
onNavigate,
}: {
children: React.ReactNode;
onToggleTheme: () => void;
isDarkMode: boolean;
currentView: string;
onNavigate: (view: string) => void;
}) => {
return (
<div data-testid="main-layout" data-dark-mode={isDarkMode} data-current-view={currentView}>
<button onClick={onToggleTheme} data-testid="toggle-theme">
Toggle Theme
</button>
<div data-testid="main-layout" data-current-view={currentView}>
<button onClick={() => onNavigate("home")} data-testid="nav-home">
Home
</button>
Expand Down Expand Up @@ -242,45 +236,24 @@ describe("App", () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetActiveAccount.mockReturnValue(null);
window.localStorage.clear();
});

const renderApp = () =>
render(
<ThemeProvider>
<App />
</ThemeProvider>
);

it("renders with FluentProvider and MainLayout", () => {
render(<App />);
renderApp();
expect(screen.getByTestId("main-layout")).toBeInTheDocument();
expect(screen.getByTestId("home-view")).toBeInTheDocument();
});

it("starts in dark mode", () => {
render(<App />);
expect(screen.getByTestId("main-layout")).toHaveAttribute(
"data-dark-mode",
"true"
);
});

it("toggles theme when onToggleTheme is called", () => {
render(<App />);

expect(screen.getByTestId("main-layout")).toHaveAttribute(
"data-dark-mode",
"true"
);

fireEvent.click(screen.getByTestId("toggle-theme"));
expect(screen.getByTestId("main-layout")).toHaveAttribute(
"data-dark-mode",
"false"
);

fireEvent.click(screen.getByTestId("toggle-theme"));
expect(screen.getByTestId("main-layout")).toHaveAttribute(
"data-dark-mode",
"true"
);
});

it("starts in home view", () => {
render(<App />);
renderApp();

expect(screen.getByTestId("main-layout")).toHaveAttribute(
"data-current-view",
Expand All @@ -290,7 +263,7 @@ describe("App", () => {
});

it("switches to chat view", () => {
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("nav-chat"));

Expand All @@ -302,7 +275,7 @@ describe("App", () => {
});

it("switches to config view", () => {
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("nav-config"));

Expand All @@ -314,7 +287,7 @@ describe("App", () => {
});

it("switches back to chat from config", () => {
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("nav-config"));
expect(screen.getByTestId("target-config")).toBeInTheDocument();
Expand All @@ -324,7 +297,7 @@ describe("App", () => {
});

it("sets conversationId from chat window", () => {
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("nav-chat"));
expect(screen.getByTestId("conversation-id")).toHaveTextContent("none");
Expand All @@ -334,7 +307,7 @@ describe("App", () => {
});

it("clears conversationId on new attack", () => {
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("nav-chat"));
fireEvent.click(screen.getByTestId("set-conversation"));
Expand All @@ -345,7 +318,7 @@ describe("App", () => {
});

it("sets active target from config page and passes to chat", () => {
render(<App />);
renderApp();

// Switch to chat and confirm no target initially
fireEvent.click(screen.getByTestId("nav-chat"));
Expand All @@ -361,7 +334,7 @@ describe("App", () => {
});

it("switches to history view", () => {
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("nav-history"));

Expand All @@ -374,7 +347,7 @@ describe("App", () => {

it("opens attack from history and switches to chat", async () => {
mockGetAttack.mockResolvedValue({ attack_result_id: "ar-attack-1", conversation_id: "attack-conv-1", labels: { operator: "roakey" } });
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("nav-history"));
fireEvent.click(screen.getByTestId("open-attack"));
Expand All @@ -393,7 +366,7 @@ describe("App", () => {
conversation_id: "home-conv-1",
labels: { operator: "roakey" },
});
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("home-open-attack"));

Expand All @@ -406,7 +379,7 @@ describe("App", () => {
});

it("navigates to config from the home view", () => {
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("home-go-config"));

Expand All @@ -419,7 +392,7 @@ describe("App", () => {

it("handles failed attack open gracefully", async () => {
mockGetAttack.mockRejectedValue(new Error("Not found"));
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("nav-history"));
fireEvent.click(screen.getByTestId("open-attack"));
Expand All @@ -445,7 +418,7 @@ describe("App", () => {
() => new Promise((resolve) => { resolveGetAttack = resolve })
);

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

// Simulate: user is already on attack A with a branched conv selected.
fireEvent.click(screen.getByTestId("nav-chat"));
Expand Down Expand Up @@ -489,7 +462,7 @@ describe("App", () => {
default_labels: { operator: "default_user", custom: "value" },
});

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

// The version API is called on mount and labels get merged
await waitFor(() => {
Expand All @@ -512,7 +485,7 @@ describe("App", () => {
default_labels: { custom: "value" },
});

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

// Home receives the same labels prop — assert there to avoid racing the
// async initLabels effect against a view-change re-render.
Expand All @@ -530,7 +503,7 @@ describe("App", () => {
default_labels: { operator: "backend_user", custom: "value" },
});

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

await waitFor(() => {
const labels = screen.getByTestId("home-labels-json").textContent ?? "";
Expand All @@ -540,7 +513,7 @@ describe("App", () => {
});

it("stores attack target when conversation is created with active target", () => {
render(<App />);
renderApp();

// Set a target first
fireEvent.click(screen.getByTestId("nav-config"));
Expand All @@ -553,7 +526,7 @@ describe("App", () => {
});

it("sets active conversation when onSelectConversation is called", () => {
render(<App />);
renderApp();

fireEvent.click(screen.getByTestId("nav-chat"));

Expand Down
12 changes: 2 additions & 10 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useState, useCallback, useEffect } from 'react'
import { FluentProvider, webLightTheme, webDarkTheme } from '@fluentui/react-components'
import { useMsal } from '@azure/msal-react'
import MainLayout from './components/Layout/MainLayout'
import ChatWindow from './components/Chat/ChatWindow'
Expand Down Expand Up @@ -42,7 +41,6 @@ function ConnectionBannerContainer() {

function App() {
const { instance } = useMsal()
const [isDarkMode, setIsDarkMode] = useState(true)
const [currentView, setCurrentView] = useState<ViewName>('home')
const [activeTarget, setActiveTarget] = useState<TargetInstance | null>(null)
const [globalLabels, setGlobalLabels] = useState<Record<string, string>>({ ...DEFAULT_GLOBAL_LABELS })
Expand Down Expand Up @@ -177,20 +175,14 @@ function App() {
}
}, [attackResultId, clearAttackState])

const toggleTheme = () => {
setIsDarkMode(!isDarkMode)
}

return (
<ErrorBoundary>
<ConnectionHealthProvider>
<FluentProvider theme={isDarkMode ? webDarkTheme : webLightTheme}>
<>
<ConnectionBannerContainer />
<MainLayout
currentView={currentView}
onNavigate={setCurrentView}
onToggleTheme={toggleTheme}
isDarkMode={isDarkMode}
>
{currentView === 'home' && (
<Home
Expand Down Expand Up @@ -233,7 +225,7 @@ function App() {
/>
)}
</MainLayout>
</FluentProvider>
</>
</ConnectionHealthProvider>
</ErrorBoundary>
)
Expand Down
26 changes: 1 addition & 25 deletions frontend/src/components/Layout/MainLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,14 @@ jest.mock("../../services/api", () => ({
// Mock Navigation to simplify testing
jest.mock("../Sidebar/Navigation", () => {
const MockNavigation = ({
onToggleTheme,
isDarkMode,
currentView,
onNavigate,
}: {
onToggleTheme: () => void;
isDarkMode: boolean;
currentView: string;
onNavigate: (view: string) => void;
}) => {
return (
<div data-testid="navigation" data-dark-mode={isDarkMode} data-current-view={currentView}>
<button onClick={onToggleTheme}>Toggle</button>
<div data-testid="navigation" data-current-view={currentView}>
<button onClick={() => onNavigate("config")}>Config</button>
</div>
);
Expand All @@ -55,8 +50,6 @@ describe("MainLayout", () => {
});

const defaultProps = {
onToggleTheme: jest.fn(),
isDarkMode: false,
currentView: 'chat' as const,
onNavigate: jest.fn(),
};
Expand Down Expand Up @@ -145,21 +138,4 @@ describe("MainLayout", () => {
expect(mockedVersionApi.getVersion).toHaveBeenCalled();
});
});

it("passes theme props to Navigation", async () => {
mockedVersionApi.getVersion.mockResolvedValue({ version: "1.0.0" });

renderWithProvider(
<MainLayout {...defaultProps} isDarkMode={true}>
<div>Content</div>
</MainLayout>
);

const navigation = screen.getByTestId("navigation");
expect(navigation).toHaveAttribute("data-dark-mode", "true");

await waitFor(() => {
expect(mockedVersionApi.getVersion).toHaveBeenCalled();
});
});
});
6 changes: 0 additions & 6 deletions frontend/src/components/Layout/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,12 @@ interface MainLayoutProps {
children: React.ReactNode
currentView: ViewName
onNavigate: (view: ViewName) => void
onToggleTheme: () => void
isDarkMode: boolean
}

export default function MainLayout({
children,
currentView,
onNavigate,
onToggleTheme,
isDarkMode,
}: MainLayoutProps) {
const styles = useMainLayoutStyles()
const [version, setVersion] = useState<string>('Loading...')
Expand Down Expand Up @@ -55,8 +51,6 @@ export default function MainLayout({
<Navigation
currentView={currentView}
onNavigate={onNavigate}
onToggleTheme={onToggleTheme}
isDarkMode={isDarkMode}
/>
</aside>
<main className={styles.main}>{children}</main>
Expand Down
Loading
Loading