-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create test suite for core functionality (#3)
* write basic tests * add component tests
- Loading branch information
1 parent
ff79496
commit 5c4b769
Showing
7 changed files
with
370 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.