mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
* imaginary code that uses uSES * revert backward compatibility code as this is not going to be v4 * use use-sync-external-store * revert to react 17 * handle error by our own * v4.0.0-alpha.2 * fix&refactor a bit * update uSES experimental package * remove error propagation hack * update size snapshot * update uSES and add dts * split react.ts and no export wild * split useStore impl * context to follow the new api, export wild again * v4.0.0-alpha.3 * add missing await * update uSES * update uSES * uses uSES extra! * v4.0.0-alpha.3 * fix(types): Rename from UseStore to UseBoundStore * breaking(types): drop deprecated UseStore type * breaking(core): drop v2 hook compatibility * breaking(middleware): drop deprecated persist options * breaking(core): drop deprecated store.subscribe with selector * update uSES * fix update uSES * v4.0.0-alpha.5 * combine subscribe type * intentional undefined type * add useDebugValue * update uSES * update uSES types * breaking(middleware): make persist options.removeItem required * update uSES * v4.0.0-alpha.6 * fix(readme): remove memoization section which is no longer valid with uSES * feat(readme): add new createStore/useStore usage * update useSES * update uSES and deps * v4.0.0-alpha.7 * update uSES * update uSES * shave bytes * vanilla: add higher kinded mutator types * persist: add higher kinded mutator types * persist: try to minimize diff * use `PopArgument` in vanilla too * update uSES * use overloads instead of `createWithState` * avoid symbols * add new types to middlewares * add new types react * add new types to context * fix persist types * add immer * migrate middleware type tests * fix react type, export `UseBoundStore` * migrate vanilla type tests * rename `_createStore` to `createStoreImpl` * Default to no mutations in `StateCreator` * migrate context.test.tsx * fix devtools.test.tsx type erros * context: remove callsignature in useStoreApi * context: remove `UseContextStore` type * context: fix useBoundStore type * context: keep `UseContextStore` for tooltip just don't export it * react: remove duplicate overload in create * export `WithPersist` * devtools: preserve try/catch * devtools: preserve window check * add a test case for v3 style create * devtools: preverse test fix from base branch * remove StoreApiWithFoo types, don't export WithFoo types * style * devtools: preverse `originalIsRecording` change * fix bug in devtools * empty commit * 4.0.0-beta.1 * fix lint * style * export immer fix tests * style, minor fixes * devtools: fix test * Update tests/devtools.test.tsx * breaking(middleware/devtools): use official devtools extension types * type object.create * avoid emitting @redux-devtools/extension * fix type with any * refactor * fix yarn lock * temporary fix #829 * v4.0.0-beta.2 * fix lint * lock date-fns version * test middleware subtyping * fix errors in conflict resolution * lock testing-library/react alpha version * more correct (and strict) persist types * migrate tests * wip release notes * fix devtools merge with base Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com> * add a test case for persist with partialize option * update readme * fix lint * immer: mutate `store.setState` * fix devtools merge with base * immer: fix mutations order * changes in readme * move and rename v4 migration md * add `combine` usage in readme * typos * create separate md for typescript, add common recipes * minor fixes * devtools: minor type fix (probably I copy pasted from persist and forgot to remove `U`) * add more migrations * context: fix import Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com> * devtools: minor refactor rearrange code for better diff * fix lint: run prettier * wip * uSES rc.1 * getServerState for #886, no types yet * uSES v1 * devtools: remove deprecations and warnings * fix tests * v4.0.0-beta.2 * wip * migrate tests * persist: keep diff minimal * fix merge in package.json and yarn.lock * fix merge for persist * don't use `import type` * docs(typescript): add slices pattern * fix selector & equals types for inference with useCallback, see issue #812 * add test for setState with replace * remove undefined selector overload * make immer more generic * make devtools replace-friendly and more correctly typed * migrate tests * make setState bivariant to make the state covariant * devtools: return the result of `setState` * devtools: make the fallback branch in `StoreSetStateWithAction` bivariant too (I forgot to make this bivaraint) * remove strict replace * fix lint Co-authored-by: daishi <daishi@axlight.com> Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
528 lines
16 KiB
TypeScript
528 lines
16 KiB
TypeScript
import { devtools, redux } from 'zustand/middleware'
|
|
import create, { StoreApi } from 'zustand/vanilla'
|
|
|
|
let extensionSubscriber: ((message: any) => void) | undefined
|
|
const extension = {
|
|
subscribe: jest.fn((f) => {
|
|
extensionSubscriber = f
|
|
return () => {}
|
|
}),
|
|
unsubscribe: jest.fn(),
|
|
send: jest.fn(),
|
|
init: jest.fn(),
|
|
error: jest.fn(),
|
|
}
|
|
const extensionConnector = { connect: jest.fn(() => extension) }
|
|
;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = extensionConnector
|
|
|
|
beforeEach(() => {
|
|
extensionConnector.connect.mockClear()
|
|
extension.subscribe.mockClear()
|
|
extension.unsubscribe.mockClear()
|
|
extension.send.mockClear()
|
|
extension.init.mockClear()
|
|
extension.error.mockClear()
|
|
extensionSubscriber = undefined
|
|
})
|
|
|
|
it('connects to the extension by passing the options and initializes', () => {
|
|
const options = { name: 'test', foo: 'bar' }
|
|
const initialState = { count: 0 }
|
|
create(devtools(() => initialState, options))
|
|
|
|
expect(extensionConnector.connect).toHaveBeenLastCalledWith(options)
|
|
expect(extension.init).toHaveBeenLastCalledWith(initialState)
|
|
})
|
|
|
|
describe('If there is no extension installed...', () => {
|
|
let savedDEV: boolean
|
|
beforeAll(() => {
|
|
savedDEV = __DEV__
|
|
;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = undefined
|
|
})
|
|
afterAll(() => {
|
|
__DEV__ = savedDEV
|
|
;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = extensionConnector
|
|
})
|
|
|
|
it('does not throw', () => {
|
|
__DEV__ = false
|
|
expect(() => {
|
|
create(devtools(() => ({ count: 0 })))
|
|
}).not.toThrow()
|
|
})
|
|
|
|
it('[DEV-ONLY] warns in dev env', () => {
|
|
__DEV__ = true
|
|
const originalConsoleWarn = console.warn
|
|
console.warn = jest.fn()
|
|
|
|
create(devtools(() => ({ count: 0 })))
|
|
expect(console.warn).toHaveBeenLastCalledWith(
|
|
'[zustand devtools middleware] Please install/enable Redux devtools extension'
|
|
)
|
|
|
|
console.warn = originalConsoleWarn
|
|
})
|
|
|
|
it('[PRD-ONLY] does not warn if not in dev env', () => {
|
|
__DEV__ = false
|
|
const consoleWarn = jest.spyOn(console, 'warn')
|
|
|
|
create(devtools(() => ({ count: 0 })))
|
|
expect(consoleWarn).not.toBeCalled()
|
|
|
|
consoleWarn.mockRestore()
|
|
})
|
|
})
|
|
|
|
describe('When state changes...', () => {
|
|
it("sends { type: setStateName || 'anonymous` } as the action with current state", () => {
|
|
const api = create(
|
|
devtools(() => ({ count: 0, foo: 'bar' }), { name: 'testOptionsName' })
|
|
)
|
|
api.setState({ count: 10 }, false, 'testSetStateName')
|
|
expect(extension.send).toHaveBeenLastCalledWith(
|
|
{ type: 'testSetStateName' },
|
|
{ count: 10, foo: 'bar' }
|
|
)
|
|
api.setState({ count: 5, foo: 'baz' }, true)
|
|
expect(extension.send).toHaveBeenLastCalledWith(
|
|
{ type: 'anonymous' },
|
|
{ count: 5, foo: 'baz' }
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when it receives an message of type...', () => {
|
|
describe('ACTION...', () => {
|
|
it('does nothing', () => {
|
|
const initialState = { count: 0 }
|
|
const api = create(devtools(() => initialState))
|
|
const setState = jest.spyOn(api, 'setState')
|
|
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'ACTION',
|
|
payload: '{ "type": "INCREMENT" }',
|
|
})
|
|
|
|
expect(api.getState()).toBe(initialState)
|
|
expect(setState).not.toBeCalled()
|
|
})
|
|
|
|
it('unless action type is __setState', () => {
|
|
const initialState = { count: 0 }
|
|
const api = create(devtools(() => initialState))
|
|
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'ACTION',
|
|
payload: '{ "type": "__setState", "state": { "foo": "bar" } }',
|
|
})
|
|
|
|
expect(api.getState()).toStrictEqual({ ...initialState, foo: 'bar' })
|
|
})
|
|
|
|
it('does nothing even if there is `api.dispatch`', () => {
|
|
const initialState = { count: 0 }
|
|
const api = create(devtools(() => initialState))
|
|
;(api as any).dispatch = jest.fn()
|
|
const setState = jest.spyOn(api, 'setState')
|
|
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'ACTION',
|
|
payload: '{ "type": "INCREMENT" }',
|
|
})
|
|
|
|
expect(api.getState()).toBe(initialState)
|
|
expect(setState).not.toBeCalled()
|
|
expect((api as any).dispatch).not.toBeCalled()
|
|
})
|
|
|
|
it('dispatches with `api.dispatch` when `api.dispatchFromDevtools` is set to true', () => {
|
|
const initialState = { count: 0 }
|
|
const api = create(devtools(() => initialState))
|
|
;(api as any).dispatch = jest.fn()
|
|
;(api as any).dispatchFromDevtools = true
|
|
const setState = jest.spyOn(api, 'setState')
|
|
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'ACTION',
|
|
payload: '{ "type": "INCREMENT" }',
|
|
})
|
|
|
|
expect(api.getState()).toBe(initialState)
|
|
expect(setState).not.toBeCalled()
|
|
expect((api as any).dispatch).toHaveBeenLastCalledWith({
|
|
type: 'INCREMENT',
|
|
})
|
|
})
|
|
|
|
it('does not throw for unsupported payload', () => {
|
|
const initialState = { count: 0 }
|
|
const api = create(devtools(() => initialState))
|
|
;(api as any).dispatch = jest.fn()
|
|
;(api as any).dispatchFromDevtools = true
|
|
const setState = jest.spyOn(api, 'setState')
|
|
const originalConsoleError = console.error
|
|
console.error = jest.fn()
|
|
|
|
expect(() => {
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'ACTION',
|
|
payload: 'this.increment()',
|
|
})
|
|
}).not.toThrow()
|
|
|
|
expect(console.error).toHaveBeenLastCalledWith(
|
|
'[zustand devtools middleware] Could not parse the received json',
|
|
(() => {
|
|
try {
|
|
JSON.parse('this.increment()')
|
|
} catch (e) {
|
|
return e
|
|
}
|
|
})()
|
|
)
|
|
|
|
expect(() => {
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'ACTION',
|
|
payload: { name: 'increment', args: [] },
|
|
})
|
|
}).not.toThrow()
|
|
|
|
expect(console.error).toHaveBeenLastCalledWith(
|
|
'[zustand devtools middleware] Unsupported action format'
|
|
)
|
|
|
|
expect(api.getState()).toBe(initialState)
|
|
expect(setState).not.toBeCalled()
|
|
expect((api as any).dispatch).not.toBeCalled()
|
|
|
|
console.error = originalConsoleError
|
|
})
|
|
})
|
|
|
|
describe('DISPATCH and payload of type...', () => {
|
|
it('RESET, it inits with initial state', () => {
|
|
const initialState = { count: 0 }
|
|
const api = create(devtools(() => initialState))
|
|
api.setState({ count: 1 })
|
|
|
|
extension.send.mockClear()
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'RESET' },
|
|
})
|
|
|
|
expect(api.getState()).toStrictEqual(initialState)
|
|
expect(extension.init).toHaveBeenLastCalledWith(initialState)
|
|
expect(extension.send).not.toBeCalled()
|
|
})
|
|
|
|
it('COMMIT, it inits with current state', () => {
|
|
const initialState = { count: 0 }
|
|
const api = create(devtools(() => initialState))
|
|
api.setState({ count: 2 })
|
|
const currentState = api.getState()
|
|
|
|
extension.send.mockClear()
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'COMMIT' },
|
|
})
|
|
|
|
expect(extension.init).toHaveBeenLastCalledWith(currentState)
|
|
expect(extension.send).not.toBeCalled()
|
|
})
|
|
|
|
describe('ROLLBACK...', () => {
|
|
it('it updates state without recording and inits with `message.state`', () => {
|
|
const initialState = { count: 0, increment: () => {} }
|
|
const api = create(devtools(() => initialState))
|
|
const newState = { foo: 'bar' }
|
|
|
|
extension.send.mockClear()
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'ROLLBACK' },
|
|
state: JSON.stringify(newState),
|
|
})
|
|
|
|
expect(api.getState()).toStrictEqual({ ...initialState, ...newState })
|
|
expect(extension.init).toHaveBeenLastCalledWith({
|
|
...initialState,
|
|
...newState,
|
|
})
|
|
expect(extension.send).not.toBeCalled()
|
|
})
|
|
|
|
it('does not throw for unparsable `message.state`', () => {
|
|
const increment = () => {}
|
|
const initialState = { count: 0, increment }
|
|
const api = create(devtools(() => initialState))
|
|
const originalConsoleError = console.error
|
|
console.error = jest.fn()
|
|
|
|
extension.init.mockClear()
|
|
extension.send.mockClear()
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'ROLLBACK' },
|
|
state: 'foobar',
|
|
})
|
|
|
|
expect(console.error).toHaveBeenLastCalledWith(
|
|
'[zustand devtools middleware] Could not parse the received json',
|
|
(() => {
|
|
try {
|
|
JSON.parse('foobar')
|
|
} catch (e) {
|
|
return e
|
|
}
|
|
})()
|
|
)
|
|
expect(api.getState()).toBe(initialState)
|
|
expect(extension.init).not.toBeCalled()
|
|
expect(extension.send).not.toBeCalled()
|
|
|
|
console.error = originalConsoleError
|
|
})
|
|
})
|
|
|
|
describe('JUMP_TO_STATE...', () => {
|
|
const increment = () => {}
|
|
it('it updates state without recording with `message.state`', () => {
|
|
const initialState = { count: 0, increment }
|
|
const api = create(devtools(() => initialState))
|
|
const newState = { foo: 'bar' }
|
|
|
|
extension.send.mockClear()
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'JUMP_TO_STATE' },
|
|
state: JSON.stringify(newState),
|
|
})
|
|
expect(api.getState()).toStrictEqual({ ...initialState, ...newState })
|
|
expect(extension.send).not.toBeCalled()
|
|
})
|
|
|
|
it('does not throw for unparsable `message.state`', () => {
|
|
const initialState = { count: 0, increment: () => {} }
|
|
const api = create(devtools(() => initialState))
|
|
const originalConsoleError = console.error
|
|
console.error = jest.fn()
|
|
|
|
extension.send.mockClear()
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'JUMP_TO_STATE' },
|
|
state: 'foobar',
|
|
})
|
|
|
|
expect(console.error).toHaveBeenLastCalledWith(
|
|
'[zustand devtools middleware] Could not parse the received json',
|
|
(() => {
|
|
try {
|
|
JSON.parse('foobar')
|
|
} catch (e) {
|
|
return e
|
|
}
|
|
})()
|
|
)
|
|
expect(api.getState()).toBe(initialState)
|
|
expect(extension.send).not.toBeCalled()
|
|
|
|
console.error = originalConsoleError
|
|
})
|
|
})
|
|
|
|
describe('JUMP_TO_ACTION...', () => {
|
|
it('it updates state without recording with `message.state`', () => {
|
|
const initialState = { count: 0, increment: () => {} }
|
|
const api = create(devtools(() => initialState))
|
|
const newState = { foo: 'bar' }
|
|
|
|
extension.send.mockClear()
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'JUMP_TO_ACTION' },
|
|
state: JSON.stringify(newState),
|
|
})
|
|
expect(api.getState()).toStrictEqual({ ...initialState, ...newState })
|
|
expect(extension.send).not.toBeCalled()
|
|
})
|
|
|
|
it('does not throw for unparsable `message.state`', () => {
|
|
const increment = () => {}
|
|
const initialState = { count: 0, increment }
|
|
const api = create(devtools(() => initialState))
|
|
const originalConsoleError = console.error
|
|
console.error = jest.fn()
|
|
|
|
extension.send.mockClear()
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'JUMP_TO_ACTION' },
|
|
state: 'foobar',
|
|
})
|
|
|
|
expect(console.error).toHaveBeenLastCalledWith(
|
|
'[zustand devtools middleware] Could not parse the received json',
|
|
(() => {
|
|
try {
|
|
JSON.parse('foobar')
|
|
} catch (e) {
|
|
return e
|
|
}
|
|
})()
|
|
)
|
|
expect(api.getState()).toBe(initialState)
|
|
expect(extension.send).not.toBeCalled()
|
|
|
|
console.error = originalConsoleError
|
|
})
|
|
})
|
|
|
|
it('IMPORT_STATE, it updates state without recording and inits the last computedState', () => {
|
|
const initialState = { count: 0, increment: () => {} }
|
|
const api = create(devtools(() => initialState))
|
|
const nextLiftedState = {
|
|
computedStates: [{ state: { count: 4 } }, { state: { count: 5 } }],
|
|
}
|
|
|
|
extension.send.mockClear()
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: {
|
|
type: 'IMPORT_STATE',
|
|
nextLiftedState,
|
|
},
|
|
})
|
|
expect(api.getState()).toStrictEqual({
|
|
...initialState,
|
|
...nextLiftedState.computedStates.slice(-1)[0]?.state,
|
|
})
|
|
expect(extension.send).toHaveBeenLastCalledWith(null, nextLiftedState)
|
|
})
|
|
|
|
it('PAUSE_RECORDING, it toggles the sending of actions', () => {
|
|
const api = create(devtools(() => ({ count: 0 })))
|
|
|
|
api.setState({ count: 1 }, false, 'increment')
|
|
expect(extension.send).toHaveBeenLastCalledWith(
|
|
{ type: 'increment' },
|
|
{ count: 1 }
|
|
)
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'PAUSE_RECORDING' },
|
|
})
|
|
|
|
api.setState({ count: 2 }, false, 'increment')
|
|
expect(extension.send).toHaveBeenLastCalledWith(
|
|
{ type: 'increment' },
|
|
{ count: 1 }
|
|
)
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'PAUSE_RECORDING' },
|
|
})
|
|
|
|
api.setState({ count: 3 }, false, 'increment')
|
|
expect(extension.send).toHaveBeenLastCalledWith(
|
|
{ type: 'increment' },
|
|
{ count: 3 }
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('with redux middleware', () => {
|
|
let api: StoreApi<{
|
|
count: number
|
|
dispatch: (
|
|
action: { type: 'INCREMENT' } | { type: 'DECREMENT' }
|
|
) => { type: 'INCREMENT' } | { type: 'DECREMENT' }
|
|
}>
|
|
|
|
it('works as expected', () => {
|
|
api = create(
|
|
devtools(
|
|
redux(
|
|
(
|
|
{ count },
|
|
{ type }: { type: 'INCREMENT' } | { type: 'DECREMENT' }
|
|
) => ({
|
|
count: count + (type === 'INCREMENT' ? 1 : -1),
|
|
}),
|
|
{ count: 0 }
|
|
)
|
|
)
|
|
)
|
|
;(api as any).dispatch({ type: 'INCREMENT' })
|
|
;(api as any).dispatch({ type: 'INCREMENT' })
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'ACTION',
|
|
payload: JSON.stringify({ type: 'DECREMENT' }),
|
|
})
|
|
|
|
expect(extension.init.mock.calls).toMatchObject([[{ count: 0 }]])
|
|
expect(extension.send.mock.calls).toMatchObject([
|
|
[{ type: 'INCREMENT' }, { count: 1 }],
|
|
[{ type: 'INCREMENT' }, { count: 2 }],
|
|
[{ type: 'DECREMENT' }, { count: 1 }],
|
|
])
|
|
expect(api.getState()).toMatchObject({ count: 1 })
|
|
})
|
|
|
|
it('[DEV-ONLY] warns about misusage', () => {
|
|
const originalConsoleWarn = console.warn
|
|
console.warn = jest.fn()
|
|
;(api as any).dispatch({ type: '__setState' as any })
|
|
expect(console.warn).toHaveBeenLastCalledWith(
|
|
'[zustand devtools middleware] "__setState" action type is reserved ' +
|
|
'to set state from the devtools. Avoid using it.'
|
|
)
|
|
|
|
console.warn = originalConsoleWarn
|
|
})
|
|
})
|
|
|
|
it('works in non-browser env', () => {
|
|
const originalWindow = global.window
|
|
global.window = undefined as any
|
|
|
|
expect(() => {
|
|
create(devtools(() => ({ count: 0 })))
|
|
}).not.toThrow()
|
|
|
|
global.window = originalWindow
|
|
})
|
|
|
|
it('works in react native env', () => {
|
|
const originalWindow = global.window
|
|
global.window = {} as any
|
|
|
|
expect(() => {
|
|
create(devtools(() => ({ count: 0 })))
|
|
}).not.toThrow()
|
|
|
|
global.window = originalWindow
|
|
})
|
|
|
|
it('preserves isRecording after setting from devtools', () => {
|
|
const api = create(devtools(() => ({ count: 0 })))
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'DISPATCH',
|
|
payload: { type: 'PAUSE_RECORDING' },
|
|
})
|
|
;(extensionSubscriber as (message: any) => void)({
|
|
type: 'ACTION',
|
|
payload: '{ "type": "__setState", "state": { "foo": "bar" } }',
|
|
})
|
|
|
|
api.setState({ count: 1 })
|
|
expect(extension.send).not.toBeCalled()
|
|
})
|