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() {