Spaces:
Running
Running
File size: 4,218 Bytes
c2b7eb3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | import type { PayloadAction } from '@reduxjs/toolkit'
import {
createAsyncThunk,
createAction,
createSlice,
configureStore,
createEntityAdapter,
} from '@reduxjs/toolkit'
import type { EntityAdapter } from '@internal/entities/models'
import type { BookModel } from '@internal/entities/tests/fixtures/book'
describe('Combined entity slice', () => {
let adapter: EntityAdapter<BookModel, string>
beforeEach(() => {
adapter = createEntityAdapter({
selectId: (book: BookModel) => book.id,
sortComparer: (a, b) => a.title.localeCompare(b.title),
})
})
it('Entity and async features all works together', async () => {
const upsertBook = createAction<BookModel>('otherBooks/upsert')
type BooksState = ReturnType<typeof adapter.getInitialState> & {
loading: 'initial' | 'pending' | 'finished' | 'failed'
lastRequestId: string | null
}
const initialState: BooksState = adapter.getInitialState({
loading: 'initial',
lastRequestId: null,
})
const fakeBooks: BookModel[] = [
{ id: 'b', title: 'Second' },
{ id: 'a', title: 'First' },
]
const fetchBooksTAC = createAsyncThunk<
BookModel[],
void,
{
state: { books: BooksState }
}
>(
'books/fetch',
async (arg, { getState, dispatch, extra, requestId, signal }) => {
const state = getState()
return fakeBooks
},
)
const booksSlice = createSlice({
name: 'books',
initialState,
reducers: {
addOne: adapter.addOne,
removeOne(state, action: PayloadAction<string>) {
const sizeBefore = state.ids.length
// Originally, having nested `produce` calls don't mutate `state` here as I would have expected.
// (note that `state` here is actually an Immer Draft<S>, from `createReducer`)
// One woarkound was to return the new plain result value instead
// See https://github.com/immerjs/immer/issues/533
// However, after tweaking `createStateOperator` to check if the argument is a draft,
// we can just treat the operator as strictly mutating, without returning a result,
// and the result should be correct.
const result = adapter.removeOne(state, action)
const sizeAfter = state.ids.length
if (sizeBefore > 0) {
expect(sizeAfter).toBe(sizeBefore - 1)
}
//Deliberately _don't_ return result
},
},
extraReducers: (builder) => {
builder.addCase(upsertBook, (state, action) => {
return adapter.upsertOne(state, action)
})
builder.addCase(fetchBooksTAC.pending, (state, action) => {
state.loading = 'pending'
state.lastRequestId = action.meta.requestId
})
builder.addCase(fetchBooksTAC.fulfilled, (state, action) => {
if (
state.loading === 'pending' &&
action.meta.requestId === state.lastRequestId
) {
adapter.setAll(state, action.payload)
state.loading = 'finished'
state.lastRequestId = null
}
})
},
})
const { addOne, removeOne } = booksSlice.actions
const { reducer } = booksSlice
const store = configureStore({
reducer: {
books: reducer,
},
})
await store.dispatch(fetchBooksTAC())
const { books: booksAfterLoaded } = store.getState()
// Sorted, so "First" goes first
expect(booksAfterLoaded.ids).toEqual(['a', 'b'])
expect(booksAfterLoaded.lastRequestId).toBe(null)
expect(booksAfterLoaded.loading).toBe('finished')
store.dispatch(addOne({ id: 'd', title: 'Remove Me' }))
store.dispatch(removeOne('d'))
store.dispatch(addOne({ id: 'c', title: 'Middle' }))
const { books: booksAfterAddOne } = store.getState()
// Sorted, so "Middle" goes in the middle
expect(booksAfterAddOne.ids).toEqual(['a', 'c', 'b'])
store.dispatch(upsertBook({ id: 'c', title: 'Zeroth' }))
const { books: booksAfterUpsert } = store.getState()
// Sorted, so "Zeroth" goes last
expect(booksAfterUpsert.ids).toEqual(['a', 'b', 'c'])
})
})
|