diff --git a/frontend/testing/unit/pages/SettingsExport.test.tsx b/frontend/testing/unit/pages/SettingsExport.test.tsx
new file mode 100644
index 00000000..2df6de2c
--- /dev/null
+++ b/frontend/testing/unit/pages/SettingsExport.test.tsx
@@ -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(
+
+
+
+
+ ,
+ )
+}
+
+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_.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()
+ })
+})
diff --git a/frontend/testing/unit/pages/SettingsTheme.test.tsx b/frontend/testing/unit/pages/SettingsTheme.test.tsx
index 39bc4cfc..36ee93c7 100644
--- a/frontend/testing/unit/pages/SettingsTheme.test.tsx
+++ b/frontend/testing/unit/pages/SettingsTheme.test.tsx
@@ -13,6 +13,16 @@ vi.mock('../../../src/api', async () => {
}
})
+function renderSettings() {
+ render(
+
+
+
+
+ ,
+ )
+}
+
describe('Settings theme wiring', () => {
beforeEach(() => {
window.localStorage.removeItem('secuscan-theme')
@@ -22,26 +32,29 @@ describe('Settings theme wiring', () => {
it('applies selected theme globally and persists it', async () => {
const user = userEvent.setup()
-
- render(
-
-
-
-
- ,
- )
+ 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()
+ })
})