mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
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:
parent
2a2c180c73
commit
279bfb1e64
@ -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'
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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' },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user