| 1 | import { createApi } from '@reduxjs/toolkit/query/react'
|
|---|
| 2 | import { createAction } from '@reduxjs/toolkit'
|
|---|
| 3 | import {
|
|---|
| 4 | actionsReducer,
|
|---|
| 5 | hookWaitFor,
|
|---|
| 6 | setupApiStore,
|
|---|
| 7 | } from '../../tests/utils/helpers'
|
|---|
| 8 | import {
|
|---|
| 9 | render,
|
|---|
| 10 | renderHook,
|
|---|
| 11 | act,
|
|---|
| 12 | waitFor,
|
|---|
| 13 | screen,
|
|---|
| 14 | } from '@testing-library/react'
|
|---|
| 15 | import { delay } from 'msw'
|
|---|
| 16 |
|
|---|
| 17 | interface Post {
|
|---|
| 18 | id: string
|
|---|
| 19 | title: string
|
|---|
| 20 | contents: string
|
|---|
| 21 | }
|
|---|
| 22 |
|
|---|
| 23 | interface FolderT {
|
|---|
| 24 | id: number
|
|---|
| 25 | children: FolderT[]
|
|---|
| 26 | }
|
|---|
| 27 |
|
|---|
| 28 | const baseQuery = vi.fn()
|
|---|
| 29 | beforeEach(() => baseQuery.mockReset())
|
|---|
| 30 |
|
|---|
| 31 | const postAddedAction = createAction<string>('postAdded')
|
|---|
| 32 |
|
|---|
| 33 | const api = createApi({
|
|---|
| 34 | baseQuery: (...args: any[]) => {
|
|---|
| 35 | const result = baseQuery(...args)
|
|---|
| 36 | if (typeof result === 'object' && 'then' in result)
|
|---|
| 37 | return result
|
|---|
| 38 | .then((data: any) => ({ data, meta: 'meta' }))
|
|---|
| 39 | .catch((e: any) => ({ error: e }))
|
|---|
| 40 | return { data: result, meta: 'meta' }
|
|---|
| 41 | },
|
|---|
| 42 | tagTypes: ['Post', 'Folder'],
|
|---|
| 43 | endpoints: (build) => ({
|
|---|
| 44 | getPosts: build.query<Post[], void>({ query: () => '/posts' }),
|
|---|
| 45 | post: build.query<Post, string>({
|
|---|
| 46 | query: (id) => `post/${id}`,
|
|---|
| 47 | providesTags: ['Post'],
|
|---|
| 48 | }),
|
|---|
| 49 | updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
|
|---|
| 50 | query: ({ id, ...patch }) => ({
|
|---|
| 51 | url: `post/${id}`,
|
|---|
| 52 | method: 'PATCH',
|
|---|
| 53 | body: patch,
|
|---|
| 54 | }),
|
|---|
| 55 | async onQueryStarted(arg, { dispatch, queryFulfilled, getState }) {
|
|---|
| 56 | const currentItem = api.endpoints.post.select(arg.id)(getState())
|
|---|
| 57 | if (currentItem?.data) {
|
|---|
| 58 | dispatch(
|
|---|
| 59 | api.util.upsertQueryData('post', arg.id, {
|
|---|
| 60 | ...currentItem.data,
|
|---|
| 61 | ...arg,
|
|---|
| 62 | }),
|
|---|
| 63 | )
|
|---|
| 64 | }
|
|---|
| 65 | },
|
|---|
| 66 | invalidatesTags: (result) => (result ? ['Post'] : []),
|
|---|
| 67 | }),
|
|---|
| 68 | post2: build.query<Post, string>({
|
|---|
| 69 | queryFn: async (id) => {
|
|---|
| 70 | await delay(20)
|
|---|
| 71 | return { data: { id, title: 'All about cheese.', contents: 'TODO' } }
|
|---|
| 72 | },
|
|---|
| 73 | }),
|
|---|
| 74 | postWithSideEffect: build.query<Post, string>({
|
|---|
| 75 | query: (id) => `post/${id}`,
|
|---|
| 76 | providesTags: (result) => {
|
|---|
| 77 | if (result) {
|
|---|
| 78 | return [{ type: 'Post', id: result.id } as const]
|
|---|
| 79 | }
|
|---|
| 80 | return []
|
|---|
| 81 | },
|
|---|
| 82 | async onCacheEntryAdded(arg, api) {
|
|---|
| 83 | // Verify that lifecycle promise resolution works
|
|---|
| 84 | const res = await api.cacheDataLoaded
|
|---|
| 85 |
|
|---|
| 86 | // and leave a side effect we can check in the test
|
|---|
| 87 | api.dispatch(postAddedAction(res.data.id))
|
|---|
| 88 | },
|
|---|
| 89 | keepUnusedDataFor: 0.1,
|
|---|
| 90 | }),
|
|---|
| 91 | getFolder: build.query<FolderT, number>({
|
|---|
| 92 | queryFn: async (args) => {
|
|---|
| 93 | return {
|
|---|
| 94 | data: {
|
|---|
| 95 | id: args,
|
|---|
| 96 | // Folder contains children that are as well folders
|
|---|
| 97 | children: [{ id: 2, children: [] }],
|
|---|
| 98 | },
|
|---|
| 99 | }
|
|---|
| 100 | },
|
|---|
| 101 | providesTags: (result, err, args) => [{ type: 'Folder', id: args }],
|
|---|
| 102 | onQueryStarted: async (args, queryApi) => {
|
|---|
| 103 | const { data } = await queryApi.queryFulfilled
|
|---|
| 104 |
|
|---|
| 105 | // Upsert getFolder endpoint with children from response data
|
|---|
| 106 | const upsertData = data.children.map((child) => ({
|
|---|
| 107 | arg: child.id,
|
|---|
| 108 | endpointName: 'getFolder' as const,
|
|---|
| 109 | value: child,
|
|---|
| 110 | }))
|
|---|
| 111 |
|
|---|
| 112 | queryApi.dispatch(api.util.upsertQueryEntries(upsertData))
|
|---|
| 113 | },
|
|---|
| 114 | }),
|
|---|
| 115 | }),
|
|---|
| 116 | })
|
|---|
| 117 |
|
|---|
| 118 | const storeRef = setupApiStore(api, { ...actionsReducer })
|
|---|
| 119 |
|
|---|
| 120 | describe('basic lifecycle', () => {
|
|---|
| 121 | let onStart = vi.fn(),
|
|---|
| 122 | onError = vi.fn(),
|
|---|
| 123 | onSuccess = vi.fn()
|
|---|
| 124 |
|
|---|
| 125 | const extendedApi = api.injectEndpoints({
|
|---|
| 126 | endpoints: (build) => ({
|
|---|
| 127 | test: build.mutation({
|
|---|
| 128 | query: (x) => x,
|
|---|
| 129 | async onQueryStarted(arg, api) {
|
|---|
| 130 | onStart(arg)
|
|---|
| 131 | try {
|
|---|
| 132 | const result = await api.queryFulfilled
|
|---|
| 133 | onSuccess(result)
|
|---|
| 134 | } catch (e) {
|
|---|
| 135 | onError(e)
|
|---|
| 136 | }
|
|---|
| 137 | },
|
|---|
| 138 | }),
|
|---|
| 139 | }),
|
|---|
| 140 | overrideExisting: true,
|
|---|
| 141 | })
|
|---|
| 142 |
|
|---|
| 143 | beforeEach(() => {
|
|---|
| 144 | onStart.mockReset()
|
|---|
| 145 | onError.mockReset()
|
|---|
| 146 | onSuccess.mockReset()
|
|---|
| 147 | })
|
|---|
| 148 |
|
|---|
| 149 | test('Does basic inserts and upserts', async () => {
|
|---|
| 150 | const newPost: Post = {
|
|---|
| 151 | id: '3',
|
|---|
| 152 | contents: 'Inserted content',
|
|---|
| 153 | title: 'Inserted title',
|
|---|
| 154 | }
|
|---|
| 155 | const insertPromise = storeRef.store.dispatch(
|
|---|
| 156 | api.util.upsertQueryData('post', newPost.id, newPost),
|
|---|
| 157 | )
|
|---|
| 158 |
|
|---|
| 159 | await insertPromise
|
|---|
| 160 |
|
|---|
| 161 | const selectPost3 = api.endpoints.post.select(newPost.id)
|
|---|
| 162 | const insertedPostEntry = selectPost3(storeRef.store.getState())
|
|---|
| 163 | expect(insertedPostEntry.isSuccess).toBe(true)
|
|---|
| 164 | expect(insertedPostEntry.data).toEqual(newPost)
|
|---|
| 165 |
|
|---|
| 166 | const updatedPost: Post = {
|
|---|
| 167 | id: '3',
|
|---|
| 168 | contents: 'Updated content',
|
|---|
| 169 | title: 'Updated title',
|
|---|
| 170 | }
|
|---|
| 171 |
|
|---|
| 172 | const updatePromise = storeRef.store.dispatch(
|
|---|
| 173 | api.util.upsertQueryData('post', updatedPost.id, updatedPost),
|
|---|
| 174 | )
|
|---|
| 175 |
|
|---|
| 176 | await updatePromise
|
|---|
| 177 |
|
|---|
| 178 | const updatedPostEntry = selectPost3(storeRef.store.getState())
|
|---|
| 179 |
|
|---|
| 180 | expect(updatedPostEntry.isSuccess).toBe(true)
|
|---|
| 181 | expect(updatedPostEntry.data).toEqual(updatedPost)
|
|---|
| 182 | })
|
|---|
| 183 |
|
|---|
| 184 | test('success', async () => {
|
|---|
| 185 | const { result } = renderHook(
|
|---|
| 186 | () => extendedApi.endpoints.test.useMutation(),
|
|---|
| 187 | { wrapper: storeRef.wrapper },
|
|---|
| 188 | )
|
|---|
| 189 |
|
|---|
| 190 | baseQuery.mockResolvedValue('success')
|
|---|
| 191 |
|
|---|
| 192 | expect(onStart).not.toHaveBeenCalled()
|
|---|
| 193 | expect(baseQuery).not.toHaveBeenCalled()
|
|---|
| 194 | act(() => void result.current[0]('arg'))
|
|---|
| 195 | expect(onStart).toHaveBeenCalledWith('arg')
|
|---|
| 196 | expect(baseQuery).toHaveBeenCalledWith('arg', expect.any(Object), undefined)
|
|---|
| 197 |
|
|---|
| 198 | expect(onError).not.toHaveBeenCalled()
|
|---|
| 199 | expect(onSuccess).not.toHaveBeenCalled()
|
|---|
| 200 | await act(() => delay(5))
|
|---|
| 201 | expect(onError).not.toHaveBeenCalled()
|
|---|
| 202 | expect(onSuccess).toHaveBeenCalledWith({ data: 'success', meta: 'meta' })
|
|---|
| 203 | })
|
|---|
| 204 |
|
|---|
| 205 | test('error', async () => {
|
|---|
| 206 | const { result } = renderHook(
|
|---|
| 207 | () => extendedApi.endpoints.test.useMutation(),
|
|---|
| 208 | { wrapper: storeRef.wrapper },
|
|---|
| 209 | )
|
|---|
| 210 |
|
|---|
| 211 | baseQuery.mockRejectedValueOnce('error')
|
|---|
| 212 |
|
|---|
| 213 | expect(onStart).not.toHaveBeenCalled()
|
|---|
| 214 | expect(baseQuery).not.toHaveBeenCalled()
|
|---|
| 215 | act(() => void result.current[0]('arg'))
|
|---|
| 216 | expect(onStart).toHaveBeenCalledWith('arg')
|
|---|
| 217 | expect(baseQuery).toHaveBeenCalledWith('arg', expect.any(Object), undefined)
|
|---|
| 218 |
|
|---|
| 219 | expect(onError).not.toHaveBeenCalled()
|
|---|
| 220 | expect(onSuccess).not.toHaveBeenCalled()
|
|---|
| 221 | await act(() => delay(5))
|
|---|
| 222 | expect(onError).toHaveBeenCalledWith({
|
|---|
| 223 | error: 'error',
|
|---|
| 224 | isUnhandledError: false,
|
|---|
| 225 | meta: undefined,
|
|---|
| 226 | })
|
|---|
| 227 | expect(onSuccess).not.toHaveBeenCalled()
|
|---|
| 228 | })
|
|---|
| 229 | })
|
|---|
| 230 |
|
|---|
| 231 | describe('upsertQueryData', () => {
|
|---|
| 232 | test('inserts cache entry', async () => {
|
|---|
| 233 | baseQuery
|
|---|
| 234 | .mockResolvedValueOnce({
|
|---|
| 235 | id: '3',
|
|---|
| 236 | title: 'All about cheese.',
|
|---|
| 237 | contents: 'TODO',
|
|---|
| 238 | })
|
|---|
| 239 | // TODO I have no idea why the query is getting called multiple times,
|
|---|
| 240 | // but passing an additional mocked value (_any_ value)
|
|---|
| 241 | // seems to silence some annoying "got an undefined result" logging
|
|---|
| 242 | .mockResolvedValueOnce(42)
|
|---|
| 243 | const { result } = renderHook(() => api.endpoints.post.useQuery('3'), {
|
|---|
| 244 | wrapper: storeRef.wrapper,
|
|---|
| 245 | })
|
|---|
| 246 | await hookWaitFor(() => expect(result.current.isSuccess).toBeTruthy())
|
|---|
| 247 |
|
|---|
| 248 | const dataBefore = result.current.data
|
|---|
| 249 | expect(dataBefore).toEqual({
|
|---|
| 250 | id: '3',
|
|---|
| 251 | title: 'All about cheese.',
|
|---|
| 252 | contents: 'TODO',
|
|---|
| 253 | })
|
|---|
| 254 |
|
|---|
| 255 | await act(async () => {
|
|---|
| 256 | storeRef.store.dispatch(
|
|---|
| 257 | api.util.upsertQueryData('post', '3', {
|
|---|
| 258 | id: '3',
|
|---|
| 259 | title: 'All about cheese.',
|
|---|
| 260 | contents: 'I love cheese!',
|
|---|
| 261 | }),
|
|---|
| 262 | )
|
|---|
| 263 | })
|
|---|
| 264 |
|
|---|
| 265 | expect(result.current.data).not.toBe(dataBefore)
|
|---|
| 266 | expect(result.current.data).toEqual({
|
|---|
| 267 | id: '3',
|
|---|
| 268 | title: 'All about cheese.',
|
|---|
| 269 | contents: 'I love cheese!',
|
|---|
| 270 | })
|
|---|
| 271 | })
|
|---|
| 272 |
|
|---|
| 273 | test('does update non-existing values', async () => {
|
|---|
| 274 | baseQuery
|
|---|
| 275 | // throw an error to make sure there is no cached data
|
|---|
| 276 | .mockImplementationOnce(async () => {
|
|---|
| 277 | throw new Error('failed to load')
|
|---|
| 278 | })
|
|---|
| 279 | .mockResolvedValueOnce(42)
|
|---|
| 280 |
|
|---|
| 281 | // a subscriber is needed to have the data stay in the cache
|
|---|
| 282 | // Not sure if this is the wanted behavior, I would have liked
|
|---|
| 283 | // it to stay in the cache for the x amount of time the cache
|
|---|
| 284 | // is preserved normally after the last subscriber was unmounted
|
|---|
| 285 | const { result, rerender } = renderHook(
|
|---|
| 286 | () => api.endpoints.post.useQuery('4'),
|
|---|
| 287 | { wrapper: storeRef.wrapper },
|
|---|
| 288 | )
|
|---|
| 289 | await hookWaitFor(() => expect(result.current.isError).toBeTruthy())
|
|---|
| 290 |
|
|---|
| 291 | // upsert the data
|
|---|
| 292 | act(() => {
|
|---|
| 293 | storeRef.store.dispatch(
|
|---|
| 294 | api.util.upsertQueryData('post', '4', {
|
|---|
| 295 | id: '4',
|
|---|
| 296 | title: 'All about cheese',
|
|---|
| 297 | contents: 'I love cheese!',
|
|---|
| 298 | }),
|
|---|
| 299 | )
|
|---|
| 300 | })
|
|---|
| 301 |
|
|---|
| 302 | // rerender the hook
|
|---|
| 303 | rerender()
|
|---|
| 304 | // wait until everything has settled
|
|---|
| 305 | await hookWaitFor(() => expect(result.current.isSuccess).toBeTruthy())
|
|---|
| 306 |
|
|---|
| 307 | // the cached data is returned as the result
|
|---|
| 308 | expect(result.current.data).toStrictEqual({
|
|---|
| 309 | id: '4',
|
|---|
| 310 | title: 'All about cheese',
|
|---|
| 311 | contents: 'I love cheese!',
|
|---|
| 312 | })
|
|---|
| 313 | })
|
|---|
| 314 |
|
|---|
| 315 | test('upsert while a normal query is running (success)', async () => {
|
|---|
| 316 | const fetchedData = {
|
|---|
| 317 | id: '3',
|
|---|
| 318 | title: 'All about cheese.',
|
|---|
| 319 | contents: 'Yummy',
|
|---|
| 320 | }
|
|---|
| 321 | baseQuery.mockImplementation(() => delay(20).then(() => fetchedData))
|
|---|
| 322 | const upsertedData = {
|
|---|
| 323 | id: '3',
|
|---|
| 324 | title: 'Data from a SSR Render',
|
|---|
| 325 | contents: 'This is just some random data',
|
|---|
| 326 | }
|
|---|
| 327 |
|
|---|
| 328 | const selector = api.endpoints.post.select('3')
|
|---|
| 329 | const fetchRes = storeRef.store.dispatch(api.endpoints.post.initiate('3'))
|
|---|
| 330 | const upsertRes = storeRef.store.dispatch(
|
|---|
| 331 | api.util.upsertQueryData('post', '3', upsertedData),
|
|---|
| 332 | )
|
|---|
| 333 |
|
|---|
| 334 | await upsertRes
|
|---|
| 335 | let state = selector(storeRef.store.getState())
|
|---|
| 336 | expect(state.data).toEqual(upsertedData)
|
|---|
| 337 |
|
|---|
| 338 | await fetchRes
|
|---|
| 339 | state = selector(storeRef.store.getState())
|
|---|
| 340 | expect(state.data).toEqual(fetchedData)
|
|---|
| 341 | })
|
|---|
| 342 | test('upsert while a normal query is running (rejected)', async () => {
|
|---|
| 343 | baseQuery.mockImplementationOnce(async () => {
|
|---|
| 344 | await delay(20)
|
|---|
| 345 | // eslint-disable-next-line no-throw-literal
|
|---|
| 346 | throw 'Error!'
|
|---|
| 347 | })
|
|---|
| 348 | const upsertedData = {
|
|---|
| 349 | id: '3',
|
|---|
| 350 | title: 'Data from a SSR Render',
|
|---|
| 351 | contents: 'This is just some random data',
|
|---|
| 352 | }
|
|---|
| 353 |
|
|---|
| 354 | const selector = api.endpoints.post.select('3')
|
|---|
| 355 | const fetchRes = storeRef.store.dispatch(api.endpoints.post.initiate('3'))
|
|---|
| 356 | const upsertRes = storeRef.store.dispatch(
|
|---|
| 357 | api.util.upsertQueryData('post', '3', upsertedData),
|
|---|
| 358 | )
|
|---|
| 359 |
|
|---|
| 360 | await upsertRes
|
|---|
| 361 | let state = selector(storeRef.store.getState())
|
|---|
| 362 | expect(state.data).toEqual(upsertedData)
|
|---|
| 363 | expect(state.isSuccess).toBeTruthy()
|
|---|
| 364 |
|
|---|
| 365 | await fetchRes
|
|---|
| 366 | state = selector(storeRef.store.getState())
|
|---|
| 367 | expect(state.data).toEqual(upsertedData)
|
|---|
| 368 | expect(state.isError).toBeTruthy()
|
|---|
| 369 | })
|
|---|
| 370 | })
|
|---|
| 371 |
|
|---|
| 372 | describe('upsertQueryEntries', () => {
|
|---|
| 373 | const posts: Post[] = [
|
|---|
| 374 | { id: '1', contents: 'A', title: 'A' },
|
|---|
| 375 | { id: '2', contents: 'B', title: 'B' },
|
|---|
| 376 | { id: '3', contents: 'C', title: 'C' },
|
|---|
| 377 | ]
|
|---|
| 378 |
|
|---|
| 379 | const entriesAction = api.util.upsertQueryEntries([
|
|---|
| 380 | { endpointName: 'getPosts', arg: undefined, value: posts },
|
|---|
| 381 | ...posts.map((post) => ({
|
|---|
| 382 | endpointName: 'postWithSideEffect' as const,
|
|---|
| 383 | arg: post.id,
|
|---|
| 384 | value: post,
|
|---|
| 385 | })),
|
|---|
| 386 | ])
|
|---|
| 387 |
|
|---|
| 388 | test('Upserts many entries at once', async () => {
|
|---|
| 389 | storeRef.store.dispatch(entriesAction)
|
|---|
| 390 |
|
|---|
| 391 | const state = storeRef.store.getState()
|
|---|
| 392 |
|
|---|
| 393 | expect(api.endpoints.getPosts.select()(state).data).toBe(posts)
|
|---|
| 394 |
|
|---|
| 395 | for (const post of posts) {
|
|---|
| 396 | expect(api.endpoints.postWithSideEffect.select(post.id)(state).data).toBe(
|
|---|
| 397 | post,
|
|---|
| 398 | )
|
|---|
| 399 |
|
|---|
| 400 | // Should have added tags
|
|---|
| 401 | expect(state.api.provided.tags.Post[post.id]).toEqual([
|
|---|
| 402 | `postWithSideEffect("${post.id}")`,
|
|---|
| 403 | ])
|
|---|
| 404 | }
|
|---|
| 405 | })
|
|---|
| 406 |
|
|---|
| 407 | test('Triggers cache lifecycles and side effects', async () => {
|
|---|
| 408 | storeRef.store.dispatch(entriesAction)
|
|---|
| 409 |
|
|---|
| 410 | // Tricky timing. The cache data promises will be resolved
|
|---|
| 411 | // in microtasks. We need to wait for them. Best to do this
|
|---|
| 412 | // in a loop just to avoid a hardcoded delay, but also this
|
|---|
| 413 | // needs to complete before `keepUnusedDataFor` expires them.
|
|---|
| 414 | await waitFor(
|
|---|
| 415 | () => {
|
|---|
| 416 | const state = storeRef.store.getState()
|
|---|
| 417 |
|
|---|
| 418 | // onCacheEntryAdded should have run for each post,
|
|---|
| 419 | // including cache data being resolved
|
|---|
| 420 | for (const post of posts) {
|
|---|
| 421 | const matchingSideEffectAction = state.actions.find(
|
|---|
| 422 | (action) =>
|
|---|
| 423 | postAddedAction.match(action) && action.payload === post.id,
|
|---|
| 424 | )
|
|---|
| 425 | expect(matchingSideEffectAction).toBeTruthy()
|
|---|
| 426 | }
|
|---|
| 427 |
|
|---|
| 428 | const selectedData =
|
|---|
| 429 | api.endpoints.postWithSideEffect.select('1')(state).data
|
|---|
| 430 |
|
|---|
| 431 | expect(selectedData).toBe(posts[0])
|
|---|
| 432 | },
|
|---|
| 433 | { timeout: 150, interval: 5 },
|
|---|
| 434 | )
|
|---|
| 435 |
|
|---|
| 436 | // The cache data should be removed after the keepUnusedDataFor time,
|
|---|
| 437 | // so wait longer than that
|
|---|
| 438 | await delay(300)
|
|---|
| 439 |
|
|---|
| 440 | const stateAfter = storeRef.store.getState()
|
|---|
| 441 |
|
|---|
| 442 | expect(api.endpoints.postWithSideEffect.select('1')(stateAfter).data).toBe(
|
|---|
| 443 | undefined,
|
|---|
| 444 | )
|
|---|
| 445 | })
|
|---|
| 446 |
|
|---|
| 447 | test('Handles repeated upserts and async lifecycles', async () => {
|
|---|
| 448 | const StateForUpsertFolder = ({ folderId }: { folderId: number }) => {
|
|---|
| 449 | const { status } = api.useGetFolderQuery(folderId)
|
|---|
| 450 |
|
|---|
| 451 | return (
|
|---|
| 452 | <>
|
|---|
| 453 | <div>
|
|---|
| 454 | Status getFolder with ID (
|
|---|
| 455 | {folderId === 1 ? 'original request' : 'upserted'}) {folderId}:{' '}
|
|---|
| 456 | <span data-testid={`status-${folderId}`}>{status}</span>
|
|---|
| 457 | </div>
|
|---|
| 458 | </>
|
|---|
| 459 | )
|
|---|
| 460 | }
|
|---|
| 461 |
|
|---|
| 462 | const Folder = () => {
|
|---|
| 463 | const { data, isLoading, isError } = api.useGetFolderQuery(1)
|
|---|
| 464 |
|
|---|
| 465 | return (
|
|---|
| 466 | <div>
|
|---|
| 467 | <h1>Folders</h1>
|
|---|
| 468 |
|
|---|
| 469 | {isLoading && <div>Loading...</div>}
|
|---|
| 470 |
|
|---|
| 471 | {isError && <div>Error...</div>}
|
|---|
| 472 |
|
|---|
| 473 | <StateForUpsertFolder key={`state-${1}`} folderId={1} />
|
|---|
| 474 | <StateForUpsertFolder key={`state-${2}`} folderId={2} />
|
|---|
| 475 | </div>
|
|---|
| 476 | )
|
|---|
| 477 | }
|
|---|
| 478 |
|
|---|
| 479 | render(<Folder />, { wrapper: storeRef.wrapper })
|
|---|
| 480 |
|
|---|
| 481 | await waitFor(() => {
|
|---|
| 482 | const { actions } = storeRef.store.getState()
|
|---|
| 483 | // Inspection:
|
|---|
| 484 | // - 2 inits
|
|---|
| 485 | // - 2 pendings, 2 fulfilleds for the hook queries
|
|---|
| 486 | // - 2 upserts
|
|---|
| 487 | expect(actions.length).toBe(8)
|
|---|
| 488 | expect(
|
|---|
| 489 | actions.filter((a) => api.util.upsertQueryEntries.match(a)).length,
|
|---|
| 490 | ).toBe(2)
|
|---|
| 491 | })
|
|---|
| 492 | expect(screen.getByTestId('status-2').textContent).toBe('fulfilled')
|
|---|
| 493 | })
|
|---|
| 494 | })
|
|---|
| 495 |
|
|---|
| 496 | describe('full integration', () => {
|
|---|
| 497 | test('success case', async () => {
|
|---|
| 498 | baseQuery
|
|---|
| 499 | .mockResolvedValueOnce({
|
|---|
| 500 | id: '3',
|
|---|
| 501 | title: 'All about cheese.',
|
|---|
| 502 | contents: 'TODO',
|
|---|
| 503 | })
|
|---|
| 504 | .mockResolvedValueOnce({
|
|---|
| 505 | id: '3',
|
|---|
| 506 | title: 'Meanwhile, this changed server-side.',
|
|---|
| 507 | contents: 'Delicious cheese!',
|
|---|
| 508 | })
|
|---|
| 509 | .mockResolvedValueOnce({
|
|---|
| 510 | id: '3',
|
|---|
| 511 | title: 'Meanwhile, this changed server-side.',
|
|---|
| 512 | contents: 'Delicious cheese!',
|
|---|
| 513 | })
|
|---|
| 514 | .mockResolvedValueOnce(42)
|
|---|
| 515 | const { result } = renderHook(
|
|---|
| 516 | () => ({
|
|---|
| 517 | query: api.endpoints.post.useQuery('3'),
|
|---|
| 518 | mutation: api.endpoints.updatePost.useMutation(),
|
|---|
| 519 | }),
|
|---|
| 520 | { wrapper: storeRef.wrapper },
|
|---|
| 521 | )
|
|---|
| 522 | await hookWaitFor(() => expect(result.current.query.isSuccess).toBeTruthy())
|
|---|
| 523 |
|
|---|
| 524 | expect(result.current.query.data).toEqual({
|
|---|
| 525 | id: '3',
|
|---|
| 526 | title: 'All about cheese.',
|
|---|
| 527 | contents: 'TODO',
|
|---|
| 528 | })
|
|---|
| 529 |
|
|---|
| 530 | await act(async () => {
|
|---|
| 531 | await result.current.mutation[0]({
|
|---|
| 532 | id: '3',
|
|---|
| 533 | contents: 'Delicious cheese!',
|
|---|
| 534 | })
|
|---|
| 535 | })
|
|---|
| 536 |
|
|---|
| 537 | expect(result.current.query.data).toEqual({
|
|---|
| 538 | id: '3',
|
|---|
| 539 | title: 'Meanwhile, this changed server-side.',
|
|---|
| 540 | contents: 'Delicious cheese!',
|
|---|
| 541 | })
|
|---|
| 542 |
|
|---|
| 543 | await hookWaitFor(() =>
|
|---|
| 544 | expect(result.current.query.data).toEqual({
|
|---|
| 545 | id: '3',
|
|---|
| 546 | title: 'Meanwhile, this changed server-side.',
|
|---|
| 547 | contents: 'Delicious cheese!',
|
|---|
| 548 | }),
|
|---|
| 549 | )
|
|---|
| 550 | })
|
|---|
| 551 |
|
|---|
| 552 | test('error case', async () => {
|
|---|
| 553 | baseQuery
|
|---|
| 554 | .mockResolvedValueOnce({
|
|---|
| 555 | id: '3',
|
|---|
| 556 | title: 'All about cheese.',
|
|---|
| 557 | contents: 'TODO',
|
|---|
| 558 | })
|
|---|
| 559 | .mockRejectedValueOnce('some error!')
|
|---|
| 560 | .mockResolvedValueOnce({
|
|---|
| 561 | id: '3',
|
|---|
| 562 | title: 'Meanwhile, this changed server-side.',
|
|---|
| 563 | contents: 'TODO',
|
|---|
| 564 | })
|
|---|
| 565 | .mockResolvedValueOnce(42)
|
|---|
| 566 |
|
|---|
| 567 | const { result } = renderHook(
|
|---|
| 568 | () => ({
|
|---|
| 569 | query: api.endpoints.post.useQuery('3'),
|
|---|
| 570 | mutation: api.endpoints.updatePost.useMutation(),
|
|---|
| 571 | }),
|
|---|
| 572 | { wrapper: storeRef.wrapper },
|
|---|
| 573 | )
|
|---|
| 574 | await hookWaitFor(() => expect(result.current.query.isSuccess).toBeTruthy())
|
|---|
| 575 |
|
|---|
| 576 | expect(result.current.query.data).toEqual({
|
|---|
| 577 | id: '3',
|
|---|
| 578 | title: 'All about cheese.',
|
|---|
| 579 | contents: 'TODO',
|
|---|
| 580 | })
|
|---|
| 581 |
|
|---|
| 582 | await act(async () => {
|
|---|
| 583 | await result.current.mutation[0]({
|
|---|
| 584 | id: '3',
|
|---|
| 585 | contents: 'Delicious cheese!',
|
|---|
| 586 | })
|
|---|
| 587 | })
|
|---|
| 588 |
|
|---|
| 589 | // optimistic update
|
|---|
| 590 | expect(result.current.query.data).toEqual({
|
|---|
| 591 | id: '3',
|
|---|
| 592 | title: 'All about cheese.',
|
|---|
| 593 | contents: 'Delicious cheese!',
|
|---|
| 594 | })
|
|---|
| 595 |
|
|---|
| 596 | // mutation failed - will not invalidate query and not refetch data from the server
|
|---|
| 597 | await expect(() =>
|
|---|
| 598 | hookWaitFor(
|
|---|
| 599 | () =>
|
|---|
| 600 | expect(result.current.query.data).toEqual({
|
|---|
| 601 | id: '3',
|
|---|
| 602 | title: 'Meanwhile, this changed server-side.',
|
|---|
| 603 | contents: 'TODO',
|
|---|
| 604 | }),
|
|---|
| 605 | 50,
|
|---|
| 606 | ),
|
|---|
| 607 | ).rejects.toBeTruthy()
|
|---|
| 608 |
|
|---|
| 609 | act(() => void result.current.query.refetch())
|
|---|
| 610 |
|
|---|
| 611 | // manually refetching gives up-to-date data
|
|---|
| 612 | await hookWaitFor(
|
|---|
| 613 | () =>
|
|---|
| 614 | expect(result.current.query.data).toEqual({
|
|---|
| 615 | id: '3',
|
|---|
| 616 | title: 'Meanwhile, this changed server-side.',
|
|---|
| 617 | contents: 'TODO',
|
|---|
| 618 | }),
|
|---|
| 619 | 50,
|
|---|
| 620 | )
|
|---|
| 621 | })
|
|---|
| 622 |
|
|---|
| 623 | test('Interop with in-flight requests', async () => {
|
|---|
| 624 | await act(async () => {
|
|---|
| 625 | const fetchRes = storeRef.store.dispatch(
|
|---|
| 626 | api.endpoints.post2.initiate('3'),
|
|---|
| 627 | )
|
|---|
| 628 |
|
|---|
| 629 | const upsertRes = storeRef.store.dispatch(
|
|---|
| 630 | api.util.upsertQueryData('post2', '3', {
|
|---|
| 631 | id: '3',
|
|---|
| 632 | title: 'Upserted title',
|
|---|
| 633 | contents: 'Upserted contents',
|
|---|
| 634 | }),
|
|---|
| 635 | )
|
|---|
| 636 |
|
|---|
| 637 | const selectEntry = api.endpoints.post2.select('3')
|
|---|
| 638 | await waitFor(
|
|---|
| 639 | () => {
|
|---|
| 640 | const entry1 = selectEntry(storeRef.store.getState())
|
|---|
| 641 | expect(entry1.data).toEqual({
|
|---|
| 642 | id: '3',
|
|---|
| 643 | title: 'Upserted title',
|
|---|
| 644 | contents: 'Upserted contents',
|
|---|
| 645 | })
|
|---|
| 646 | },
|
|---|
| 647 | { interval: 1, timeout: 15 },
|
|---|
| 648 | )
|
|---|
| 649 | await waitFor(
|
|---|
| 650 | () => {
|
|---|
| 651 | const entry2 = selectEntry(storeRef.store.getState())
|
|---|
| 652 | expect(entry2.data).toEqual({
|
|---|
| 653 | id: '3',
|
|---|
| 654 | title: 'All about cheese.',
|
|---|
| 655 | contents: 'TODO',
|
|---|
| 656 | })
|
|---|
| 657 | },
|
|---|
| 658 | { interval: 1 },
|
|---|
| 659 | )
|
|---|
| 660 | })
|
|---|
| 661 | })
|
|---|
| 662 | })
|
|---|