1 | import type { PayloadAction } from '@reduxjs/toolkit'
|
2 | import {
|
3 | createAsyncThunk,
|
4 | createAction,
|
5 | createSlice,
|
6 | configureStore,
|
7 | createEntityAdapter,
|
8 | } from '@reduxjs/toolkit'
|
9 | import type { EntityAdapter } from '@internal/entities/models'
|
10 | import type { BookModel } from '@internal/entities/tests/fixtures/book'
|
11 |
|
12 | describe('Combined entity slice', () => {
|
13 | let adapter: EntityAdapter<BookModel>
|
14 |
|
15 | beforeEach(() => {
|
16 | adapter = createEntityAdapter({
|
17 | selectId: (book: BookModel) => book.id,
|
18 | sortComparer: (a, b) => a.title.localeCompare(b.title),
|
19 | })
|
20 | })
|
21 |
|
22 | it('Entity and async features all works together', async () => {
|
23 | const upsertBook = createAction<BookModel>('otherBooks/upsert')
|
24 |
|
25 | type BooksState = ReturnType<typeof adapter.getInitialState> & {
|
26 | loading: 'initial' | 'pending' | 'finished' | 'failed'
|
27 | lastRequestId: string | null
|
28 | }
|
29 |
|
30 | const initialState: BooksState = adapter.getInitialState({
|
31 | loading: 'initial',
|
32 | lastRequestId: null,
|
33 | })
|
34 |
|
35 | const fakeBooks: BookModel[] = [
|
36 | { id: 'b', title: 'Second' },
|
37 | { id: 'a', title: 'First' },
|
38 | ]
|
39 |
|
40 | const fetchBooksTAC = createAsyncThunk<
|
41 | BookModel[],
|
42 | void,
|
43 | {
|
44 | state: { books: BooksState }
|
45 | }
|
46 | >(
|
47 | 'books/fetch',
|
48 | async (arg, { getState, dispatch, extra, requestId, signal }) => {
|
49 | const state = getState()
|
50 | return fakeBooks
|
51 | }
|
52 | )
|
53 |
|
54 | const booksSlice = createSlice({
|
55 | name: 'books',
|
56 | initialState,
|
57 | reducers: {
|
58 | addOne: adapter.addOne,
|
59 | removeOne(state, action: PayloadAction<string>) {
|
60 | const sizeBefore = state.ids.length
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 | const result = adapter.removeOne(state, action)
|
69 |
|
70 | const sizeAfter = state.ids.length
|
71 | if (sizeBefore > 0) {
|
72 | expect(sizeAfter).toBe(sizeBefore - 1)
|
73 | }
|
74 |
|
75 |
|
76 | },
|
77 | },
|
78 | extraReducers: (builder) => {
|
79 | builder.addCase(upsertBook, (state, action) => {
|
80 | return adapter.upsertOne(state, action)
|
81 | })
|
82 | builder.addCase(fetchBooksTAC.pending, (state, action) => {
|
83 | state.loading = 'pending'
|
84 | state.lastRequestId = action.meta.requestId
|
85 | })
|
86 | builder.addCase(fetchBooksTAC.fulfilled, (state, action) => {
|
87 | if (
|
88 | state.loading === 'pending' &&
|
89 | action.meta.requestId === state.lastRequestId
|
90 | ) {
|
91 | adapter.setAll(state, action.payload)
|
92 | state.loading = 'finished'
|
93 | state.lastRequestId = null
|
94 | }
|
95 | })
|
96 | },
|
97 | })
|
98 |
|
99 | const { addOne, removeOne } = booksSlice.actions
|
100 | const { reducer } = booksSlice
|
101 |
|
102 | const store = configureStore({
|
103 | reducer: {
|
104 | books: reducer,
|
105 | },
|
106 | })
|
107 |
|
108 | await store.dispatch(fetchBooksTAC())
|
109 |
|
110 | const { books: booksAfterLoaded } = store.getState()
|
111 |
|
112 | expect(booksAfterLoaded.ids).toEqual(['a', 'b'])
|
113 | expect(booksAfterLoaded.lastRequestId).toBe(null)
|
114 | expect(booksAfterLoaded.loading).toBe('finished')
|
115 |
|
116 | store.dispatch(addOne({ id: 'd', title: 'Remove Me' }))
|
117 | store.dispatch(removeOne('d'))
|
118 |
|
119 | store.dispatch(addOne({ id: 'c', title: 'Middle' }))
|
120 |
|
121 | const { books: booksAfterAddOne } = store.getState()
|
122 |
|
123 |
|
124 | expect(booksAfterAddOne.ids).toEqual(['a', 'c', 'b'])
|
125 |
|
126 | store.dispatch(upsertBook({ id: 'c', title: 'Zeroth' }))
|
127 |
|
128 | const { books: booksAfterUpsert } = store.getState()
|
129 |
|
130 |
|
131 | expect(booksAfterUpsert.ids).toEqual(['a', 'b', 'c'])
|
132 | })
|
133 | })
|