From 055262cc0af2bbc5ecfae9fcc85f961c0b97a024 Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Sat, 27 May 2023 10:56:45 -0400 Subject: [PATCH 1/4] fix async test --- __tests__/options.test.ts | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/__tests__/options.test.ts b/__tests__/options.test.ts index 47ab076..97abb03 100644 --- a/__tests__/options.test.ts +++ b/__tests__/options.test.ts @@ -414,13 +414,14 @@ describe('Middleware options', () => { expect(console.info).toHaveBeenCalledTimes(2); }); - it('should correctly use throttling', () => { + it.only('should correctly use throttling', () => { global.console.error = vi.fn(); vi.useFakeTimers(); const storeWithHandleSet = createVanillaStore({ handleSet: (handleSet) => { return throttle((state) => { console.error('handleSet called'); + console.log('handleSet called'); handleSet(state); }, 1000); }, @@ -428,25 +429,38 @@ describe('Middleware options', () => { const { doNothing, increment } = storeWithHandleSet.getState(); act(() => { increment(); + increment(); + increment(); + increment(); }); - vi.runAllTimers(); expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(1); expect(console.error).toHaveBeenCalledTimes(1); + vi.advanceTimersByTime(1001); + // By default, lodash.throttle includes trailing event + expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(2); + expect(console.error).toHaveBeenCalledTimes(2); act(() => { doNothing(); + doNothing(); + doNothing(); + doNothing(); }); - vi.runAllTimers(); - expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(2); - expect(console.error).toHaveBeenCalledTimes(2); + expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(3); + expect(console.error).toHaveBeenCalledTimes(3); + vi.advanceTimersByTime(1001); + expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(4); + expect(console.error).toHaveBeenCalledTimes(4); act(() => { - storeWithHandleSet.temporal.getState().undo(2); + // Does not call handle set (and is not throttled) + storeWithHandleSet.temporal.getState().undo(4); + storeWithHandleSet.temporal.getState().redo(1); }); - vi.runAllTimers(); - expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(0); + expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(1); expect(storeWithHandleSet.temporal.getState().futureStates.length).toBe( - 2, + 3, ); - expect(console.warn).toHaveBeenCalledTimes(2); + expect(console.error).toHaveBeenCalledTimes(4); + vi.useRealTimers(); }); }); From 04e5621e017d9fbbe8030a265f4f84fe38cda4ad Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Sat, 27 May 2023 11:06:57 -0400 Subject: [PATCH 2/4] add setState tests --- README.md | 1 - __tests__/options.test.ts | 20 +++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aea84a0..f222c6c 100644 --- a/README.md +++ b/README.md @@ -401,7 +401,6 @@ This is a work in progress. Submit a PR! ## Road Map - [ ] create nicer API, or a helper hook in react land (useTemporal). or vanilla version of the it -- [ ] create a `present` object that holds the current state? perhaps - [ ] support history branches rather than clearing the future states - [ ] store state delta rather than full object - [ ] track state for multiple stores at once diff --git a/__tests__/options.test.ts b/__tests__/options.test.ts index 97abb03..29e0fcf 100644 --- a/__tests__/options.test.ts +++ b/__tests__/options.test.ts @@ -414,7 +414,7 @@ describe('Middleware options', () => { expect(console.info).toHaveBeenCalledTimes(2); }); - it.only('should correctly use throttling', () => { + it('should correctly use throttling', () => { global.console.error = vi.fn(); vi.useFakeTimers(); const storeWithHandleSet = createVanillaStore({ @@ -610,3 +610,21 @@ describe('Middleware options', () => { }); }); }); + +describe('setState', () => { + it('it should correctly update the state', () => { + const store = createVanillaStore(); + const setState = store.setState; + act(() => { + setState({ count: 100 }); + }); + expect(store.getState().count).toBe(100); + expect(store.temporal.getState().pastStates.length).toBe(1); + act(() => { + store.temporal.getState().undo(); + }); + expect(store.getState().count).toBe(0); + expect(store.temporal.getState().pastStates.length).toBe(0); + expect(store.temporal.getState().futureStates.length).toBe(1); + }); +}); From 32b2eeb5e00d00bfa300d2403ffeac45dd1fea07 Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Sat, 27 May 2023 11:14:17 -0400 Subject: [PATCH 3/4] add more examples --- __tests__/options.test.ts | 2 +- examples/web/package.json | 1 + examples/web/pages/throttle.tsx | 120 ++++++++++++++++++++++++++++++++ examples/web/pages/wrapped.tsx | 119 +++++++++++++++++++++++++++++++ pnpm-lock.yaml | 7 ++ 5 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 examples/web/pages/throttle.tsx create mode 100644 examples/web/pages/wrapped.tsx diff --git a/__tests__/options.test.ts b/__tests__/options.test.ts index 29e0fcf..35c62f0 100644 --- a/__tests__/options.test.ts +++ b/__tests__/options.test.ts @@ -10,7 +10,7 @@ import type { TemporalState, Write, } from '../src/types'; -import throttle from '../node_modules/lodash.throttle'; +import throttle from 'lodash.throttle'; interface MyState { count: number; diff --git a/examples/web/package.json b/examples/web/package.json index 4330cb6..8aa4221 100644 --- a/examples/web/package.json +++ b/examples/web/package.json @@ -9,6 +9,7 @@ "start": "next start" }, "dependencies": { + "just-throttle": "4.2.0", "lodash.merge": "4.6.2", "lodash.throttle": "4.1.1", "next": "13.4.3", diff --git a/examples/web/pages/throttle.tsx b/examples/web/pages/throttle.tsx new file mode 100644 index 0000000..99d44b1 --- /dev/null +++ b/examples/web/pages/throttle.tsx @@ -0,0 +1,120 @@ +import { temporal } from 'zundo'; +import { create } from 'zustand'; +import throttle from 'just-throttle'; + +interface MyState { + bears: number; + bees: number; + increment: () => void; + decrement: () => void; + incrementBees: () => void; + decrementBees: () => void; +} + +const useMyStore = create( + temporal( + (set) => ({ + bears: 0, + bees: 10, + increment: () => set((state) => ({ bears: state.bears + 1 })), + decrement: () => set((state) => ({ bears: state.bears - 1 })), + incrementBees: () => set((state) => ({ bees: state.bees + 1 })), + decrementBees: () => set((state) => ({ bees: state.bees - 1 })), + }), + { + wrapTemporal: (config) => { + const thing: typeof config = (_set, get, store) => { + const set: typeof _set = throttle((partial, replace) => { + console.info('handleSet called'); + console.log( + 'calling wrapped setter', + JSON.stringify(partial, null, 2), + ); + _set(partial, replace); + }, 1000); + return config(set, get, store); + }; + return thing; + }, + }, + ), +); +const useTemporalStore = create(useMyStore.temporal); + +const UndoBar = () => { + const { undo, redo, futureStates, pastStates } = useTemporalStore(); + return ( +
+ past states: {JSON.stringify(pastStates)} +
+ future states: {JSON.stringify(futureStates)} +
+ + +
+ ); +}; + +const StateBear = () => { + const store = useMyStore((state) => ({ + bears: state.bears, + increment: state.increment, + decrement: state.decrement, + })); + const { bears, increment, decrement } = store; + return ( +
+ current state: {JSON.stringify(store)} +
+
+ bears: {bears} +
+ + +
+ ); +}; + +const StateBee = () => { + const store = useMyStore(); + console.log(store); + const { bees, increment, decrement } = store; + return ( +
+ current state: {JSON.stringify(store)} +
+
+ bees: {bees} +
+ + +
+ ); +}; + +const App = () => { + return ( +
+

+ {' '} + + 🐻 + {' '} + + ♻️ + {' '} + Zundo! +

+ + +
+ +
+ ); +}; + +export default App; diff --git a/examples/web/pages/wrapped.tsx b/examples/web/pages/wrapped.tsx new file mode 100644 index 0000000..62371d2 --- /dev/null +++ b/examples/web/pages/wrapped.tsx @@ -0,0 +1,119 @@ +import { temporal } from 'zundo'; +import create from 'zustand'; + +interface MyState { + bears: number; + bees: number; + increment: () => void; + decrement: () => void; + incrementBees: () => void; + decrementBees: () => void; +} + +const useStore = create( + temporal( + (set) => ({ + bears: 0, + bees: 10, + increment: () => set((state) => ({ bears: state.bears + 1 })), + decrement: () => set((state) => ({ bears: state.bears - 1 })), + incrementBees: () => set((state) => ({ bees: state.bees + 1 })), + decrementBees: () => set((state) => ({ bees: state.bees - 1 })), + }), + { + wrapTemporal: (config) => { + const thing: typeof config = (_set, get, store) => { + const set: typeof _set = (partial, replace) => { + console.info('handleSet called'); + console.log( + 'calling wrapped setter', + JSON.stringify(partial, null, 2), + ); + _set(partial, replace); + }; + return config(set, get, store); + }; + return thing; + }, + }, + ), +); +const useTemporalStore = create(useStore.temporal); + +const UndoBar = () => { + const { undo, redo, futureStates, pastStates } = useTemporalStore(); + return ( +
+ past states: {JSON.stringify(pastStates)} +
+ future states: {JSON.stringify(futureStates)} +
+ + +
+ ); +}; + +const StateBear = () => { + const store = useStore((state) => ({ + bears: state.bears, + increment: state.increment, + decrement: state.decrement, + })); + const { bears, increment, decrement } = store; + return ( +
+ current state: {JSON.stringify(store)} +
+
+ bears: {bears} +
+ + +
+ ); +}; + +const StateBee = () => { + const store = useStore(); + console.log(store); + const { bees, increment, decrement } = store; + return ( +
+ current state: {JSON.stringify(store)} +
+
+ bees: {bees} +
+ + +
+ ); +}; + +const App = () => { + return ( +
+

+ {' '} + + 🐻 + {' '} + + ♻️ + {' '} + Zundo! +

+ + +
+ +
+ ); +}; + +export default App; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19bfd3a..7009915 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,9 @@ importers: examples/web: dependencies: + just-throttle: + specifier: 4.2.0 + version: 4.2.0 lodash.merge: specifier: 4.6.2 version: 4.6.2 @@ -1980,6 +1983,10 @@ packages: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} dev: true + /just-throttle@4.2.0: + resolution: {integrity: sha512-/iAZv1953JcExpvsywaPKjSzfTiCLqeguUTE6+VmK15mOcwxBx7/FHrVvS4WEErMR03TRazH8kcBSHqMagYIYg==} + dev: false + /levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} From 286469df53d827dc64933bcfee211af2852c32b7 Mon Sep 17 00:00:00 2001 From: Charles Kornoelje <33156025+charkour@users.noreply.github.com> Date: Sat, 27 May 2023 11:28:44 -0400 Subject: [PATCH 4/4] add more wrapTemporal tests --- __tests__/options.test.ts | 120 +++++++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/__tests__/options.test.ts b/__tests__/options.test.ts index 35c62f0..f9a787b 100644 --- a/__tests__/options.test.ts +++ b/__tests__/options.test.ts @@ -11,6 +11,7 @@ import type { Write, } from '../src/types'; import throttle from 'lodash.throttle'; +import { persist } from 'zustand/middleware'; interface MyState { count: number; @@ -402,6 +403,12 @@ describe('Middleware options', () => { increment(); doNothing(); }); + expect(storeWithHandleSet.temporal.getState().pastStates[0]).toContain({ + count: 0, + }); + expect(storeWithHandleSet.temporal.getState().pastStates[1]).toContain({ + count: 1, + }); expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(2); expect(console.info).toHaveBeenCalledTimes(2); act(() => { @@ -414,6 +421,43 @@ describe('Middleware options', () => { expect(console.info).toHaveBeenCalledTimes(2); }); + it('should call function if set (wrapTemporal)', () => { + global.console.info = vi.fn(); + const storeWithHandleSet = createVanillaStore({ + wrapTemporal: (config) => { + return (_set, get, store) => { + const set: typeof _set = (partial, replace) => { + console.info('handleSet called'); + _set(partial, replace); + }; + return config(set, get, store); + }; + }, + }); + const { doNothing, increment } = storeWithHandleSet.getState(); + act(() => { + increment(); + doNothing(); + }); + expect(storeWithHandleSet.temporal.getState().pastStates[0]).toContain({ + count: 0, + }); + expect(storeWithHandleSet.temporal.getState().pastStates[1]).toContain({ + count: 1, + }); + expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(2); + expect(console.info).toHaveBeenCalledTimes(2); + act(() => { + storeWithHandleSet.temporal.getState().undo(2); + }); + expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(0); + expect(storeWithHandleSet.temporal.getState().futureStates.length).toBe( + 2, + ); + // Note: in the above test, the handleSet function is called twice, but in this test it is called 3 times because it is also called when undo() and redo() are called. + expect(console.info).toHaveBeenCalledTimes(3); + }); + it('should correctly use throttling', () => { global.console.error = vi.fn(); vi.useFakeTimers(); @@ -421,7 +465,6 @@ describe('Middleware options', () => { handleSet: (handleSet) => { return throttle((state) => { console.error('handleSet called'); - console.log('handleSet called'); handleSet(state); }, 1000); }, @@ -462,6 +505,79 @@ describe('Middleware options', () => { expect(console.error).toHaveBeenCalledTimes(4); vi.useRealTimers(); }); + it('should correctly use throttling (wrapTemporal)', () => { + global.console.error = vi.fn(); + vi.useFakeTimers(); + const storeWithHandleSet = createVanillaStore({ + wrapTemporal: (config) => { + return (_set, get, store) => { + const set: typeof _set = throttle( + (partial, replace) => { + console.error('handleSet called'); + _set(partial, replace); + }, + 1000, + ); + return config(set, get, store); + }; + }, + }); + const { doNothing, increment } = storeWithHandleSet.getState(); + act(() => { + increment(); + }); + vi.runAllTimers(); + expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(1); + expect(console.error).toHaveBeenCalledTimes(1); + act(() => { + doNothing(); + }); + vi.runAllTimers(); + expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(2); + expect(console.error).toHaveBeenCalledTimes(2); + act(() => { + storeWithHandleSet.temporal.getState().undo(2); + }); + vi.runAllTimers(); + expect(storeWithHandleSet.temporal.getState().pastStates.length).toBe(0); + expect(storeWithHandleSet.temporal.getState().futureStates.length).toBe( + 2, + ); + expect(console.warn).toHaveBeenCalledTimes(2); + }); + }); + + describe('wrapTemporal', () => { + describe('should wrap temporal store in given middlewares', () => { + it('persist', () => { + const storeWithTemporalWithPersist = createVanillaStore({ + wrapTemporal: (config) => persist(config, { name: '123' }), + }); + + expect(storeWithTemporalWithPersist.temporal).toHaveProperty('persist'); + }); + + it('temporal', () => { + const storeWithTemporalWithTemporal = createVanillaStore({ + wrapTemporal: (store) => temporal(store), + }); + expect(storeWithTemporalWithTemporal.temporal).toHaveProperty( + 'temporal', + ); + }); + + it('temporal and persist', () => { + const storeWithTemporalWithMiddleware = createVanillaStore({ + wrapTemporal: (store) => temporal(persist(store, { name: '123' })), + }); + expect(storeWithTemporalWithMiddleware.temporal).toHaveProperty( + 'persist', + ); + expect(storeWithTemporalWithMiddleware.temporal).toHaveProperty( + 'temporal', + ); + }); + }); }); describe('secret internals', () => { @@ -502,7 +618,7 @@ describe('Middleware options', () => { _onSave(storeWithOnSave.getState(), storeWithOnSave.getState()); }); expect(storeWithOnSave.temporal.getState().pastStates.length).toBe(0); - expect(console.error).toHaveBeenCalledTimes(1); + expect(console.info).toHaveBeenCalledTimes(1); }); it('should call onSave cb without adding a new state and respond to new setOnSave', () => { global.console.dir = vi.fn();