Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
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(screen.getByRole('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(screen.getByRole('button', { name: 'Go to scanner' }))

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