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

Create test suite for core functionality #3

Merged
merged 2 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions __tests__/01_basic.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { create } from 'zustand';
import { createSlice, withSlices } from '../src/index';

describe('basic spec', () => {
it('should export functions', () => {
expect(createSlice).toBeDefined();
expect(withSlices).toBeDefined();
});
describe('createSlice', () => {
it('should return a slice config', () => {
const slice = createSlice({
name: 'counter',
value: 0,
actions: {
increment: () => (prev) => prev + 1,
},
});
// returns the input
expect(createSlice(slice)).toBe(slice);
});
});
describe('withSlices', () => {
it('should combine slices and nest state', () => {
const countSlice = createSlice({
name: 'count',
value: 0,
actions: {
inc: () => (prev) => prev + 1,
reset: () => () => 0,
},
});

const textSlice = createSlice({
name: 'text',
value: 'Hello',
actions: {
updateText: (newText: string) => () => newText,
reset: () => () => 'Hello',
},
});

const combinedConfig = withSlices(countSlice, textSlice);

expect(combinedConfig).toBeInstanceOf(Function);

const store = create(combinedConfig);

const state = store.getState();

expect(state.count).toBe(countSlice.value);
expect(state.text).toBe(textSlice.value);

expect(state.inc).toBeInstanceOf(Function);
expect(state.reset).toBeInstanceOf(Function);
expect(state.updateText).toBeInstanceOf(Function);
});
});
});
8 changes: 0 additions & 8 deletions __tests__/01_basic_spec.tsx

This file was deleted.

File renamed without changes.
153 changes: 153 additions & 0 deletions __tests__/03_component.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { ReactNode, createContext, useContext, useState } from 'react';
import { StoreApi, create, useStore as useZustandStore } from 'zustand';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render, RenderOptions } from '@testing-library/react';
// eslint-disable-next-line import/no-extraneous-dependencies
import userEvent from '@testing-library/user-event';
import { createSlice, withSlices } from '../src/index';

type ExtractState<S> = S extends {
getState: () => infer State;
}
? State
: never;

function createZustandContext<Store extends StoreApi<any>>(
makeStore: () => Store,
) {
const Context = createContext<Store | undefined>(undefined);
const StoreProvider = ({ children }: { children: ReactNode }) => {
const [store] = useState(makeStore);
return <Context.Provider value={store}>{children}</Context.Provider>;
};
function useStore() {
const store = useContext(Context);
if (!store) {
throw new Error('useStore must be used within a StoreProvider');
}
return store;
}
function useSelector<Selected>(
selector: (state: ExtractState<Store>) => Selected,
) {
const store = useStore();
return useZustandStore(store, selector);
}
return { StoreProvider, useStore, useSelector };
}

const countSlice = createSlice({
name: 'count',
value: 0,
actions: {
inc: () => (state) => state + 1,
reset: () => () => 0,
},
});

const textSlice = createSlice({
name: 'text',
value: 'Hello',
actions: {
updateText: (text: string) => () => text,
reset: () => () => 'Hello',
},
});

const makeStore = () => create(withSlices(countSlice, textSlice));

const { StoreProvider, useStore, useSelector } =
createZustandContext(makeStore);

const renderWithProvider = (
ui: ReactNode,
options?: Omit<RenderOptions, 'wrapper'>,
) => render(ui, { wrapper: StoreProvider, ...options });

const Counter = () => {
const count = useSelector((state) => state.count);
const { inc } = useStore().getState();
return (
<div>
<p data-testid="count">{count}</p>
<button type="button" onClick={inc}>
Increment
</button>
</div>
);
};

const Text = () => {
const text = useSelector((state) => state.text);
const { updateText } = useStore().getState();
return (
<div>
<input value={text} onChange={(e) => updateText(e.target.value)} />
</div>
);
};

const App = () => {
const { reset } = useStore().getState();
return (
<div>
<Counter />
<Text />
<button type="button" onClick={reset}>
Reset
</button>
</div>
);
};

describe('component spec', () => {
const user = userEvent.setup();
it('should render the app', () => {
const { getByRole, getByTestId } = renderWithProvider(<App />);
expect(getByTestId('count')).toHaveTextContent('0');
expect(getByRole('textbox')).toBeInTheDocument();
});
it('should increment the count when the button is pressed', async () => {
const { getByRole, getByTestId } = renderWithProvider(<App />);

const count = getByTestId('count');
expect(count).toHaveTextContent('0');

const button = getByRole('button', { name: 'Increment' });
await user.click(button);

expect(count).toHaveTextContent('1');
});
it('should update the text when the input is changed', async () => {
const { getByRole } = renderWithProvider(<App />);

const input = getByRole('textbox');
expect(input).toHaveValue('Hello');

await user.type(input, ' World');

expect(input).toHaveValue('Hello World');
});
it('should reset the state when the reset button is pressed', async () => {
const { getByRole, getByTestId } = renderWithProvider(<App />);

const resetButton = getByRole('button', { name: 'Reset' });

const count = getByTestId('count');
expect(count).toHaveTextContent('0');

const incrementButton = getByRole('button', { name: 'Increment' });
await user.click(incrementButton);
expect(count).toHaveTextContent('1');

const input = getByRole('textbox');
await user.type(input, ' World');
expect(input).toHaveValue('Hello World');

await user.click(resetButton);

// both slices reset because the action name is the same
expect(count).toHaveTextContent('0');
expect(input).toHaveValue('Hello');
});
});
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
},
"jest": {
"testEnvironment": "jsdom",
"preset": "ts-jest/presets/js-with-ts"
"preset": "ts-jest/presets/js-with-ts",
"setupFilesAfterEnv": [
"./test-setup.ts"
]
},
"keywords": [
"react",
Expand All @@ -44,6 +47,9 @@
],
"license": "MIT",
"devDependencies": {
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^15.0.2",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.7",
"@types/react": "^18.2.79",
Expand Down
Loading
Loading