Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

restructure __internals to shave bytes; add react tests #82

Merged
merged 12 commits into from
Apr 22, 2023
97 changes: 97 additions & 0 deletions apps/web/pages/reactive.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { TemporalState, temporal } from 'zundo';
import { StoreApi, useStore, create } from 'zustand';

interface MyState {
bears: number;
increment: () => void;
decrement: () => void;
}

const useMyStore = create(
temporal<MyState>((set) => ({
bears: 0,
increment: () => set((state) => ({ bears: state.bears + 1 })),
decrement: () => set((state) => ({ bears: state.bears - 1 })),
})),
);

type ExtractState<S> = S extends {
getState: () => infer T;
} ? T : never;
type ReadonlyStoreApi<T> = Pick<StoreApi<T>, 'getState' | 'subscribe'>;
type WithReact<S extends ReadonlyStoreApi<unknown>> = S & {
getServerState?: () => ExtractState<S>;
};

const useTemporalStore = <S extends WithReact<StoreApi<TemporalState<MyState>>>, U>(
selector: (state: ExtractState<S>) => U,
equality?: (a: U, b: U) => boolean,
): U => {
const state = useStore(useMyStore.temporal as any, selector, equality);
return state
}

const HistoryBar = () => {
const futureStates = useTemporalStore((state) => state.futureStates);
const pastStates = useTemporalStore((state) => state.pastStates);
return (
<div>
past states: {JSON.stringify(pastStates)}
<br />
future states: {JSON.stringify(futureStates)}
<br />
</div>
);
};

const UndoBar = () => {
const { undo, redo } = useTemporalStore((state) => ({
undo: state.undo,
redo: state.redo,
}));
return (
<div>
<button onClick={() => undo()}>undo</button>
<button onClick={() => redo()}>redo</button>
</div>
);
};

const StateBar = () => {
const store = useMyStore();
const { bears, increment, decrement } = store;
return (
<div>
current state: {JSON.stringify(store)}
<br />
<br />
bears: {bears}
<br />
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
</div>
);
};

const App = () => {
return (
<div>
<h1>
{' '}
<span role="img" aria-label="bear">
🐻
</span>{' '}
<span role="img" aria-label="recycle">
♻️
</span>{' '}
Zundo!
</h1>
<StateBar />
<br />
<UndoBar />
<HistoryBar />
</div>
);
};

export default App;
5 changes: 4 additions & 1 deletion apps/web/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"extends": "tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
"compilerOptions": {
"jsx": "react-jsx"
}
}
2 changes: 1 addition & 1 deletion packages/zundo/__tests__/createVanillaTemporal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('createVanillaTemporal', () => {
};
});

const temporalStore = createVanillaTemporal(store.setState, store.getState);
const temporalStore = createVanillaTemporal(store.setState, store.getState, (state) => state);
const { undo, redo, clear, pastStates, futureStates } =
temporalStore.getState();
it('should have the objects defined', () => {
Expand Down
49 changes: 22 additions & 27 deletions packages/zundo/__tests__/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ describe('Middleware options', () => {

it('should call a new onSave function after being set', () => {
global.console.info = vi.fn();
global.console.log = vi.fn();
global.console.warn = vi.fn();
global.console.error = vi.fn();
const storeWithOnSave = createVanillaStore({
onSave: (pastStates) => {
Expand All @@ -325,11 +325,11 @@ describe('Middleware options', () => {
});
expect(storeWithOnSave.temporal.getState().pastStates.length).toBe(2);
expect(console.info).toHaveBeenCalledTimes(2);
expect(console.log).toHaveBeenCalledTimes(0);
expect(console.warn).toHaveBeenCalledTimes(0);
expect(console.error).toHaveBeenCalledTimes(0);
act(() => {
setOnSave((pastStates, currentState) => {
console.log(pastStates, currentState);
console.warn(pastStates, currentState);
});
});
act(() => {
Expand All @@ -338,7 +338,7 @@ describe('Middleware options', () => {
});
expect(storeWithOnSave.temporal.getState().pastStates.length).toBe(4);
expect(console.info).toHaveBeenCalledTimes(2);
expect(console.log).toHaveBeenCalledTimes(2);
expect(console.warn).toHaveBeenCalledTimes(2);
expect(console.error).toHaveBeenCalledTimes(0);
act(() => {
setOnSave((pastStates, currentState) => {
Expand All @@ -351,7 +351,7 @@ describe('Middleware options', () => {
});
expect(storeWithOnSave.temporal.getState().pastStates.length).toBe(6);
expect(console.info).toHaveBeenCalledTimes(2);
expect(console.log).toHaveBeenCalledTimes(2);
expect(console.warn).toHaveBeenCalledTimes(2);
expect(console.error).toHaveBeenCalledTimes(2);
});
});
Expand Down Expand Up @@ -425,17 +425,16 @@ describe('Middleware options', () => {
expect(storeWithHandleSet.temporal.getState().futureStates.length).toBe(
2,
);
expect(console.log).toHaveBeenCalledTimes(2);
expect(console.warn).toHaveBeenCalledTimes(2);
});
});

describe('secret internals', () => {
it('should have a secret internal state', () => {
const { __internal } =
const { __handleUserSet, __onSave } =
store.temporal.getState() as TemporalStateWithInternals<MyState>;
expect(__internal).toBeDefined();
expect(__internal.handleUserSet).toBeInstanceOf(Function);
expect(__internal.onSave).toBe(undefined);
expect(__handleUserSet).toBeInstanceOf(Function);
expect(__onSave).toBe(undefined);
});
describe('onSave', () => {
it('should call onSave cb without adding a new state when onSave is set by user', () => {
Expand All @@ -446,13 +445,12 @@ describe('Middleware options', () => {
console.error(pastStates, currentState);
});
});
const { __internal } =
const { __onSave } =
store.temporal.getState() as TemporalStateWithInternals<MyState>;
const { onSave } = __internal;
act(() => {
onSave(store.getState(), store.getState());
__onSave(store.getState(), store.getState());
});
expect(__internal.onSave).toBeInstanceOf(Function);
expect(__onSave).toBeInstanceOf(Function);
expect(store.temporal.getState().pastStates.length).toBe(0);
expect(console.error).toHaveBeenCalledTimes(1);
});
Expand All @@ -463,11 +461,10 @@ describe('Middleware options', () => {
console.info(pastStates);
},
});
const { __internal } =
const { __onSave } =
storeWithOnSave.temporal.getState() as TemporalStateWithInternals<MyState>;
const { onSave } = __internal;
act(() => {
onSave(storeWithOnSave.getState(), storeWithOnSave.getState());
__onSave(storeWithOnSave.getState(), storeWithOnSave.getState());
});
expect(storeWithOnSave.temporal.getState().pastStates.length).toBe(0);
expect(console.error).toHaveBeenCalledTimes(1);
Expand All @@ -483,7 +480,7 @@ describe('Middleware options', () => {
act(() => {
(
storeWithOnSave.temporal.getState() as TemporalStateWithInternals<MyState>
).__internal.onSave(
).__onSave(
storeWithOnSave.getState(),
storeWithOnSave.getState(),
);
Expand All @@ -501,7 +498,7 @@ describe('Middleware options', () => {
act(() => {
(
storeWithOnSave.temporal.getState() as TemporalStateWithInternals<MyState>
).__internal.onSave(store.getState(), store.getState());
).__onSave(store.getState(), store.getState());
});
expect(store.temporal.getState().pastStates.length).toBe(0);
expect(console.dir).toHaveBeenCalledTimes(1);
Expand All @@ -511,31 +508,29 @@ describe('Middleware options', () => {

describe('handleUserSet', () => {
it('should update the temporal store with the pastState when called', () => {
const { __internal } =
const { __handleUserSet } =
store.temporal.getState() as TemporalStateWithInternals<MyState>;
const { handleUserSet } = __internal;
act(() => {
handleUserSet(store.getState());
__handleUserSet(store.getState());
});
expect(store.temporal.getState().pastStates.length).toBe(1);
});

it('should only update if the the status is tracking', () => {
const { __internal } =
const { __handleUserSet } =
store.temporal.getState() as TemporalStateWithInternals<MyState>;
const { handleUserSet } = __internal;
act(() => {
handleUserSet(store.getState());
__handleUserSet(store.getState());
});
expect(store.temporal.getState().pastStates.length).toBe(1);
act(() => {
store.temporal.getState().pause();
handleUserSet(store.getState());
__handleUserSet(store.getState());
});
expect(store.temporal.getState().pastStates.length).toBe(1);
act(() => {
store.temporal.getState().resume();
handleUserSet(store.getState());
__handleUserSet(store.getState());
});
});

Expand Down
40 changes: 40 additions & 0 deletions packages/zundo/__tests__/react.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest';
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import Reactive from '../../../apps/web/pages/reactive';

describe('React Re-renders when state changes', () => {
it('it', () => {
const { queryByLabelText, getByLabelText, queryByText, getByText } = render(
<Reactive />,
);

expect(queryByText(/bears: 0/i)).toBeTruthy();
expect(queryByText(/increment/i)).toBeTruthy();
expect(queryByText(/past states: \[\]/i)).toBeTruthy();
expect(queryByText(/future states: \[\]/i)).toBeTruthy();

const incrementButton = getByText(/increment/i);
fireEvent.click(incrementButton);
fireEvent.click(incrementButton);

expect(queryByText(/bears: 2/i)).toBeTruthy();
expect(queryByText(/past states: \[{"bears":0},{"bears":1}\]/i)).toBeTruthy();
expect(queryByText(/future states: \[\]/i)).toBeTruthy();

expect(queryByText(/undo/i, {
selector: 'button',
})).toBeTruthy();

const undoButton = getByText(/undo/i, {
selector: 'button',
});

fireEvent.click(undoButton);
fireEvent.click(undoButton);

expect(queryByText(/bears: 0/i)).toBeTruthy();
expect(queryByText(/past states: \[\]/i)).toBeTruthy();
expect(queryByText(/future states: \[{"bears":2},{"bears":1}\]/i)).toBeTruthy();
});
});
11 changes: 11 additions & 0 deletions packages/zundo/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import matchers from '@testing-library/jest-dom/matchers';

// extends Vitest's expect method with methods from react-testing-library
expect.extend(matchers);

// runs a cleanup after each test case (e.g. clearing jsdom)
afterEach(() => {
cleanup();
});
5 changes: 5 additions & 0 deletions packages/zundo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,20 @@
},
"devDependencies": {
"@size-limit/preset-small-lib": "8.2.4",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "14.0.0",
"@types/lodash.throttle": "4.1.7",
"@types/react-dom": "18.0.11",
"jsdom": "21.1.1",
"lodash.throttle": "4.1.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-test-renderer": "18.2.0",
"size-limit": "8.2.4",
"tsconfig": "workspace:*",
"tsup": "6.7.0",
"typescript": "5.0.4",
"vite": "4.2.1",
"vitest": "0.30.1",
"zustand": "4.3.7"
},
Expand Down
9 changes: 3 additions & 6 deletions packages/zundo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,20 @@ const zundoImpl =
type TState = ReturnType<typeof config>;
type StoreAddition = StoreApi<TemporalState<TState>>;

const temporalStore = createVanillaTemporal<TState>(set, get, {
partialize,
...restOptions,
});
const temporalStore = createVanillaTemporal<TState>(set, get, partialize, restOptions);

const store = _store as Mutate<
StoreApi<TState>,
[['temporal', StoreAddition]]
>;
const { setState } = store;
const setState = store.setState;

// TODO: should temporal be only temporalStore.getState()?
// We can hide the rest of the store in the secret internals.
store.temporal = temporalStore;

const curriedUserLandSet = userlandSetFactory(
temporalStore.getState().__internal.handleUserSet,
temporalStore.getState().__handleUserSet,
);

const modifiedSetState: typeof setState = (state, replace) => {
Expand Down
Loading