diff --git a/README.md b/README.md index 97e2825..d421215 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ yarn add use-travel mutative ### API -You can use `useTravel` to create a time travel state. And it returns a tuple with the current state, the state setter, and the controls. The controls include `back`, `forward`, `reset`, `canBack`, `canForward`, `getHistory`, `patches`, `position`, and `go`. +You can use `useTravel` to create a time travel state. And it returns a tuple with the current state, the state setter, and the controls. The controls include `back`, `forward`, `reset`, `canBack`, `canForward`, `getHistory`, `patches`, `position`, `archive`, and `go`. ```jsx import { useTravel } from 'use-travel'; @@ -82,26 +82,24 @@ const App = () => { | ---------------- | ------------- | ------------------------------------- | -------------------------------- | | `maxHistory` | number | The maximum number of history to keep | 10 | | `initialPatches` | TravelPatches | The initial patches | {patches: [],inversePatches: []} | +| `autoArchive` | boolean | Auto archive the state | true | ### Return -| Return | type | description | -| --------------------- | -------------------------- | ------------------------------------------------------------------ | -| `state` | T | The current state | -| `setState` | Dispatch | The state setter, support mutation update or return immutable data | -| `controls.back` | () => void | Go back to the previous state | -| `controls.forward` | () => void | Go forward to the next state | -| `controls.reset` | () => void | Reset the state to the initial state | -| `controls.canBack` | () => boolean | Check if can go back to the previous state | -| `controls.canForward` | () => boolean | Check if can go forward to the next state | -| `controls.getHistory` | () => T[] | Get the history of the state | -| `controls.patches` | TravelPatches[] | Get the patches history of the state | -| `controls.position` | number | Get the current position of the state | -| `controls.go` | (position: number) => void | Go to the specific position of the state | - -### TODO - -- [ ] add `archive` functionality +| Return | type | description | +| --------------------- | -------------------------- | ---------------------------------------------------------------------- | +| `state` | T | The current state | +| `setState` | Dispatch | The state setter, support mutation update or return immutable data | +| `controls.back` | () => void | Go back to the previous state | +| `controls.forward` | () => void | Go forward to the next state | +| `controls.reset` | () => void | Reset the state to the initial state | +| `controls.canBack` | () => boolean | Check if can go back to the previous state | +| `controls.canForward` | () => boolean | Check if can go forward to the next state | +| `controls.getHistory` | () => T[] | Get the history of the state | +| `controls.patches` | TravelPatches[] | Get the patches history of the state | +| `controls.position` | number | Get the current position of the state | +| `controls.go` | (position: number) => void | Go to the specific position of the state | +| `controls.archive` | () => void | Archive the current state(the `autoArchive` options should be `false`) | ## License diff --git a/src/index.ts b/src/index.ts index a9abdaf..778fb69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,10 +14,24 @@ type TravelPatches = { inversePatches: Patches[]; }; -type Options = { +type Options = { + /** + * The maximum number of history to keep, by default `10` + */ maxHistory?: number; + /** + * The initial position in the history, by default `0` + */ + initialPosition?: number; + /** + * The initial patches of the history + */ initialPatches?: TravelPatches; -} & MutativeOptions; + /** + * Whether to automatically archive the current state, by default `true` + */ + autoArchive?: A; +} & Omit, 'enablePatches'>; type InitialValue = I extends (...args: any) => infer R ? R : I; type DraftFunction = (draft: Draft) => void; @@ -25,62 +39,81 @@ type Updater = (value: S | (() => S) | DraftFunction) => void; type Value = F extends true ? Immutable> : InitialValue; -type StateValue = - | InitialValue - | (() => InitialValue) - | DraftFunction>; -type Result = [ +interface Controls { + /** + * The current position in the history + */ + position: number; + /** + * Get the history of the state + */ + getHistory: () => Value[]; + /** + * The patches of the history + */ + patches: TravelPatches; + /** + * Go back in the history + */ + back: (amount?: number) => void; + /** + * Go forward in the history + */ + forward: (amount?: number) => void; + /** + * Reset the history + */ + reset: () => void; + /** + * Go to a specific position in the history + */ + go: (position: number) => void; + /** + * Check if it's possible to go back + */ + canBack: () => boolean; + /** + * Check if it's possible to go forward + */ + canForward: () => boolean; +} + +type Result = [ Value, Updater>, - { - /** - * The current position in the history - */ - position: number; - /** - * Get the history of the state - */ - getHistory: () => Value[]; - /** - * The patches of the history - */ - patches: TravelPatches; - /** - * Go back in the history - */ - back: (amount?: number) => void; - /** - * Go forward in the history - */ - forward: (amount?: number) => void; - /** - * Reset the history - */ - reset: () => void; - /** - * Go to a specific position in the history - */ - go: (position: number) => void; - /** - * Check if it's possible to go back - */ - canBack: () => boolean; - /** - * Check if it's possible to go forward - */ - canForward: () => boolean; - } + A extends false + ? Controls & { + /** + * Archive the current state + */ + archive: () => void; + } + : Controls ]; /** * A hook to travel in the history of a state */ -export const useTravel = ( +export const useTravel = ( initialState: S, - { maxHistory = 10, initialPatches, ...options }: Options = {} + _options: Options = {} ) => { - const [position, setPosition] = useState(0); + const { + maxHistory = 10, + initialPatches, + initialPosition = 0, + autoArchive = true, + ...options + } = _options; + const [position, setPosition] = useState(initialPosition); + const [tempPatches, setTempPatches] = useMutative( + () => + ({ + patches: [], + inversePatches: [], + } as TravelPatches) + ); const [allPatches, setAllPatches] = useMutative( () => (initialPatches ?? { @@ -97,38 +130,113 @@ export const useTravel = ( : create(state, () => updater, { ...options, enablePatches: true }) ) as [S, Patches, Patches]; setState(nextState); - setPosition(position + 1); - setAllPatches((_allPatches) => { - const notLast = position < _allPatches.patches.length; + if (autoArchive) { + setPosition( + maxHistory < allPatches.patches.length + 1 ? maxHistory : position + 1 + ); + setAllPatches((allPatchesDraft) => { + const notLast = position < allPatchesDraft.patches.length; + // Remove all patches after the current position + if (notLast) { + allPatchesDraft.patches.splice( + position, + allPatchesDraft.patches.length - position + ); + allPatchesDraft.inversePatches.splice( + position, + allPatchesDraft.inversePatches.length - position + ); + } + allPatchesDraft.patches.push(patches); + allPatchesDraft.inversePatches.push(inversePatches); + if (maxHistory < allPatchesDraft.patches.length) { + allPatchesDraft.patches = allPatchesDraft.patches.slice( + -maxHistory + ); + allPatchesDraft.inversePatches = + allPatchesDraft.inversePatches.slice(-maxHistory); + } + }); + } else { + const notLast = + position < + allPatches.patches.length + Number(!!tempPatches.patches.length); // Remove all patches after the current position if (notLast) { - _allPatches.patches.splice( + allPatches.patches.splice( position, - _allPatches.patches.length - position + allPatches.patches.length - position ); - _allPatches.inversePatches.splice( + allPatches.inversePatches.splice( position, - _allPatches.inversePatches.length - position + allPatches.inversePatches.length - position ); } - _allPatches.patches.push(patches); - _allPatches.inversePatches.push(inversePatches); - if (maxHistory < _allPatches.patches.length) { - _allPatches.patches = _allPatches.patches.slice(-maxHistory); - _allPatches.inversePatches = _allPatches.inversePatches.slice( - -maxHistory + if (!tempPatches.patches.length || notLast) { + setPosition( + maxHistory < allPatches.patches.length + 1 + ? maxHistory + : position + 1 ); } - }); + setTempPatches((tempPatchesDraft) => { + if (notLast) { + tempPatchesDraft.patches.length = 0; + tempPatchesDraft.inversePatches.length = 0; + } + tempPatchesDraft.patches.push(patches); + tempPatchesDraft.inversePatches.push(inversePatches); + }); + } }, - [state, position] + [state, position, setTempPatches] ); + const archive = () => { + if (autoArchive) { + console.warn('Auto archive is enabled, no need to archive manually'); + return; + } + if (!tempPatches.patches.length) return; + setAllPatches((allPatchesDraft) => { + allPatchesDraft.patches.push(tempPatches.patches.flat()); + allPatchesDraft.inversePatches.push(tempPatches.inversePatches.flat()); + if (maxHistory < allPatchesDraft.patches.length) { + allPatchesDraft.patches = allPatchesDraft.patches.slice(-maxHistory); + allPatchesDraft.inversePatches = allPatchesDraft.inversePatches.slice( + -maxHistory + ); + } + }); + setTempPatches((tempPatchesDraft) => { + tempPatchesDraft.patches.length = 0; + tempPatchesDraft.inversePatches.length = 0; + }); + }; + + const _allPatches = useMemo(() => { + const shouldArchive = !(autoArchive || !tempPatches.patches.length); + let mergedPatches = allPatches; + if (shouldArchive) { + mergedPatches = { + patches: allPatches.patches.concat(), + inversePatches: allPatches.inversePatches.concat(), + }; + mergedPatches.patches.push(tempPatches.patches.flat()); + mergedPatches.inversePatches.push(tempPatches.inversePatches.flat()); + } + return mergedPatches; + }, [allPatches, tempPatches]); + const cachedTravels = useMemo(() => { const go = (nextPosition: number) => { const back = nextPosition < position; - if (nextPosition > allPatches.patches.length) { + const shouldArchive = !(autoArchive || !tempPatches.patches.length); + if (shouldArchive) { + archive(); + } + if (nextPosition > _allPatches.patches.length) { console.warn(`Can't go forward to position ${nextPosition}`); - nextPosition = allPatches.patches.length; + nextPosition = _allPatches.patches.length; } if (nextPosition < 0) { console.warn(`Can't go back to position ${nextPosition}`); @@ -140,8 +248,21 @@ export const useTravel = ( apply( state as object, back - ? allPatches.inversePatches.slice(nextPosition).flat().reverse() - : allPatches.patches.slice(position, nextPosition).flat() + ? _allPatches.inversePatches + .slice( + _allPatches.inversePatches.length - maxHistory, + _allPatches.inversePatches.length + ) + .slice(nextPosition) + .flat() + .reverse() + : _allPatches.patches + .slice( + _allPatches.inversePatches.length - maxHistory, + _allPatches.inversePatches.length + ) + .slice(position, nextPosition) + .flat() ) as S ); setPosition(nextPosition); @@ -153,19 +274,23 @@ export const useTravel = ( if (cachedHistory) return cachedHistory; cachedHistory = [state]; let currentState = state as any; - for (let i = position; i < allPatches.patches.length; i++) { - currentState = apply( - currentState as object, - allPatches.patches[i] - ) as S; + const patches = + !autoArchive && _allPatches.patches.length > maxHistory + ? _allPatches.patches.slice(_allPatches.patches.length - maxHistory) + : _allPatches.patches; + const inversePatches = + !autoArchive && _allPatches.patches.length > maxHistory + ? _allPatches.inversePatches.slice( + _allPatches.inversePatches.length - maxHistory + ) + : _allPatches.inversePatches; + for (let i = position; i < patches.length; i++) { + currentState = apply(currentState as object, patches[i]) as S; cachedHistory.push(currentState); } currentState = state as any; for (let i = position - 1; i > -1; i--) { - currentState = apply( - currentState as object, - allPatches.inversePatches[i] - ) as S; + currentState = apply(currentState as object, inversePatches[i]) as S; cachedHistory.unshift(currentState); } return cachedHistory; @@ -178,15 +303,25 @@ export const useTravel = ( go(position + amount); }, reset: () => { - setPosition(0); + setPosition(initialPosition); setAllPatches( () => initialPatches ?? { patches: [], inversePatches: [] } ); setState(() => initialState); + setTempPatches(() => ({ patches: [], inversePatches: [] })); }, go, - canBack: () => position > 0, - canForward: () => position < allPatches.patches.length, + canBack: () => { + return position > 0; + }, + canForward: () => { + const shouldArchive = !(autoArchive || !tempPatches.patches.length); + if (shouldArchive) { + return position < _allPatches.patches.length - 1; + } + return position < allPatches.patches.length; + }, + archive, }; }, [ position, @@ -196,6 +331,8 @@ export const useTravel = ( setState, initialState, state, + tempPatches, + _allPatches, ]); - return [state, cachedSetState, cachedTravels] as Result; + return [state, cachedSetState, cachedTravels] as Result; }; diff --git a/test/index.test.ts b/test/index.test.ts index 2a83d0b..d18c97a 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -11,13 +11,15 @@ describe('useTravel', () => { expect(controls.getHistory()).toEqual([{ todos: [] }]); act(() => - setState((draft) => { - draft.todos.push({ - name: 'todo 1', - }); - draft.todos.push({ - name: 'todo 2', - }); + setState({ + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + ], }) ); [nextState, setState, controls] = result.current; @@ -410,5 +412,789 @@ describe('useTravel', () => { }); expect(fnWarning).toHaveBeenCalledWith(`Can't go back to position -1`); + + //@ts-expect-error + act(() => controls.archive()); + [nextState, setState, controls] = result.current; + expect(fnWarning).toHaveBeenCalledWith(`Auto archive is enabled, no need to archive manually`); + }); + + it('[useTravel] with normal init state and disable autoArchive', () => { + const { result } = renderHook(() => + useTravel({ todos: [] } as { todos: { name: string }[] }, { + autoArchive: false, + }) + ); + let [nextState, setState, controls] = result.current; + expect(nextState).toEqual({ todos: [] }); + expect(controls.getHistory()).toEqual([{ todos: [] }]); + + act(() => + setState((draft) => { + draft.todos.push({ + name: 'todo 1', + }); + draft.todos.push({ + name: 'todo 2', + }); + }) + ); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual({ + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + ], + }); + expect(controls.patches.patches.length).toBe(0); + expect(controls.position).toBe(1); + expect(controls.getHistory()).toEqual([ + { todos: [] }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + ], + }, + ]); + + act(() => + setState((draft) => { + draft.todos.push({ + name: 'todo 3', + }); + }) + ); + [nextState, setState, controls] = result.current; + + expect(controls.patches.patches.length).toBe(0); + expect(controls.position).toBe(1); + expect(controls.getHistory()).toEqual([ + { todos: [] }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }, + ]); + + act(() => controls.archive()); + [nextState, setState, controls] = result.current; + + expect(controls.getHistory()).toEqual([ + { todos: [] }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }, + ]); + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + + expect(nextState).toEqual({ + todos: [], + }); + + act(() => controls.forward()); + [nextState, setState, controls] = result.current; + + expect(nextState).toEqual({ + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }); + + act(() => controls.go(0)); + [nextState, setState, controls] = result.current; + + expect(nextState).toEqual({ + todos: [], + }); + + act(() => controls.go(1)); + [nextState, setState, controls] = result.current; + + expect(nextState).toEqual({ + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }); + + expect(controls.getHistory()).toEqual([ + { + todos: [], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }, + ]); + expect(controls.position).toBe(1); + + act(() => + setState((draft) => { + draft.todos.push({ + name: 'todo 4', + }); + }) + ); + [nextState, setState, controls] = result.current; + expect(controls.position).toBe(2); + expect(nextState).toEqual({ + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + { + name: 'todo 4', + }, + ], + }); + + // act(() => controls.archive()); + // [nextState, setState, controls] = result.current; + act(() => controls.back()); + [nextState, setState, controls] = result.current; + expect(controls.position).toBe(1); + + expect(nextState).toEqual({ + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }); + + expect(controls.getHistory()).toEqual([ + { + todos: [], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + { + name: 'todo 4', + }, + ], + }, + ]); + + act(() => controls.forward()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual({ + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + { + name: 'todo 4', + }, + ], + }); + expect(controls.getHistory()).toEqual([ + { + todos: [], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + { + name: 'todo 4', + }, + ], + }, + ]); + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + + expect(nextState).toEqual({ + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }); + + expect(controls.getHistory()).toEqual([ + { + todos: [], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + { + name: 'todo 4', + }, + ], + }, + ]); + + act(() => + setState((draft) => { + draft.todos.push({ + name: 'todo 5', + }); + }) + ); + [nextState, setState, controls] = result.current; + + expect(nextState).toEqual({ + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + { + name: 'todo 5', + }, + ], + }); + + expect(controls.getHistory()).toEqual([ + { + todos: [], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + { + name: 'todo 5', + }, + ], + }, + ]); + + act(() => controls.go(1)); + [nextState, setState, controls] = result.current; + + expect(nextState).toEqual({ + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }); + expect(controls.getHistory()).toEqual([ + { + todos: [], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + ], + }, + { + todos: [ + { + name: 'todo 1', + }, + { + name: 'todo 2', + }, + { + name: 'todo 3', + }, + { + name: 'todo 5', + }, + ], + }, + ]); + + act(() => controls.reset()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual({ todos: [] }); + expect(controls.getHistory()).toEqual([{ todos: [] }]); + }); + + it('[useTravel] maxHistory', () => { + const { result } = renderHook(() => + useTravel(0, { + maxHistory: 3, + }) + ); + let [nextState, setState, controls] = result.current; + expect(nextState).toEqual(0); + expect(controls.position).toEqual(0); + expect(controls.getHistory()).toEqual([0]); + + act(() => setState(() => 1)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(1); + expect(controls.position).toEqual(1); + expect(controls.getHistory()).toEqual([0, 1]); + + act(() => setState(2)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(2); + expect(controls.position).toEqual(2); + expect(controls.getHistory()).toEqual([0, 1, 2]); + + act(() => setState(3)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(3); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([0, 1, 2, 3]); + + act(() => setState(4)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(4); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([1, 2, 3, 4]); + + act(() => setState(5)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(5); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(false); + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(4); + expect(controls.position).toEqual(2); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(3); + expect(controls.position).toEqual(1); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(2); + expect(controls.position).toEqual(0); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(false); + expect(controls.canForward()).toBe(true); + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(2); + expect(controls.position).toEqual(0); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(false); + expect(controls.canForward()).toBe(true); + + act(() => controls.forward()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(3); + expect(controls.position).toEqual(1); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); + + act(() => controls.forward()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(4); + expect(controls.position).toEqual(2); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); + + act(() => controls.forward()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(5); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(false); + + act(() => controls.forward()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(5); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(false); + }); + + it('[useTravel] maxHistory with autoArchive: false', () => { + let { result } = renderHook(() => + useTravel(0, { + maxHistory: 3, + autoArchive: false, + }) + ); + let [nextState, setState, controls] = result.current; + expect(nextState).toEqual(0); + expect(controls.position).toEqual(0); + expect(controls.getHistory()).toEqual([0]); + + act(() => setState(() => 1)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(1); + expect(controls.position).toEqual(1); + expect(controls.getHistory()).toEqual([0, 1]); + + act(() => controls.archive()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(1); + expect(controls.position).toEqual(1); + expect(controls.getHistory()).toEqual([0, 1]); + + act(() => setState(2)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(2); + expect(controls.position).toEqual(2); + expect(controls.getHistory()).toEqual([0, 1, 2]); + + + act(() => controls.archive()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(2); + expect(controls.position).toEqual(2); + expect(controls.getHistory()).toEqual([0, 1, 2]); + + act(() => setState(3)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(3); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([0, 1, 2, 3]); + + + act(() => controls.archive()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(3); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([0, 1, 2, 3]); + + act(() => setState(4)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(4); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([1, 2, 3, 4]); + + act(() => controls.archive()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(4); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([1, 2, 3, 4]); + + act(() => setState(5)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(5); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(false); + + act(() => controls.archive()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(5); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(false); + + act(() => controls.archive()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(5); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([2, 3, 4, 5]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(false); + + act(() => setState(6)); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(6); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(false); + + act(() => controls.archive()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(6); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(false); + (global as any).x = true; + act(() => controls.back()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(5); + expect(controls.position).toEqual(2); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); + // return; + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(4); + expect(controls.position).toEqual(1); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(3); + expect(controls.position).toEqual(0); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(false); + expect(controls.canForward()).toBe(true); + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(3); + expect(controls.position).toEqual(0); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(false); + expect(controls.canForward()).toBe(true); + + act(() => controls.forward()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(4); + expect(controls.position).toEqual(1); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); + + act(() => controls.forward()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(5); + expect(controls.position).toEqual(2); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); + + act(() => controls.forward()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(6); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(false); + + act(() => controls.forward()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(6); + expect(controls.position).toEqual(3); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(false); + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + expect(nextState).toEqual(5); + expect(controls.position).toEqual(2); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); + + result = renderHook(() => + useTravel(nextState, { + maxHistory: 3, + autoArchive: false, + initialPatches: controls.patches, + initialPosition: controls.position, + }) + ).result; + [nextState, setState, controls] = result.current; + + act(() => controls.back()); + [nextState, setState, controls] = result.current; + + expect(nextState).toEqual(4); + expect(controls.position).toEqual(1); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); + + act(() => controls.reset()); + [nextState, setState, controls] = result.current; + + expect(nextState).toEqual(5); + expect(controls.position).toEqual(2); + expect(controls.getHistory()).toEqual([3, 4, 5, 6]); + expect(controls.canBack()).toBe(true); + expect(controls.canForward()).toBe(true); }); });