diff --git a/apps/extension/__tests__/options-app.test.jsx b/apps/extension/__tests__/options-app.test.jsx new file mode 100644 index 0000000..4094fdd --- /dev/null +++ b/apps/extension/__tests__/options-app.test.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; + +import App from '../src/options/App.jsx'; + +describe('extension options app', () => { + beforeEach(() => { + global.chrome = { + alarms: { + create: vi.fn().mockResolvedValue(undefined), + }, + storage: { + local: { + get: vi.fn((keys, callback) => callback({})), + set: vi.fn().mockResolvedValue(undefined), + }, + }, + }; + }); + + it('normalizes a blank scan interval and reschedules the alarm on save', async () => { + render(); + + const intervalInput = screen.getByLabelText(/event check interval/i); + fireEvent.change(intervalInput, { target: { value: '' } }); + fireEvent.click(screen.getByRole('button', { name: /save settings/i })); + + await waitFor(() => { + expect(chrome.storage.local.set).toHaveBeenCalledWith( + expect.objectContaining({ scanInterval: 5 }) + ); + }); + expect(chrome.alarms.create).toHaveBeenCalledWith('threatcrush-event-check', { + periodInMinutes: 5, + }); + }); + + it('persists the selected scan interval and applies it to the event alarm', async () => { + render(); + + const intervalInput = screen.getByLabelText(/event check interval/i); + fireEvent.change(intervalInput, { target: { value: '15' } }); + fireEvent.click(screen.getByRole('button', { name: /save settings/i })); + + await waitFor(() => { + expect(chrome.storage.local.set).toHaveBeenCalledWith( + expect.objectContaining({ scanInterval: 15 }) + ); + }); + expect(chrome.alarms.create).toHaveBeenCalledWith('threatcrush-event-check', { + periodInMinutes: 15, + }); + }); +}); diff --git a/apps/extension/src/options/App.jsx b/apps/extension/src/options/App.jsx index dbc4d97..04cb925 100644 --- a/apps/extension/src/options/App.jsx +++ b/apps/extension/src/options/App.jsx @@ -8,6 +8,20 @@ const DEFAULT_SETTINGS = { scanInterval: 5, }; +const MIN_SCAN_INTERVAL = 1; +const MAX_SCAN_INTERVAL = 60; +const EVENT_CHECK_ALARM = 'threatcrush-event-check'; + +function normalizeScanInterval(value) { + const parsed = Number.parseInt(value, 10); + + if (!Number.isFinite(parsed)) { + return DEFAULT_SETTINGS.scanInterval; + } + + return Math.min(MAX_SCAN_INTERVAL, Math.max(MIN_SCAN_INTERVAL, parsed)); +} + export default function App() { const [settings, setSettings] = useState(DEFAULT_SETTINGS); const [saved, setSaved] = useState(false); @@ -21,7 +35,16 @@ export default function App() { async function handleSave(e) { e.preventDefault(); - await chrome.storage.local.set(settings); + const nextSettings = { + ...settings, + scanInterval: normalizeScanInterval(settings.scanInterval), + }; + + await chrome.storage.local.set(nextSettings); + await chrome.alarms.create(EVENT_CHECK_ALARM, { + periodInMinutes: nextSettings.scanInterval, + }); + setSettings(nextSettings); setSaved(true); setTimeout(() => setSaved(false), 2000); } @@ -48,8 +71,9 @@ export default function App() {
- + updateSetting('serverUrl', e.target.value)} @@ -58,8 +82,9 @@ export default function App() {
- + updateSetting('licenseKey', e.target.value)} @@ -105,15 +130,16 @@ export default function App() {
-