Skip to content

Commit

Permalink
persist user settings
Browse files Browse the repository at this point in the history
  • Loading branch information
warrenday committed Jul 17, 2024
1 parent 6b115b1 commit b9c14c0
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 48 deletions.
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"128": "icon.png"
},
"manifest_version": 3,
"permissions": ["webRequest"],
"permissions": ["webRequest", "storage"],
"host_permissions": ["<all_urls>"],
"devtools_page": "devtools/devtools.html",
"content_scripts": [
Expand Down
5 changes: 5 additions & 0 deletions src/containers/Main/Main.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
35 changes: 20 additions & 15 deletions src/containers/NetworkPanel/index.test.tsx
Original file line number Diff line number Diff line change
@@ -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: "<div>hi</div>",
markup: '<div>hi</div>',
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(
<NetworkPanel
selectedRowId={null}
Expand All @@ -20,22 +25,22 @@ describe("NetworkPanel", () => {
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(
<NetworkPanel
selectedRowId={null}
Expand All @@ -45,14 +50,14 @@ describe("NetworkPanel", () => {
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()
})
})
69 changes: 37 additions & 32 deletions src/containers/NetworkPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 }
}
Expand All @@ -48,7 +49,7 @@ const filterNetworkRequests = (
}

const results = networkRequests.filter((networkRequest) => {
const { operationName = "", operation } =
const { operationName = '', operation } =
networkRequest.request.primaryOperation

if (!options.operationFilters[operation]) {
Expand All @@ -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,
})

Expand All @@ -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,
Expand All @@ -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])
Expand All @@ -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])
Expand All @@ -157,12 +156,18 @@ export const NetworkPanel = (props: NetworkPanelProps) => {
<Toolbar
filterValue={filterValue}
onFilterValueChange={setFilterValue}
preserveLogs={isPreserveLogs}
onPreserveLogsChange={setIsPreserveLogs}
inverted={isInverted}
onInvertedChange={setIsInverted}
regexActive={isRegexActive}
onRegexActiveChange={onIsRegexActiveChange}
preserveLogs={userSettings.isPreserveLogsActive}
onPreserveLogsChange={(isPreserveLogsActive) => {
setUserSettings({ isPreserveLogsActive })
}}
inverted={userSettings.isInvertFilterActive}
onInvertedChange={(isInvertFilterActive) => {
setUserSettings({ isInvertFilterActive })
}}
regexActive={userSettings.isRegexActive}
onRegexActiveChange={(isRegexActive) => {
setUserSettings({ isRegexActive })
}}
onClear={() => {
setSelectedRowId(null)
clearWebRequests()
Expand Down
49 changes: 49 additions & 0 deletions src/hooks/useUserSettings.test.ts
Original file line number Diff line number Diff line change
@@ -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,
})
})
})
32 changes: 32 additions & 0 deletions src/hooks/useUserSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useEffect, useState } from 'react'
import {
getUserSettings,
IUserSettings,
setUserSettings,
} from '../services/userSettingsService'

const useUserSettings = () => {
const [settings, setSettings] = useState<IUserSettings>({
isPreserveLogsActive: false,
isInvertFilterActive: false,
isRegexActive: false,
})

// Load initial settings on component mount
useEffect(() => {
getUserSettings((userSettings) => {
setSettings((prevSettings) => {
return { ...prevSettings, ...userSettings }
})
})
}, [])

const setSettingsProxy = (newSettings: Partial<IUserSettings>) => {
setUserSettings({ ...settings, ...newSettings })
setSettings({ ...settings, ...newSettings })
}

return [settings, setSettingsProxy] as const
}

export default useUserSettings
12 changes: 12 additions & 0 deletions src/mocks/mock-chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -95,6 +97,16 @@ const mockedChrome: DeepPartial<typeof chrome> = {
removeListener: () => {},
},
},
storage: {
local: {
get: ((keys, cb) => {
return cb({ ...mockStorage })
}) as typeof chrome.storage.local.get,
set: async (items: Record<string, any>) => {
mockStorage = { ...mockStorage, ...items }
},
},
},
}

const mockChrome = mockedChrome as typeof chrome
Expand Down
21 changes: 21 additions & 0 deletions src/services/userSettingsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { chromeProvider } from './chromeProvider'

export interface IUserSettings {
isPreserveLogsActive: boolean
isInvertFilterActive: boolean
isRegexActive: boolean
}

export const getUserSettings = (
cb: (settings: Partial<IUserSettings>) => void
) => {
const chrome = chromeProvider()
chrome.storage.local.get('userSettings', (result) => {
cb(result.userSettings || {})
})
}

export const setUserSettings = (userSettings: Partial<IUserSettings>): void => {
const chrome = chromeProvider()
chrome.storage.local.set({ userSettings })
}

0 comments on commit b9c14c0

Please sign in to comment.