diff --git a/tests/01_basic.spec.tsx b/tests/01_basic.spec.tsx index 9ffd22c..8eabf65 100644 --- a/tests/01_basic.spec.tsx +++ b/tests/01_basic.spec.tsx @@ -1,59 +1,48 @@ -import { describe, expect, it } from 'vitest'; +import { expect, test } from 'vitest'; import { create } from 'zustand'; import { createSlice, withSlices } from 'zustand-slices'; -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(); +test('should export functions', () => { + expect(createSlice).toBeDefined(); + expect(withSlices).toBeDefined(); +}); - expect(state.count).toBe(countSlice.value); - expect(state.text).toBe(textSlice.value); +test('createSlice', () => { + const slice = createSlice({ + name: 'counter', + value: 0, + actions: { + increment: () => (prev) => prev + 1, + }, + }); + // returns the input + expect(createSlice(slice)).toBe(slice); +}); - expect(state.inc).toBeInstanceOf(Function); - expect(state.reset).toBeInstanceOf(Function); - expect(state.updateText).toBeInstanceOf(Function); - }); +test('withSlices', () => { + 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); }); diff --git a/tests/02_type.spec.tsx b/tests/02_type.spec.tsx index 36f966b..1961488 100644 --- a/tests/02_type.spec.tsx +++ b/tests/02_type.spec.tsx @@ -1,149 +1,143 @@ -import { describe, it } from 'vitest'; +import { test } from 'vitest'; import { expectType } from 'ts-expect'; import type { TypeEqual } from 'ts-expect'; import { createSlice, withSlices } from 'zustand-slices'; -describe('slice type', () => { - it('single slice', () => { - const countSlice = createSlice({ - name: 'count', - value: 0, - actions: { - inc: () => (prev) => prev + 1, - }, - }); - expectType< - TypeEqual< - { - name: 'count'; - value: number; - actions: { - inc: () => (prev: number) => number; - }; - }, - typeof countSlice - > - >(true); +test('slice type: single slice', () => { + const countSlice = createSlice({ + name: 'count', + value: 0, + actions: { + inc: () => (prev) => prev + 1, + }, }); + expectType< + TypeEqual< + { + name: 'count'; + value: number; + actions: { + inc: () => (prev: number) => number; + }; + }, + typeof countSlice + > + >(true); }); -describe('name collisions', () => { - it('same slice names', () => { - const countSlice = createSlice({ - name: 'count', - value: 0, - actions: { - inc: () => (prev) => prev + 1, - }, - }); - const anotherCountSlice = createSlice({ - name: 'count', - value: 0, - actions: { - inc: () => (prev) => prev + 1, - }, - }); - expectType(withSlices(countSlice, anotherCountSlice)); +test('name collisions: same slice names', () => { + const countSlice = createSlice({ + name: 'count', + value: 0, + actions: { + inc: () => (prev) => prev + 1, + }, + }); + const anotherCountSlice = createSlice({ + name: 'count', + value: 0, + actions: { + inc: () => (prev) => prev + 1, + }, }); + expectType(withSlices(countSlice, anotherCountSlice)); +}); - it('slice name and action name', () => { - const countSlice = createSlice({ - name: 'count', - value: 0, - actions: { - inc: () => (prev) => prev + 1, - }, - }); - const anotherCountSlice = createSlice({ - name: 'anotherCount', - value: 0, - actions: { - count: () => (prev) => prev + 1, - }, - }); - expectType(withSlices(countSlice, anotherCountSlice)); +test('name collisions: slice name and action name', () => { + const countSlice = createSlice({ + name: 'count', + value: 0, + actions: { + inc: () => (prev) => prev + 1, + }, + }); + const anotherCountSlice = createSlice({ + name: 'anotherCount', + value: 0, + actions: { + count: () => (prev) => prev + 1, + }, }); + expectType(withSlices(countSlice, anotherCountSlice)); +}); - it('slice name and action name (overlapping case)', () => { - const countSlice = createSlice({ - name: 'count', - value: 0, - actions: { - inc: () => (prev) => prev + 1, - }, - }); - const anotherCountSlice = createSlice({ - name: 'anotherCount', - value: 0, - actions: { - count: () => (prev) => prev + 1, - dec: () => (prev) => prev - 1, - }, - }); - expectType(withSlices(countSlice, anotherCountSlice)); +test('name collisions: slice name and action name (overlapping case)', () => { + const countSlice = createSlice({ + name: 'count', + value: 0, + actions: { + inc: () => (prev) => prev + 1, + }, }); + const anotherCountSlice = createSlice({ + name: 'anotherCount', + value: 0, + actions: { + count: () => (prev) => prev + 1, + dec: () => (prev) => prev - 1, + }, + }); + expectType(withSlices(countSlice, anotherCountSlice)); }); -describe('args collisions', () => { - it('different args', () => { - const countSlice = createSlice({ - name: 'count', - value: 0, - actions: { - inc: (s: string) => (prev) => prev + (s ? 2 : 1), - }, - }); - const anotherCountSlice = createSlice({ - name: 'anotherCount', - value: 0, - actions: { - inc: (n: number) => (prev) => prev + n, - }, - }); - expectType(withSlices(countSlice, anotherCountSlice)); - expectType(withSlices(anotherCountSlice, countSlice)); +test('args collisions: different args', () => { + const countSlice = createSlice({ + name: 'count', + value: 0, + actions: { + inc: (s: string) => (prev) => prev + (s ? 2 : 1), + }, }); + const anotherCountSlice = createSlice({ + name: 'anotherCount', + value: 0, + actions: { + inc: (n: number) => (prev) => prev + n, + }, + }); + expectType(withSlices(countSlice, anotherCountSlice)); + expectType(withSlices(anotherCountSlice, countSlice)); +}); - it('same args', () => { - const countSlice = createSlice({ - name: 'count', - value: 0, - actions: { - inc: () => (prev) => prev + 1, - }, - }); - const anotherCountSlice = createSlice({ - name: 'anotherCount', - value: 0, - actions: { - anotherInc: (n: number) => (prev) => prev + n, - }, - }); - expectType<(...args: never[]) => unknown>( - withSlices(countSlice, anotherCountSlice), - ); - expectType<(...args: never[]) => unknown>( - withSlices(anotherCountSlice, countSlice), - ); +test('args collisions: same args', () => { + const countSlice = createSlice({ + name: 'count', + value: 0, + actions: { + inc: () => (prev) => prev + 1, + }, + }); + const anotherCountSlice = createSlice({ + name: 'anotherCount', + value: 0, + actions: { + anotherInc: (n: number) => (prev) => prev + n, + }, }); + expectType<(...args: never[]) => unknown>( + withSlices(countSlice, anotherCountSlice), + ); + expectType<(...args: never[]) => unknown>( + withSlices(anotherCountSlice, countSlice), + ); +}); - it('overload case', () => { - const countSlice = createSlice({ - name: 'count', - value: 0, - actions: { - inc: () => (prev) => prev + 1, - }, - }); - const anotherCountSlice = createSlice({ - name: 'anotherCount', - value: 0, - actions: { - inc: (n: number) => (prev) => prev + n, - }, - }); - expectType(withSlices(countSlice, anotherCountSlice)); - expectType(withSlices(anotherCountSlice, countSlice)); +test('args collisions: overload case', () => { + const countSlice = createSlice({ + name: 'count', + value: 0, + actions: { + inc: () => (prev) => prev + 1, + }, + }); + const anotherCountSlice = createSlice({ + name: 'anotherCount', + value: 0, + actions: { + inc: (n: number) => (prev) => prev + n, + }, }); + expectType(withSlices(countSlice, anotherCountSlice)); + expectType(withSlices(anotherCountSlice, countSlice)); }); diff --git a/tests/03_component.spec.tsx b/tests/03_component.spec.tsx index 556da5e..f6c9e14 100644 --- a/tests/03_component.spec.tsx +++ b/tests/03_component.spec.tsx @@ -1,10 +1,9 @@ -import { afterEach, describe, expect, it } from 'vitest'; -import { createContext, useContext, useState } from 'react'; +import { afterEach, expect, test } from 'vitest'; +import { createContext, useContext, useRef } from 'react'; import type { ReactNode } from 'react'; -import { create, useStore as useZustandStore } from 'zustand'; +import { create, useStore } from 'zustand'; import type { StoreApi } from 'zustand'; -import { cleanup, render } from '@testing-library/react'; -import type { RenderOptions } from '@testing-library/react'; +import { cleanup, render, screen } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import { createSlice, withSlices } from 'zustand-slices'; @@ -15,27 +14,27 @@ type ExtractState = S extends { : never; function createZustandContext>( - makeStore: () => Store, + initializeStore: () => Store, ) { - const Context = createContext(undefined); + const Context = createContext(null); const StoreProvider = ({ children }: { children: ReactNode }) => { - const [store] = useState(makeStore); + const store = useRef(initializeStore()).current; return {children}; }; - function useStore() { + function useStoreApi() { const store = useContext(Context); if (!store) { - throw new Error('useStore must be used within a StoreProvider'); + throw new Error('useStoreApi must be used within a StoreProvider'); } return store; } function useSelector( selector: (state: ExtractState) => Selected, ) { - const store = useStore(); - return useZustandStore(store, selector); + const store = useStoreApi(); + return useStore(store, selector); } - return { StoreProvider, useStore, useSelector }; + return { StoreProvider, useStoreApi, useSelector }; } const countSlice = createSlice({ @@ -56,19 +55,16 @@ const textSlice = createSlice({ }, }); -const makeStore = () => create(withSlices(countSlice, textSlice)); +const { StoreProvider, useStoreApi, useSelector } = createZustandContext(() => + create(withSlices(countSlice, textSlice)), +); -const { StoreProvider, useStore, useSelector } = - createZustandContext(makeStore); - -const renderWithProvider = ( - ui: ReactNode, - options?: Omit, -) => render(ui, { wrapper: StoreProvider, ...options }); +const renderWithStoreProvider = (app: ReactNode) => + render(app, { wrapper: StoreProvider }); const Counter = () => { const count = useSelector((state) => state.count); - const { inc } = useStore().getState(); + const { inc } = useStoreApi().getState(); return (

{count}

@@ -81,7 +77,7 @@ const Counter = () => { const Text = () => { const text = useSelector((state) => state.text); - const { updateText } = useStore().getState(); + const { updateText } = useStoreApi().getState(); return (
updateText(e.target.value)} /> @@ -90,7 +86,7 @@ const Text = () => { }; const App = () => { - const { reset } = useStore().getState(); + const { reset } = useStoreApi().getState(); return (
@@ -102,55 +98,40 @@ const App = () => { ); }; -describe('component spec', () => { - const user = userEvent.setup(); - afterEach(cleanup); - it('should render the app', () => { - const { getByRole, getByTestId } = renderWithProvider(); - expect(getByTestId('count')).toHaveTextContent('0'); - expect(getByRole('textbox')).toBeInTheDocument(); - }); - it('should increment the count when the button is pressed', async () => { - const { getByRole, getByTestId } = renderWithProvider(); - - 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(); - - 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(); +afterEach(cleanup); - 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'); +test('should render the app', () => { + renderWithStoreProvider(); + expect(screen.getByTestId('count')).toHaveTextContent('0'); + expect(screen.getByRole('textbox')).toBeInTheDocument(); +}); - const input = getByRole('textbox'); - await user.type(input, ' World'); - expect(input).toHaveValue('Hello World'); +test('should increment the count when the button is pressed', async () => { + const user = userEvent.setup(); + renderWithStoreProvider(); + expect(screen.getByTestId('count')).toHaveTextContent('0'); + await user.click(screen.getByRole('button', { name: 'Increment' })); + expect(screen.getByTestId('count')).toHaveTextContent('1'); +}); - await user.click(resetButton); +test('should update the text when the input is changed', async () => { + const user = userEvent.setup(); + renderWithStoreProvider(); + expect(screen.getByRole('textbox')).toHaveValue('Hello'); + await user.type(screen.getByRole('textbox'), ' World'); + expect(screen.getByRole('textbox')).toHaveValue('Hello World'); +}); - // both slices reset because the action name is the same - expect(count).toHaveTextContent('0'); - expect(input).toHaveValue('Hello'); - }); +test('should reset the state when the reset button is pressed', async () => { + const user = userEvent.setup(); + renderWithStoreProvider(); + expect(screen.getByTestId('count')).toHaveTextContent('0'); + await user.click(screen.getByRole('button', { name: 'Increment' })); + expect(screen.getByTestId('count')).toHaveTextContent('1'); + await user.type(screen.getByRole('textbox'), ' World'); + expect(screen.getByRole('textbox')).toHaveValue('Hello World'); + await user.click(screen.getByRole('button', { name: 'Reset' })); + // both slices reset because the action name is the same + expect(screen.getByTestId('count')).toHaveTextContent('0'); + expect(screen.getByRole('textbox')).toHaveValue('Hello'); });