Skip to content
Merged
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
6 changes: 6 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ npm run test:e2e:headed # Run with visible browser windows (requires display)
npm run test:e2e:ui # Interactive UI mode (requires display)
```

Jest's shared setup in `src/setupTests.ts` supplies the minimal layout signals
Fluent UI needs for dialog focus. No per-suite layout mocks are needed. Hidden
and detached elements remain excluded. Await role queries after dialog
transitions, including when returning to background controls. This is not a
layout engine; use Playwright for assertions about element dimensions or positioning.

### E2E Test Modes

E2E flow tests run in two modes controlled by Playwright projects and an environment variable:
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/AppRouter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { render, screen } from '@testing-library/react'

const mockCreateBrowserRouter = jest.fn().mockReturnValue({})

jest.mock('react-router', () => ({
createBrowserRouter: mockCreateBrowserRouter,
RouterProvider: () => <div data-testid="router-provider" />,
}))

jest.mock('./App', () => () => <div data-testid="app" />)

import AppRouter from './AppRouter'

describe('AppRouter', () => {
beforeEach(() => {
mockCreateBrowserRouter.mockClear()
})

it('creates the browser router only when the authenticated child tree mounts', () => {
expect(mockCreateBrowserRouter).not.toHaveBeenCalled()

render(<AppRouter />)

expect(mockCreateBrowserRouter).toHaveBeenCalledTimes(1)
expect(screen.getByTestId('router-provider')).toBeInTheDocument()
})
})
15 changes: 15 additions & 0 deletions frontend/src/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useState } from 'react'
import { createBrowserRouter, RouterProvider } from 'react-router'

import App from './App'

export default function AppRouter() {
const [router] = useState(() => createBrowserRouter([
{
path: '*',
element: <App />,
},
]))

return <RouterProvider router={router} />
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import type { ReactElement } from 'react'

import { FluentProvider, webLightTheme } from '@fluentui/react-components'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createMemoryRouter, RouterProvider, useLocation, useNavigate } from 'react-router'

import { configurationApi } from '@/services/api'

import Configuration from './Configuration'

jest.mock('@/services/api', () => ({
configurationApi: {
getContent: jest.fn(),
updateContent: jest.fn(),
listEnvironmentFiles: jest.fn(),
getEnvironmentFile: jest.fn(),
updateEnvironmentFile: jest.fn(),
},
initializersApi: {
getSettings: jest.fn(),
listRegistered: jest.fn(),
listCustom: jest.fn(),
register: jest.fn(),
unregister: jest.fn(),
},
}))

const mockedConfigurationApi = jest.mocked(configurationApi)
const originalRequest = globalThis.Request

class RouterTestRequest {
readonly url: string
readonly method: string
readonly signal: AbortSignal

constructor(input: string | URL, init?: { method?: string, signal?: AbortSignal | null }) {
this.url = String(input)
this.method = init?.method ?? 'GET'
this.signal = init?.signal ?? new AbortController().signal
}
}

function RouterProbe(): ReactElement {
const location = useLocation()
const navigate = useNavigate()

return (
<>
<output aria-label="Current URL">{location.pathname}{location.search}</output>
<button type="button" onClick={() => void navigate('/scanner')}>Go to scanner</button>
</>
)
}

function renderPage(): void {
const router = createMemoryRouter([
{
path: '/config',
element: (
<>
<Configuration />
<RouterProbe />
</>
),
},
{
path: '*',
element: (
<>
<h1>Other page</h1>
<RouterProbe />
</>
),
},
], {
initialEntries: ['/config?tab=environment'],
})

render(
<FluentProvider theme={webLightTheme}>
<RouterProvider router={router} />
</FluentProvider>,
)
}

describe('Configuration failed environment reload guard', () => {
beforeAll(() => {
Object.defineProperty(globalThis, 'Request', {
configurable: true,
writable: true,
value: RouterTestRequest,
})
})

afterAll(() => {
Object.defineProperty(globalThis, 'Request', {
configurable: true,
writable: true,
value: originalRequest,
})
})

beforeEach(() => {
jest.clearAllMocks()
mockedConfigurationApi.getContent.mockResolvedValue({
content: 'operator: alice\n',
source: 'C:/Users/test/.pyrit/config.yaml',
version: 'config-v1',
})
mockedConfigurationApi.listEnvironmentFiles.mockResolvedValueOnce({
items: [
{
id: '0',
name: '.env',
path: 'C:/Users/test/.pyrit/.env',
content: '',
exists: true,
version: 'v1',
},
],
})
mockedConfigurationApi.getEnvironmentFile.mockResolvedValue({
id: '0',
name: '.env',
path: 'C:/Users/test/.pyrit/.env',
content: 'API_KEY=value\n',
exists: true,
version: 'v1',
})
})

it('keeps navigation protection when a confirmed reload fails and preserves drafts', async () => {
const user = userEvent.setup()
renderPage()

const editor = await screen.findByLabelText('Environment file contents')
await user.clear(editor)
await user.type(editor, 'API_KEY=unsaved\n')

mockedConfigurationApi.listEnvironmentFiles.mockRejectedValueOnce(new Error('Reload failed'))

await user.click(screen.getByRole('button', { name: 'Reload' }))
await user.click(await screen.findByRole('button', { name: 'Discard changes' }))

expect(await screen.findByText('Reload failed')).toBeInTheDocument()
expect(screen.getByLabelText('Environment file contents')).toHaveValue('API_KEY=unsaved\n')

await user.click(await screen.findByRole('button', { name: 'Go to scanner' }))

expect(await screen.findByRole('dialog', { name: 'Discard unsaved changes?' })).toBeInTheDocument()
expect(screen.getByLabelText('Current URL')).toHaveTextContent(/^\/config\?tab=environment$/)
})
})
Loading