Skip to content
139 changes: 139 additions & 0 deletions frontend/testing/unit/pages/SettingsExport.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { vi, describe, it, expect, beforeEach } from 'vitest'
import Settings from '../../../src/pages/Settings'
import { ThemeProvider } from '../../../src/components/ThemeContext'
import { ToastProvider } from '../../../src/components/ToastContext'
import { listNotificationRules } from '../../../src/api'

vi.mock('../../../src/api', async () => {
const actual: any = await vi.importActual('../../../src/api')
return {
...actual,
listNotificationRules: vi.fn(),
}
})

function renderSettings() {
render(
<ThemeProvider>
<ToastProvider>
<Settings />
</ToastProvider>
</ThemeProvider>,
)
}

describe('Settings export flow', () => {
beforeEach(() => {
localStorage.removeItem('secuscan-config')
vi.mocked(listNotificationRules).mockResolvedValue([])
})

it('creates a download anchor with a JSON data URI and clicks it', async () => {
const user = userEvent.setup()
renderSettings()

const createdAnchors: HTMLAnchorElement[] = []
const originalCreate = document.createElement.bind(document)

vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
const el = originalCreate(tag)
if (tag === 'a') {
vi.spyOn(el as HTMLAnchorElement, 'click').mockImplementation(() => {})
createdAnchors.push(el as HTMLAnchorElement)
}
return el
})

await user.click(screen.getByRole('button', { name: /TELEMETRY_EXPORT/i }))

expect(createdAnchors).toHaveLength(1)
const anchor = createdAnchors[0]

expect(anchor.getAttribute('href')).toMatch(/^data:text\/json/)
expect(anchor.click).toHaveBeenCalledOnce()

vi.restoreAllMocks()
})

it('sets the download filename to secuscan_config_<today>.json', async () => {
const user = userEvent.setup()
renderSettings()

const createdAnchors: HTMLAnchorElement[] = []
const originalCreate = document.createElement.bind(document)

vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
const el = originalCreate(tag)
if (tag === 'a') {
vi.spyOn(el as HTMLAnchorElement, 'click').mockImplementation(() => {})
createdAnchors.push(el as HTMLAnchorElement)
}
return el
})

await user.click(screen.getByRole('button', { name: /TELEMETRY_EXPORT/i }))

const today = new Date().toISOString().split('T')[0]
expect(createdAnchors[0].getAttribute('download')).toBe(
`secuscan_config_${today}.json`,
)

vi.restoreAllMocks()
})

it('exports a valid JSON string containing the current config', async () => {
const user = userEvent.setup()
renderSettings()

const createdAnchors: HTMLAnchorElement[] = []
const originalCreate = document.createElement.bind(document)

vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
const el = originalCreate(tag)
if (tag === 'a') {
vi.spyOn(el as HTMLAnchorElement, 'click').mockImplementation(() => {})
createdAnchors.push(el as HTMLAnchorElement)
}
return el
})

await user.click(screen.getByRole('button', { name: /TELEMETRY_EXPORT/i }))

const href = createdAnchors[0].getAttribute('href') ?? ''
const jsonStr = decodeURIComponent(
href.replace('data:text/json;charset=utf-8,', ''),
)
const exported = JSON.parse(jsonStr)

expect(exported.concurrentScans).toBe(8)
expect(exported.scanIntensity).toBe('standard')
expect(exported.theme).toBe('dark')

vi.restoreAllMocks()
})

it('removes the anchor from the DOM after the download is triggered', async () => {
const user = userEvent.setup()
renderSettings()

const createdAnchors: HTMLAnchorElement[] = []
const originalCreate = document.createElement.bind(document)

vi.spyOn(document, 'createElement').mockImplementation((tag: string) => {
const el = originalCreate(tag)
if (tag === 'a') {
vi.spyOn(el as HTMLAnchorElement, 'click').mockImplementation(() => {})
createdAnchors.push(el as HTMLAnchorElement)
}
return el
})

await user.click(screen.getByRole('button', { name: /TELEMETRY_EXPORT/i }))

expect(document.body.contains(createdAnchors[0])).toBe(false)

vi.restoreAllMocks()
})
})
33 changes: 23 additions & 10 deletions frontend/testing/unit/pages/SettingsTheme.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ vi.mock('../../../src/api', async () => {
}
})

function renderSettings() {
render(
<ThemeProvider>
<ToastProvider>
<Settings />
</ToastProvider>
</ThemeProvider>,
)
}

describe('Settings theme wiring', () => {
beforeEach(() => {
window.localStorage.removeItem('secuscan-theme')
Expand All @@ -22,26 +32,29 @@ describe('Settings theme wiring', () => {

it('applies selected theme globally and persists it', async () => {
const user = userEvent.setup()

render(
<ThemeProvider>
<ToastProvider>
<Settings />
</ToastProvider>
</ThemeProvider>,
)
renderSettings()

const themeSelect = screen.getByRole('combobox', { name: /visual spectrum theme/i })

await user.selectOptions(themeSelect, 'light')
await user.click(screen.getByRole('button', { name: /COMMIT_ENGINE_CHANGES/i }))

expect(document.documentElement.classList.contains('theme-light')).toBe(true)
expect(window.localStorage.getItem('secuscan-theme')).toBe('light')

await user.selectOptions(themeSelect, 'dark')
await user.click(screen.getByRole('button', { name: /COMMIT_ENGINE_CHANGES/i }))

expect(document.documentElement.classList.contains('theme-light')).toBe(false)
expect(window.localStorage.getItem('secuscan-theme')).toBe('dark')
})

it('opens reset confirmation modal when ENGINE_RESET is clicked', async () => {
const user = userEvent.setup()
renderSettings()

await user.click(screen.getByRole('button', { name: /ENGINE_RESET/i }))

expect(
screen.getByText(/Restore engine to factory specifications/i),
).toBeInTheDocument()
})
})
Loading