diff --git a/public/manifest.json b/public/manifest.json index 85f94b4..6123d52 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -6,7 +6,7 @@ "128": "icon.png" }, "manifest_version": 3, - "permissions": ["webRequest"], + "permissions": ["webRequest", "storage"], "host_permissions": [""], "devtools_page": "devtools/devtools.html", "content_scripts": [ diff --git a/src/containers/Main/Main.test.tsx b/src/containers/Main/Main.test.tsx index d026d32..e96be21 100644 --- a/src/containers/Main/Main.test.tsx +++ b/src/containers/Main/Main.test.tsx @@ -14,6 +14,11 @@ jest.mock('@/hooks/useHighlight', () => ({ }), })) +jest.mock('@/services/userSettingsService', () => ({ + getUserSettings: jest.fn(), + setUserSettings: jest.fn(), +})) + const mockChromeProvider = chromeProvider as jest.Mock const mockOnNavigated = () => { diff --git a/src/containers/NetworkPanel/index.test.tsx b/src/containers/NetworkPanel/index.test.tsx index 69e093d..a7ce787 100644 --- a/src/containers/NetworkPanel/index.test.tsx +++ b/src/containers/NetworkPanel/index.test.tsx @@ -1,16 +1,21 @@ -import { fireEvent } from "@testing-library/react" -import { NetworkPanel } from "./index" -import { render } from "../../test-utils" +import { fireEvent } from '@testing-library/react' +import { NetworkPanel } from './index' +import { render } from '../../test-utils' -jest.mock("@/hooks/useHighlight", () => ({ +jest.mock('@/hooks/useHighlight', () => ({ useHighlight: () => ({ - markup: "
hi
", + markup: '
hi
', loading: false, }), })) -describe("NetworkPanel", () => { - it("invalid regex is provided, regex mode is on - error message is rendered", () => { +jest.mock('@/services/userSettingsService', () => ({ + getUserSettings: jest.fn(), + setUserSettings: jest.fn(), +})) + +describe('NetworkPanel', () => { + it('invalid regex is provided, regex mode is on - error message is rendered', () => { const { getByTestId, getByText } = render( { clearWebRequests={() => {}} /> ) - const filterInput = getByTestId("filter-input") - const regexCheckbox = getByTestId("regex-checkbox") + const filterInput = getByTestId('filter-input') + const regexCheckbox = getByTestId('regex-checkbox') // click the regex checkbox to turn the regex mode on fireEvent.click(regexCheckbox) // enter an invalid regex into the filter input - fireEvent.change(filterInput, { target: { value: "++" } }) + fireEvent.change(filterInput, { target: { value: '++' } }) // ensure the error message related to the invalid regex was rendered expect( - getByText("Invalid regular expression: /++/: Nothing to repeat") + getByText('Invalid regular expression: /++/: Nothing to repeat') ).toBeInTheDocument() }) - it("invalid regex is provided, regex mode is off - error message is not rendered", () => { + it('invalid regex is provided, regex mode is off - error message is not rendered', () => { const { getByTestId, queryByText } = render( { clearWebRequests={() => {}} /> ) - const filterInput = getByTestId("filter-input") + const filterInput = getByTestId('filter-input') // enter an invalid regex into the filter input - fireEvent.change(filterInput, { target: { value: "++" } }) + fireEvent.change(filterInput, { target: { value: '++' } }) // ensure the error message related to the invalid regex was not rendered expect( - queryByText("Invalid regular expression: /++/: Nothing to repeat") + queryByText('Invalid regular expression: /++/: Nothing to repeat') ).not.toBeInTheDocument() }) }) diff --git a/src/containers/NetworkPanel/index.tsx b/src/containers/NetworkPanel/index.tsx index a3c9651..e8c0f34 100644 --- a/src/containers/NetworkPanel/index.tsx +++ b/src/containers/NetworkPanel/index.tsx @@ -1,17 +1,18 @@ -import { useState, useEffect, useMemo } from "react" -import RegexParser from "regex-parser" -import { SplitPaneLayout } from "@/components/Layout" -import { onNavigate } from "@/services/networkMonitor" -import { IWebSocketNetworkRequest } from "@/hooks/useWebSocketNetworkMonitor" -import { INetworkRequest } from "@/helpers/networkHelpers" -import { NetworkTable, INetworkTableDataRow } from "./NetworkTable" -import { NetworkDetails } from "./NetworkDetails" -import { Toolbar } from "../Toolbar" -import WebSocketNetworkDetails from "./WebSocketNetworkDetails" +import { useState, useEffect, useMemo } from 'react' +import RegexParser from 'regex-parser' +import { SplitPaneLayout } from '@/components/Layout' +import { onNavigate } from '@/services/networkMonitor' +import { IWebSocketNetworkRequest } from '@/hooks/useWebSocketNetworkMonitor' +import { INetworkRequest } from '@/helpers/networkHelpers' +import { NetworkTable, INetworkTableDataRow } from './NetworkTable' +import { NetworkDetails } from './NetworkDetails' +import { Toolbar } from '../Toolbar' +import WebSocketNetworkDetails from './WebSocketNetworkDetails' import { IOperationFilters, useOperationFilters, -} from "../../hooks/useOperationFilters" +} from '../../hooks/useOperationFilters' +import useUserSettings from '../../hooks/useUserSettings' interface NetworkPanelProps { selectedRowId: string | number | null @@ -26,7 +27,7 @@ const getRegex = (str: string) => { const regex = RegexParser(str) return { regex, errorMessage: null } } catch (error) { - let message = "Invalid Regex" + let message = 'Invalid Regex' if (error instanceof Error) message = error.message return { regex: null, errorMessage: message } } @@ -48,7 +49,7 @@ const filterNetworkRequests = ( } const results = networkRequests.filter((networkRequest) => { - const { operationName = "", operation } = + const { operationName = '', operation } = networkRequest.request.primaryOperation if (!options.operationFilters[operation]) { @@ -74,16 +75,14 @@ export const NetworkPanel = (props: NetworkPanelProps) => { setSelectedRowId, } = props - const [filterValue, setFilterValue] = useState("") - const [isPreserveLogs, setIsPreserveLogs] = useState(false) - const [isInverted, setIsInverted] = useState(false) - const [isRegexActive, onIsRegexActiveChange] = useState(false) + const [filterValue, setFilterValue] = useState('') + const [userSettings, setUserSettings] = useUserSettings() const { operationFilters } = useOperationFilters() const { results: filteredNetworkRequests, errorMessage: filterError } = filterNetworkRequests(networkRequests, filterValue, { - isInverted, - isRegex: isRegexActive, + isInverted: userSettings.isInvertFilterActive, + isRegex: userSettings.isRegexActive, operationFilters, }) @@ -107,15 +106,15 @@ export const NetworkPanel = (props: NetworkPanelProps) => { useEffect(() => { return onNavigate(() => { - if (!isPreserveLogs) { + if (!userSettings.isPreserveLogsActive) { clearWebRequests() } }) - }, [isPreserveLogs, clearWebRequests]) + }, [userSettings.isPreserveLogsActive, clearWebRequests]) const networkTableData = useMemo((): INetworkTableDataRow[] => { return filteredNetworkRequests.map((networkRequest) => { - const { operationName = "", operation } = + const { operationName = '', operation } = networkRequest.request.primaryOperation return { id: networkRequest.id, @@ -126,7 +125,7 @@ export const NetworkPanel = (props: NetworkPanelProps) => { size: networkRequest.response?.bodySize || 0, time: networkRequest.time, url: networkRequest.url, - responseBody: networkRequest.response?.body || "", + responseBody: networkRequest.response?.body || '', } }) }, [filteredNetworkRequests]) @@ -135,14 +134,14 @@ export const NetworkPanel = (props: NetworkPanelProps) => { return filteredWebsocketNetworkRequests.map((websocketRequest) => { return { id: websocketRequest.id, - type: "subscription", - name: "subscription", + type: 'subscription', + name: 'subscription', total: 1, status: websocketRequest.status, size: 0, time: 0, url: websocketRequest.url, - responseBody: "", + responseBody: '', } }) }, [filteredWebsocketNetworkRequests]) @@ -157,12 +156,18 @@ export const NetworkPanel = (props: NetworkPanelProps) => { { + setUserSettings({ isPreserveLogsActive }) + }} + inverted={userSettings.isInvertFilterActive} + onInvertedChange={(isInvertFilterActive) => { + setUserSettings({ isInvertFilterActive }) + }} + regexActive={userSettings.isRegexActive} + onRegexActiveChange={(isRegexActive) => { + setUserSettings({ isRegexActive }) + }} onClear={() => { setSelectedRowId(null) clearWebRequests() diff --git a/src/hooks/useUserSettings.test.ts b/src/hooks/useUserSettings.test.ts new file mode 100644 index 0000000..b3c40c7 --- /dev/null +++ b/src/hooks/useUserSettings.test.ts @@ -0,0 +1,49 @@ +import { renderHook, act } from '@testing-library/react-hooks' +import useUserSettings from './useUserSettings' +import * as userSettingsService from '../services/userSettingsService' + +// Mock the userSettingsService module +jest.mock('../services/userSettingsService') + +const mockUserSettings = userSettingsService as jest.Mocked< + typeof userSettingsService +> + +describe('useUserSettings', () => { + it('should initialize with default settings and allow updates', async () => { + mockUserSettings.getUserSettings.mockImplementation((cb) => { + cb({ isPreserveLogsActive: true }) + }) + + const { result } = renderHook(() => useUserSettings()) + + // Expect initial settings to be loaded into state + expect(result.current[0]).toEqual({ + isPreserveLogsActive: true, + isInvertFilterActive: false, + isRegexActive: false, + }) + + // Update the state + act(() => { + result.current[1]({ + isPreserveLogsActive: false, + isInvertFilterActive: true, + }) + }) + + // Expect state to be updated + expect(result.current[0]).toEqual({ + isPreserveLogsActive: false, + isInvertFilterActive: true, + isRegexActive: false, + }) + + // Expect setUserSettings was called with the new settings + expect(userSettingsService.setUserSettings).toHaveBeenCalledWith({ + isPreserveLogsActive: false, + isInvertFilterActive: true, + isRegexActive: false, + }) + }) +}) diff --git a/src/hooks/useUserSettings.ts b/src/hooks/useUserSettings.ts new file mode 100644 index 0000000..f03c931 --- /dev/null +++ b/src/hooks/useUserSettings.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react' +import { + getUserSettings, + IUserSettings, + setUserSettings, +} from '../services/userSettingsService' + +const useUserSettings = () => { + const [settings, setSettings] = useState({ + isPreserveLogsActive: false, + isInvertFilterActive: false, + isRegexActive: false, + }) + + // Load initial settings on component mount + useEffect(() => { + getUserSettings((userSettings) => { + setSettings((prevSettings) => { + return { ...prevSettings, ...userSettings } + }) + }) + }, []) + + const setSettingsProxy = (newSettings: Partial) => { + setUserSettings({ ...settings, ...newSettings }) + setSettings({ ...settings, ...newSettings }) + } + + return [settings, setSettingsProxy] as const +} + +export default useUserSettings diff --git a/src/mocks/mock-chrome.ts b/src/mocks/mock-chrome.ts index 18fe1c2..82b7b37 100644 --- a/src/mocks/mock-chrome.ts +++ b/src/mocks/mock-chrome.ts @@ -2,6 +2,8 @@ import { DeepPartial } from 'utility-types' import EventEmitter from 'eventemitter3' import { IMockRequest, mockRequests } from '../mocks/mock-requests' +let mockStorage = {} + const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) // Configure an event to add more mock requests @@ -95,6 +97,16 @@ const mockedChrome: DeepPartial = { removeListener: () => {}, }, }, + storage: { + local: { + get: ((keys, cb) => { + return cb({ ...mockStorage }) + }) as typeof chrome.storage.local.get, + set: async (items: Record) => { + mockStorage = { ...mockStorage, ...items } + }, + }, + }, } const mockChrome = mockedChrome as typeof chrome diff --git a/src/services/userSettingsService.ts b/src/services/userSettingsService.ts new file mode 100644 index 0000000..30ea03f --- /dev/null +++ b/src/services/userSettingsService.ts @@ -0,0 +1,21 @@ +import { chromeProvider } from './chromeProvider' + +export interface IUserSettings { + isPreserveLogsActive: boolean + isInvertFilterActive: boolean + isRegexActive: boolean +} + +export const getUserSettings = ( + cb: (settings: Partial) => void +) => { + const chrome = chromeProvider() + chrome.storage.local.get('userSettings', (result) => { + cb(result.userSettings || {}) + }) +} + +export const setUserSettings = (userSettings: Partial): void => { + const chrome = chromeProvider() + chrome.storage.local.set({ userSettings }) +}