fix(middleware/devtools): add enabled option and show devtools warning only if enabled (#880)

* remove warning

* test

* rfc: enabled option

* debug

* update

* update

* [DEV-ONLY]

* [DEV-ONLY]

* let enable enabled

* let's enable enabled

* lint

* Update src/middleware/devtools.ts

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>

* add tests for different cases

* refactor test

* fix test

* tweak mock in test

* fix merge main

Co-authored-by: daishi <daishi@axlight.com>
Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
This commit is contained in:
M. Bagher Abiat 2022-04-18 05:50:21 +04:30 committed by GitHub
parent 2a2c180c73
commit 279bfb1e64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 50 additions and 40 deletions

View File

@ -57,6 +57,7 @@ type StoreSetStateWithAction<S> = S extends {
: never : never
interface DevtoolsOptions { interface DevtoolsOptions {
enabled?: boolean
name?: string name?: string
anonymousActionType?: string anonymousActionType?: string
serialize?: serialize?:
@ -113,15 +114,17 @@ const devtoolsImpl: DevtoolsImpl = (fn, options) => (set, get, api) => {
? { name: options } ? { name: options }
: options : options
let extensionConnector const { enabled } = devtoolsOptions as { enabled?: boolean }
let extensionConnector: typeof window['__REDUX_DEVTOOLS_EXTENSION__'] | false
try { try {
extensionConnector = window.__REDUX_DEVTOOLS_EXTENSION__ extensionConnector =
(enabled ?? __DEV__) && window.__REDUX_DEVTOOLS_EXTENSION__
} catch { } catch {
// ignored // ignored
} }
if (!extensionConnector) { if (!extensionConnector) {
if (__DEV__ && typeof window !== 'undefined') { if (__DEV__ && enabled) {
console.warn( console.warn(
'[zustand devtools middleware] Please install/enable Redux devtools extension' '[zustand devtools middleware] Please install/enable Redux devtools extension'
) )

View File

@ -26,7 +26,7 @@ beforeEach(() => {
}) })
it('connects to the extension by passing the options and initializes', () => { it('connects to the extension by passing the options and initializes', () => {
const options = { name: 'test', foo: 'bar' } const options = { name: 'test', foo: 'bar', enabled: true }
const initialState = { count: 0 } const initialState = { count: 0 }
create(devtools(() => initialState, options)) create(devtools(() => initialState, options))
@ -35,51 +35,57 @@ it('connects to the extension by passing the options and initializes', () => {
}) })
describe('If there is no extension installed...', () => { describe('If there is no extension installed...', () => {
let savedConsoleWarn: any
let savedDEV: boolean let savedDEV: boolean
beforeAll(() => { beforeEach(() => {
savedConsoleWarn = console.warn
console.warn = jest.fn()
savedDEV = __DEV__ savedDEV = __DEV__
;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = undefined ;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = undefined
}) })
afterAll(() => { afterEach(() => {
console.warn = savedConsoleWarn
__DEV__ = savedDEV __DEV__ = savedDEV
;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = extensionConnector ;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = extensionConnector
}) })
it('does not throw', () => { it('does not throw', () => {
__DEV__ = false
expect(() => { expect(() => {
create(devtools(() => ({ count: 0 }))) create(devtools(() => ({ count: 0 })))
}).not.toThrow() }).not.toThrow()
}) })
it('[DEV-ONLY] warns in dev env', () => { it('does not warn if not enabled', () => {
__DEV__ = true
const originalConsoleWarn = console.warn
console.warn = jest.fn()
create(devtools(() => ({ count: 0 }))) create(devtools(() => ({ count: 0 })))
expect(console.warn).toHaveBeenLastCalledWith( expect(console.warn).not.toBeCalled()
'[zustand devtools middleware] Please install/enable Redux devtools extension' })
)
console.warn = originalConsoleWarn it('[DEV-ONLY] warns if enabled in dev mode', () => {
__DEV__ = true
create(devtools(() => ({ count: 0 }), { enabled: true }))
expect(console.warn).toBeCalled()
}) })
it('[PRD-ONLY] does not warn if not in dev env', () => { it('[PRD-ONLY] does not warn if not in dev env', () => {
__DEV__ = false __DEV__ = false
const consoleWarn = jest.spyOn(console, 'warn')
create(devtools(() => ({ count: 0 }))) create(devtools(() => ({ count: 0 })))
expect(consoleWarn).not.toBeCalled() expect(console.warn).not.toBeCalled()
})
consoleWarn.mockRestore() it('[PRD-ONLY] does not warn if not in dev env even if enabled', () => {
__DEV__ = false
create(devtools(() => ({ count: 0 }), { enabled: true }))
expect(console.warn).not.toBeCalled()
}) })
}) })
describe('When state changes...', () => { describe('When state changes...', () => {
it("sends { type: setStateName || 'anonymous` } as the action with current state", () => { it("sends { type: setStateName || 'anonymous` } as the action with current state", () => {
const api = create( const api = create(
devtools(() => ({ count: 0, foo: 'bar' }), { name: 'testOptionsName' }) devtools(() => ({ count: 0, foo: 'bar' }), {
name: 'testOptionsName',
enabled: true,
})
) )
api.setState({ count: 10 }, false, 'testSetStateName') api.setState({ count: 10 }, false, 'testSetStateName')
expect(extension.send).toHaveBeenLastCalledWith( expect(extension.send).toHaveBeenLastCalledWith(
@ -98,7 +104,7 @@ describe('when it receives an message of type...', () => {
describe('ACTION...', () => { describe('ACTION...', () => {
it('does nothing', () => { it('does nothing', () => {
const initialState = { count: 0 } const initialState = { count: 0 }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
const setState = jest.spyOn(api, 'setState') const setState = jest.spyOn(api, 'setState')
;(extensionSubscriber as (message: any) => void)({ ;(extensionSubscriber as (message: any) => void)({
@ -112,7 +118,7 @@ describe('when it receives an message of type...', () => {
it('unless action type is __setState', () => { it('unless action type is __setState', () => {
const initialState = { count: 0 } const initialState = { count: 0 }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
;(extensionSubscriber as (message: any) => void)({ ;(extensionSubscriber as (message: any) => void)({
type: 'ACTION', type: 'ACTION',
@ -124,7 +130,7 @@ describe('when it receives an message of type...', () => {
it('does nothing even if there is `api.dispatch`', () => { it('does nothing even if there is `api.dispatch`', () => {
const initialState = { count: 0 } const initialState = { count: 0 }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
;(api as any).dispatch = jest.fn() ;(api as any).dispatch = jest.fn()
const setState = jest.spyOn(api, 'setState') const setState = jest.spyOn(api, 'setState')
@ -140,7 +146,7 @@ describe('when it receives an message of type...', () => {
it('dispatches with `api.dispatch` when `api.dispatchFromDevtools` is set to true', () => { it('dispatches with `api.dispatch` when `api.dispatchFromDevtools` is set to true', () => {
const initialState = { count: 0 } const initialState = { count: 0 }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
;(api as any).dispatch = jest.fn() ;(api as any).dispatch = jest.fn()
;(api as any).dispatchFromDevtools = true ;(api as any).dispatchFromDevtools = true
const setState = jest.spyOn(api, 'setState') const setState = jest.spyOn(api, 'setState')
@ -159,7 +165,7 @@ describe('when it receives an message of type...', () => {
it('does not throw for unsupported payload', () => { it('does not throw for unsupported payload', () => {
const initialState = { count: 0 } const initialState = { count: 0 }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
;(api as any).dispatch = jest.fn() ;(api as any).dispatch = jest.fn()
;(api as any).dispatchFromDevtools = true ;(api as any).dispatchFromDevtools = true
const setState = jest.spyOn(api, 'setState') const setState = jest.spyOn(api, 'setState')
@ -206,7 +212,7 @@ describe('when it receives an message of type...', () => {
describe('DISPATCH and payload of type...', () => { describe('DISPATCH and payload of type...', () => {
it('RESET, it inits with initial state', () => { it('RESET, it inits with initial state', () => {
const initialState = { count: 0 } const initialState = { count: 0 }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
api.setState({ count: 1 }) api.setState({ count: 1 })
extension.send.mockClear() extension.send.mockClear()
@ -222,7 +228,7 @@ describe('when it receives an message of type...', () => {
it('COMMIT, it inits with current state', () => { it('COMMIT, it inits with current state', () => {
const initialState = { count: 0 } const initialState = { count: 0 }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
api.setState({ count: 2 }) api.setState({ count: 2 })
const currentState = api.getState() const currentState = api.getState()
@ -239,7 +245,7 @@ describe('when it receives an message of type...', () => {
describe('ROLLBACK...', () => { describe('ROLLBACK...', () => {
it('it updates state without recording and inits with `message.state`', () => { it('it updates state without recording and inits with `message.state`', () => {
const initialState = { count: 0, increment: () => {} } const initialState = { count: 0, increment: () => {} }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
const newState = { foo: 'bar' } const newState = { foo: 'bar' }
extension.send.mockClear() extension.send.mockClear()
@ -260,7 +266,7 @@ describe('when it receives an message of type...', () => {
it('does not throw for unparsable `message.state`', () => { it('does not throw for unparsable `message.state`', () => {
const increment = () => {} const increment = () => {}
const initialState = { count: 0, increment } const initialState = { count: 0, increment }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
const originalConsoleError = console.error const originalConsoleError = console.error
console.error = jest.fn() console.error = jest.fn()
@ -294,7 +300,7 @@ describe('when it receives an message of type...', () => {
const increment = () => {} const increment = () => {}
it('it updates state without recording with `message.state`', () => { it('it updates state without recording with `message.state`', () => {
const initialState = { count: 0, increment } const initialState = { count: 0, increment }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
const newState = { foo: 'bar' } const newState = { foo: 'bar' }
extension.send.mockClear() extension.send.mockClear()
@ -309,7 +315,7 @@ describe('when it receives an message of type...', () => {
it('does not throw for unparsable `message.state`', () => { it('does not throw for unparsable `message.state`', () => {
const initialState = { count: 0, increment: () => {} } const initialState = { count: 0, increment: () => {} }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
const originalConsoleError = console.error const originalConsoleError = console.error
console.error = jest.fn() console.error = jest.fn()
@ -340,7 +346,7 @@ describe('when it receives an message of type...', () => {
describe('JUMP_TO_ACTION...', () => { describe('JUMP_TO_ACTION...', () => {
it('it updates state without recording with `message.state`', () => { it('it updates state without recording with `message.state`', () => {
const initialState = { count: 0, increment: () => {} } const initialState = { count: 0, increment: () => {} }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
const newState = { foo: 'bar' } const newState = { foo: 'bar' }
extension.send.mockClear() extension.send.mockClear()
@ -356,7 +362,7 @@ describe('when it receives an message of type...', () => {
it('does not throw for unparsable `message.state`', () => { it('does not throw for unparsable `message.state`', () => {
const increment = () => {} const increment = () => {}
const initialState = { count: 0, increment } const initialState = { count: 0, increment }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
const originalConsoleError = console.error const originalConsoleError = console.error
console.error = jest.fn() console.error = jest.fn()
@ -386,7 +392,7 @@ describe('when it receives an message of type...', () => {
it('IMPORT_STATE, it updates state without recording and inits the last computedState', () => { it('IMPORT_STATE, it updates state without recording and inits the last computedState', () => {
const initialState = { count: 0, increment: () => {} } const initialState = { count: 0, increment: () => {} }
const api = create(devtools(() => initialState)) const api = create(devtools(() => initialState, { enabled: true }))
const nextLiftedState = { const nextLiftedState = {
computedStates: [{ state: { count: 4 } }, { state: { count: 5 } }], computedStates: [{ state: { count: 4 } }, { state: { count: 5 } }],
} }
@ -407,7 +413,7 @@ describe('when it receives an message of type...', () => {
}) })
it('PAUSE_RECORDING, it toggles the sending of actions', () => { it('PAUSE_RECORDING, it toggles the sending of actions', () => {
const api = create(devtools(() => ({ count: 0 }))) const api = create(devtools(() => ({ count: 0 }), { enabled: true }))
api.setState({ count: 1 }, false, 'increment') api.setState({ count: 1 }, false, 'increment')
expect(extension.send).toHaveBeenLastCalledWith( expect(extension.send).toHaveBeenLastCalledWith(
@ -457,7 +463,8 @@ describe('with redux middleware', () => {
count: count + (type === 'INCREMENT' ? 1 : -1), count: count + (type === 'INCREMENT' ? 1 : -1),
}), }),
{ count: 0 } { count: 0 }
) ),
{ enabled: true }
) )
) )
;(api as any).dispatch({ type: 'INCREMENT' }) ;(api as any).dispatch({ type: 'INCREMENT' })
@ -494,7 +501,7 @@ it('works in non-browser env', () => {
global.window = undefined as any global.window = undefined as any
expect(() => { expect(() => {
create(devtools(() => ({ count: 0 }))) create(devtools(() => ({ count: 0 }), { enabled: true }))
}).not.toThrow() }).not.toThrow()
global.window = originalWindow global.window = originalWindow
@ -505,14 +512,14 @@ it('works in react native env', () => {
global.window = {} as any global.window = {} as any
expect(() => { expect(() => {
create(devtools(() => ({ count: 0 }))) create(devtools(() => ({ count: 0 }), { enabled: true }))
}).not.toThrow() }).not.toThrow()
global.window = originalWindow global.window = originalWindow
}) })
it('preserves isRecording after setting from devtools', () => { it('preserves isRecording after setting from devtools', () => {
const api = create(devtools(() => ({ count: 0 }))) const api = create(devtools(() => ({ count: 0 }), { enabled: true }))
;(extensionSubscriber as (message: any) => void)({ ;(extensionSubscriber as (message: any) => void)({
type: 'DISPATCH', type: 'DISPATCH',
payload: { type: 'PAUSE_RECORDING' }, payload: { type: 'PAUSE_RECORDING' },