From 12cdc2d70dd1fff6323b1b512960a94f237e3b4e Mon Sep 17 00:00:00 2001 From: Jonas Braathen Date: Sun, 2 Aug 2026 00:09:50 +0200 Subject: [PATCH] Revamp test mocking --- src/modules/login/Login.test.tsx | 28 ++++----- src/modules/membership/Membership.test.tsx | 45 ++++---------- src/modules/membership/Proof.test.tsx | 70 +++++++++------------- src/modules/membership/UserDelete.test.tsx | 22 +++---- src/modules/register/UserRegister.test.tsx | 50 +++++++--------- src/testUtils.tsx | 23 +++---- 6 files changed, 94 insertions(+), 144 deletions(-) diff --git a/src/modules/login/Login.test.tsx b/src/modules/login/Login.test.tsx index 5a00b17..7a579e5 100644 --- a/src/modules/login/Login.test.tsx +++ b/src/modules/login/Login.test.tsx @@ -1,6 +1,8 @@ import { fireEvent, waitFor, type RenderResult } from '@testing-library/react-native'; import React from 'react'; +import * as api from '@/api'; +import { ApiError } from '@/errors'; import Login from '@/modules/login/Login'; import { renderWithProviders } from '@/testUtils'; @@ -12,8 +14,6 @@ jest.mock('@react-navigation/native', () => { return { ...actual, useNavigation: () => ({ goBack: mockGoBack }) }; }); -const fetchMock = global.fetch as jest.Mock; - const fillForm = ( getByLabelText: RenderResult['getByLabelText'], { username = 'grisen@example.test', password = 'hunter2' } = {}, @@ -36,29 +36,23 @@ it('renders the login form', () => { }); it('logs in against the API and returns to the previous screen', async () => { - fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ token: 'token-123' }) }); + const login = jest.spyOn(api, 'login').mockResolvedValue({ token: 'token-123' }); const { getByText, getByLabelText } = renderWithProviders(); fillForm(getByLabelText); fireEvent.press(getByText('Logg inn')); await waitFor(() => expect(mockGoBack).toHaveBeenCalled()); - expect(fetchMock).toHaveBeenCalledWith( - 'https://dusken.example.test/auth/obtain-token/', - expect.objectContaining({ - method: 'POST', - body: JSON.stringify({ username: 'grisen@example.test', password: 'hunter2' }), - }), - ); + expect(login).toHaveBeenCalledWith('grisen@example.test', 'hunter2'); }); it('shows the server error when the credentials are rejected', async () => { jest.spyOn(console, 'log').mockImplementation(() => {}); - fetchMock.mockResolvedValueOnce({ - ok: false, - status: 400, - json: async () => ({ non_field_errors: ['Kan ikke logge inn med angitt brukernavn og passord'] }), - }); + const invalidCredentials = new ApiError( + { non_field_errors: ['Kan ikke logge inn med angitt brukernavn og passord'] }, + 400, + ); + jest.spyOn(api, 'login').mockRejectedValueOnce(invalidCredentials); const { getByText, getByLabelText, findByText } = renderWithProviders(); fillForm(getByLabelText); @@ -70,7 +64,7 @@ it('shows the server error when the credentials are rejected', async () => { it('shows a generic message when the request fails on the network', async () => { jest.spyOn(console, 'log').mockImplementation(() => {}); - fetchMock.mockRejectedValueOnce(new Error('network down')); + jest.spyOn(api, 'login').mockRejectedValueOnce(new Error('network down')); const { getByText, getByLabelText, findByText } = renderWithProviders(); fillForm(getByLabelText); @@ -87,5 +81,5 @@ it('logs in a dev-fake user without hitting the network', async () => { fireEvent.press(getByText('Logg inn')); await waitFor(() => expect(mockGoBack).toHaveBeenCalled()); - expect(fetchMock).not.toHaveBeenCalled(); + expect(global.fetch).not.toHaveBeenCalled(); }); diff --git a/src/modules/membership/Membership.test.tsx b/src/modules/membership/Membership.test.tsx index 1570e11..7c04465 100644 --- a/src/modules/membership/Membership.test.tsx +++ b/src/modules/membership/Membership.test.tsx @@ -2,8 +2,10 @@ import { fireEvent } from '@testing-library/react-native'; import React from 'react'; import { RefreshControl } from 'react-native'; +import * as api from '@/api'; +import { ApiError } from '@/errors'; import Membership from '@/modules/membership/Membership'; -import { mockMembershipTypesOnce, renderWithProviders, testUser } from '@/testUtils'; +import { renderWithProviders, testUser } from '@/testUtils'; import type { UserType } from '@/types/data'; const mockNavigate = jest.fn(); @@ -12,6 +14,10 @@ jest.mock('@react-navigation/native', () => ({ useNavigation: () => ({ navigate: mockNavigate }), })); +beforeEach(() => { + jest.clearAllMocks(); +}); + it('shows the membership pitch when logged out', async () => { const { findByText } = renderWithProviders(); @@ -22,7 +28,6 @@ it('shows the membership pitch when logged out', async () => { it('lets a logged in non-member pick a membership type and shows its price', async () => { const nonMember: UserType = { ...testUser, is_member: false, last_membership: null }; - mockMembershipTypesOnce(); const { findByText, getByText } = renderWithProviders(, { initialToken: 'test-token', initialUser: nonMember, @@ -46,7 +51,6 @@ it('lets a logged in non-member pick a membership type and shows its price', asy }); it('shows the membership proof when logged in', async () => { - mockMembershipTypesOnce(); const { findByText } = renderWithProviders(, { initialToken: 'test-token', initialUser: testUser, @@ -57,13 +61,7 @@ it('shows the membership proof when logged in', async () => { }); it('fetches the user with the stored token when only the token survives a cold start', async () => { - // The user fetch resolves first; Proof then mounts and fetches membership types - (global.fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => testUser, - }); - mockMembershipTypesOnce(); + const fetchUserData = jest.spyOn(api, 'fetchUserData').mockResolvedValue(testUser); const { findByText } = renderWithProviders(, { initialToken: 'test-token', @@ -71,20 +69,12 @@ it('fetches the user with the stored token when only the token survives a cold s expect(await findByText('MEDLEM')).toBeTruthy(); expect(await findByText('Test Testesen')).toBeTruthy(); - expect(global.fetch).toHaveBeenCalledWith( - 'https://dusken.example.test/api/me/', - expect.objectContaining({ - method: 'GET', - headers: expect.objectContaining({ Authorization: 'Token test-token' }), - }), - ); + expect(fetchUserData).toHaveBeenCalledWith('test-token'); }); it('refetches and renders updated user data on pull-to-refresh', async () => { - const fetchMock = global.fetch as jest.Mock; const nonMember: UserType = { ...testUser, is_member: false, last_membership: null }; - mockMembershipTypesOnce(); const { findByText, UNSAFE_getByType } = renderWithProviders(, { initialToken: 'test-token', initialUser: nonMember, @@ -92,25 +82,15 @@ it('refetches and renders updated user data on pull-to-refresh', async () => { expect(await findByText('Velg medlemskapstype')).toBeTruthy(); // the server now reports an active membership, bought elsewhere - fetchMock.mockResolvedValueOnce({ ok: true, status: 200, json: async () => testUser }); + const fetchUserData = jest.spyOn(api, 'fetchUserData').mockResolvedValue(testUser); fireEvent(UNSAFE_getByType(RefreshControl), 'refresh'); expect(await findByText('MEDLEM')).toBeTruthy(); - expect(fetchMock).toHaveBeenLastCalledWith( - 'https://dusken.example.test/api/me/', - expect.objectContaining({ - method: 'GET', - headers: expect.objectContaining({ Authorization: 'Token test-token' }), - }), - ); + expect(fetchUserData).toHaveBeenCalledWith('test-token'); }); it('signs out when the stored token has been revoked', async () => { - (global.fetch as jest.Mock).mockResolvedValueOnce({ - ok: false, - status: 401, - json: async () => ({ detail: 'Ugyldig token.' }), - }); + jest.spyOn(api, 'fetchUserData').mockRejectedValueOnce(new ApiError({ detail: 'Ugyldig token.' }, 401)); const { findByText } = renderWithProviders(, { initialToken: 'revoked-token', @@ -120,7 +100,6 @@ it('signs out when the stored token has been revoked', async () => { }); it('logs out and shows the logged-out membership screen again', async () => { - mockMembershipTypesOnce(); const { findByText } = renderWithProviders(, { initialToken: 'test-token', initialUser: testUser, diff --git a/src/modules/membership/Proof.test.tsx b/src/modules/membership/Proof.test.tsx index c714bcc..738570c 100644 --- a/src/modules/membership/Proof.test.tsx +++ b/src/modules/membership/Proof.test.tsx @@ -2,9 +2,10 @@ import { fireEvent, waitFor } from '@testing-library/react-native'; import * as Notifications from 'expo-notifications'; import React from 'react'; +import * as api from '@/api'; import Proof from '@/modules/membership/Proof'; -import { mockMembershipTypesOnce, renderWithProviders, testUser } from '@/testUtils'; -import type { UserType } from '@/types/data'; +import { renderWithProviders, testMembershipTypes, testUser } from '@/testUtils'; +import type { MembershipTypeOption, UserType } from '@/types/data'; // The setup-wide stripe mock resolves initPaymentSheet/presentPaymentSheet // with { error: null }; useStripe() hands out these same jest.fn instances. @@ -13,14 +14,10 @@ const { initPaymentSheet, presentPaymentSheet } = jest.requireMock('@stripe/stri presentPaymentSheet: jest.Mock; }; -const fetchMock = global.fetch as jest.Mock; - -const mockPaymentSheetResponse = () => - fetchMock.mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ paymentIntent: 'pi_secret', ephemeralKey: 'ek_secret', customer: 'cus_123' }), - }); +const mockPaymentSheet = () => + jest + .spyOn(api, 'requestStripePaymentSheet') + .mockResolvedValue({ paymentIntent: 'pi_secret', ephemeralKey: 'ek_secret', customer: 'cus_123' }); const nonMember: UserType = { ...testUser, is_member: false, last_membership: null }; @@ -30,18 +27,19 @@ const expiredMember: UserType = { last_membership: { membership_type: 'standard', end_date: '2020-01-01' }, }; -// The membership type query fires on mount, so its response must be queued -// before render; later mockResolvedValueOnce calls queue up behind it. -const renderProof = ({ user = nonMember, fetchUser = jest.fn() }: { user?: UserType; fetchUser?: () => void } = {}) => { - mockMembershipTypesOnce(); - return renderWithProviders( +const renderProof = ({ + user = nonMember, + fetchUser = jest.fn(), + membershipTypes, +}: { user?: UserType; fetchUser?: () => void; membershipTypes?: MembershipTypeOption[] | null } = {}) => + renderWithProviders( , { initialToken: 'test-token', initialUser: user, + initialMembershipTypes: membershipTypes, }, ); -}; beforeEach(() => { jest.clearAllMocks(); @@ -49,7 +47,8 @@ beforeEach(() => { }); it('disables the purchase button without showing a price while membership types load', async () => { - const { getByRole, findByText } = renderProof(); + jest.spyOn(api, 'fetchMembershipTypes').mockResolvedValue(testMembershipTypes); + const { getByRole, findByText } = renderProof({ membershipTypes: null }); // The label carries no amount until the query resolves expect(getByRole('button', { name: 'Kjøp medlemskap' })).toBeDisabled(); @@ -59,35 +58,31 @@ it('disables the purchase button without showing a price while membership types }); it('shows an error with retry instead of a price when membership types fail to load', async () => { - fetchMock.mockRejectedValueOnce(new Error('network down')); - const { findByText, getByText, getByRole } = renderProof(); + jest + .spyOn(api, 'fetchMembershipTypes') + .mockRejectedValueOnce(new Error('network down')) + .mockResolvedValue(testMembershipTypes); + const { findByText, getByText, getByRole } = renderProof({ membershipTypes: null }); expect(await findByText('Kunne ikke hente priser')).toBeTruthy(); expect(getByRole('button', { name: 'Kjøp medlemskap' })).toBeDisabled(); - // Retrying consumes the queued successful mock and recovers the real price + // Retrying hits the recovered API and gets the real price fireEvent.press(getByText('Prøv igjen')); expect(await findByText('Kjøp medlemskap (300 NOK)')).toBeTruthy(); expect(getByRole('button', { name: 'Kjøp medlemskap (300 NOK)' })).toBeEnabled(); }); it('purchases the preselected membership through the stripe payment sheet', async () => { + const requestPaymentSheet = mockPaymentSheet(); const { findByText } = renderProof(); - mockPaymentSheetResponse(); // 'Ordinær' (slug: regular) is preselected; the button enables once the // membership type query has resolved and its price is shown fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)')); expect(await findByText('Your order is confirmed!')).toBeTruthy(); - expect(fetchMock).toHaveBeenCalledWith( - 'https://dusken.example.test/api/stripe/payment-sheet/', - expect.objectContaining({ - method: 'POST', - body: JSON.stringify({ membership_type: 'regular' }), - headers: expect.objectContaining({ Authorization: 'Token test-token' }), - }), - ); + expect(requestPaymentSheet).toHaveBeenCalledWith('test-token', 'regular'); expect(initPaymentSheet).toHaveBeenCalledWith( expect.objectContaining({ paymentIntentClientSecret: 'pi_secret', @@ -103,25 +98,22 @@ it('purchases the preselected membership through the stripe payment sheet', asyn }); it('purchases the selected membership type, not the default', async () => { + const requestPaymentSheet = mockPaymentSheet(); const { findByText } = renderProof(); - mockPaymentSheetResponse(); fireEvent.press(await findByText('Student')); fireEvent.press(await findByText('Kjøp medlemskap (150 NOK)')); expect(await findByText('Your order is confirmed!')).toBeTruthy(); - expect(fetchMock).toHaveBeenCalledWith( - 'https://dusken.example.test/api/stripe/payment-sheet/', - expect.objectContaining({ body: JSON.stringify({ membership_type: 'student' }) }), - ); + expect(requestPaymentSheet).toHaveBeenCalledWith('test-token', 'student'); }); it('shows the stripe error when the payment sheet is not completed', async () => { + mockPaymentSheet(); presentPaymentSheet.mockResolvedValueOnce({ error: { code: 'Canceled', message: 'The payment flow has been canceled' }, }); const { findByText, queryByText } = renderProof(); - mockPaymentSheetResponse(); fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)')); @@ -131,12 +123,8 @@ it('shows the stripe error when the payment sheet is not completed', async () => it('shows an error and does not present the sheet when the backend rejects the purchase', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(api, 'requestStripePaymentSheet').mockRejectedValueOnce(new Error('Ugyldig medlemskapstype')); const { findByText, getByRole } = renderProof(); - fetchMock.mockResolvedValueOnce({ - ok: false, - status: 400, - json: async () => ({ detail: 'Ugyldig medlemskapstype' }), - }); fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)')); @@ -149,9 +137,9 @@ it('shows an error and does not present the sheet when the backend rejects the p it('shows an error and does not present the sheet when stripe fails to initialise it', async () => { jest.spyOn(console, 'error').mockImplementation(() => {}); + mockPaymentSheet(); initPaymentSheet.mockResolvedValueOnce({ error: { code: 'Failed', message: 'Invalid client secret' } }); const { findByText } = renderProof(); - mockPaymentSheetResponse(); fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)')); diff --git a/src/modules/membership/UserDelete.test.tsx b/src/modules/membership/UserDelete.test.tsx index e33f4d3..97b64e2 100644 --- a/src/modules/membership/UserDelete.test.tsx +++ b/src/modules/membership/UserDelete.test.tsx @@ -1,6 +1,8 @@ import { fireEvent, waitFor } from '@testing-library/react-native'; import React from 'react'; +import * as api from '@/api'; +import { ApiError } from '@/errors'; import UserDelete from '@/modules/membership/UserDelete'; import { renderWithProviders, testUser } from '@/testUtils'; @@ -10,8 +12,6 @@ jest.mock('@react-navigation/native', () => ({ useNavigation: () => ({ navigate: mockNavigate, goBack: jest.fn() }), })); -const fetchMock = global.fetch as jest.Mock; - beforeEach(() => { jest.clearAllMocks(); }); @@ -34,7 +34,7 @@ it('renders nothing and navigates back to Membership when not logged in', () => }); it('deletes the account and returns to Membership once the username is typed to confirm', async () => { - fetchMock.mockResolvedValueOnce({ ok: true, status: 204, json: async () => ({}) }); + const deleteUser = jest.spyOn(api, 'deleteUser').mockResolvedValueOnce(undefined); const { getByText, getByDisplayValue } = renderWithProviders(, { initialToken: 'test-token', initialUser: testUser, @@ -44,21 +44,13 @@ it('deletes the account and returns to Membership once the username is typed to fireEvent.press(getByText('Jeg forstår, slett kontoen min')); await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('UserStack', { screen: 'Membership' })); - expect(fetchMock).toHaveBeenCalledWith( - 'https://dusken.example.test/api/me/delete/', - expect.objectContaining({ - method: 'DELETE', - headers: expect.objectContaining({ Authorization: 'Token test-token' }), - }), - ); + expect(deleteUser).toHaveBeenCalledWith('test-token'); }); it('shows the server error when deletion fails', async () => { - fetchMock.mockResolvedValueOnce({ - ok: false, - status: 400, - json: async () => ({ detail: 'Kunne ikke slette kontoen akkurat nå' }), - }); + jest + .spyOn(api, 'deleteUser') + .mockRejectedValueOnce(new ApiError({ detail: 'Kunne ikke slette kontoen akkurat nå' }, 400)); const { getByText, getByDisplayValue, findByText } = renderWithProviders(, { initialToken: 'test-token', initialUser: testUser, diff --git a/src/modules/register/UserRegister.test.tsx b/src/modules/register/UserRegister.test.tsx index 4d2ec0b..05c71ea 100644 --- a/src/modules/register/UserRegister.test.tsx +++ b/src/modules/register/UserRegister.test.tsx @@ -2,8 +2,11 @@ import { act, fireEvent, waitFor, type RenderResult } from '@testing-library/rea import React from 'react'; import { ActivityIndicator } from 'react-native'; +import * as api from '@/api'; +import { ApiError } from '@/errors'; import UserRegister from '@/modules/register/UserRegister'; -import { renderWithProviders } from '@/testUtils'; +import { renderWithProviders, testUser } from '@/testUtils'; +import type { UserType } from '@/types/data'; // The screen is pushed on top of Login and closes itself with goBack() once // registration succeeds; stub the navigation object so we can assert on that. @@ -13,11 +16,6 @@ jest.mock('@react-navigation/native', () => { return { ...actual, useNavigation: () => ({ goBack: mockGoBack }) }; }); -const fetchMock = global.fetch as jest.Mock; - -const mockResponseOnce = (json: unknown, ok = true) => - fetchMock.mockResolvedValueOnce({ ok, status: ok ? 200 : 400, json: async () => json }); - const validFormValues: Record = { Fornavn: 'Grisen', Etternavn: 'Testesen', @@ -45,18 +43,21 @@ it('renders the registration form', () => { }); it('disables the submit button while the form is incomplete', () => { + const registerUser = jest.spyOn(api, 'registerUser'); const { getByText, getByRole, UNSAFE_queryByType } = renderWithProviders(); expect(getByRole('button', { name: 'Registrer meg' })).toBeDisabled(); fireEvent.press(getByText('Registrer meg')); - expect(fetchMock).not.toHaveBeenCalled(); + expect(registerUser).not.toHaveBeenCalled(); expect(UNSAFE_queryByType(ActivityIndicator)).toBeNull(); }); it('submits the form, shows a spinner while pending and navigates back on success', async () => { - let resolveRegister!: (response: { ok: boolean; status: number; json: () => Promise }) => void; - fetchMock.mockImplementationOnce(() => new Promise((resolve) => (resolveRegister = resolve))); + let resolveRegister!: (result: { user: UserType; token: string }) => void; + const registerUser = jest + .spyOn(api, 'registerUser') + .mockImplementationOnce(() => new Promise((resolve) => (resolveRegister = resolve))); const { getByText, getByRole, getByLabelText, UNSAFE_getByType } = renderWithProviders(); fillForm(getByLabelText); @@ -65,34 +66,28 @@ it('submits the form, shows a spinner while pending and navigates back on succes // the spinner shows while the request is in flight await waitFor(() => expect(UNSAFE_getByType(ActivityIndicator)).toBeTruthy()); - expect(fetchMock).toHaveBeenCalledWith( - 'https://dusken.example.test/api/user/register/', + expect(registerUser).toHaveBeenCalledWith( expect.objectContaining({ - method: 'POST', - body: JSON.stringify({ - first_name: 'Grisen', - last_name: 'Testesen', - email: 'grisen@example.test', - phone_number: '+4712345678', - password: 'hunter2', - }), + first_name: 'Grisen', + last_name: 'Testesen', + email: 'grisen@example.test', + phone_number: '+4712345678', + password: 'hunter2', }), ); expect(mockGoBack).not.toHaveBeenCalled(); await act(async () => { - resolveRegister({ - ok: true, - status: 200, - json: async () => ({ username: 'grisen', email: 'grisen@example.test', auth_token: 'token-456' }), - }); + resolveRegister({ user: testUser, token: 'token-456' }); }); await waitFor(() => expect(mockGoBack).toHaveBeenCalled()); }); it('shows server field errors on the matching form field', async () => { - mockResponseOnce({ email: ['Denne e-postadressen er allerede i bruk'] }, false); + jest + .spyOn(api, 'registerUser') + .mockRejectedValueOnce(new ApiError({ email: ['Denne e-postadressen er allerede i bruk'] }, 400)); const { getByText, getByRole, getByLabelText, findByText } = renderWithProviders(); fillForm(getByLabelText); @@ -103,7 +98,8 @@ it('shows server field errors on the matching form field', async () => { expect(mockGoBack).not.toHaveBeenCalled(); }); -it('rejects an invalid e-mail address without hitting the network', async () => { +it('rejects an invalid e-mail address without submitting', async () => { + const registerUser = jest.spyOn(api, 'registerUser'); const { getByLabelText, findByText } = renderWithProviders(); fillForm(getByLabelText, { 'E-post': 'not-an-email' }); @@ -111,5 +107,5 @@ it('rejects an invalid e-mail address without hitting the network', async () => fireEvent(getByLabelText('Passord'), 'submitEditing'); expect(await findByText('er ikke en gyldig e-post')).toBeTruthy(); - expect(fetchMock).not.toHaveBeenCalled(); + expect(registerUser).not.toHaveBeenCalled(); }); diff --git a/src/testUtils.tsx b/src/testUtils.tsx index 844de34..74dcb89 100644 --- a/src/testUtils.tsx +++ b/src/testUtils.tsx @@ -22,27 +22,21 @@ export const testUser: UserType = { }, }; -/** The purchase options offered in the app, as served by /api/graphql/. */ +/** The purchase options offered in the app. */ export const testMembershipTypes: MembershipTypeOption[] = [ { slug: 'student', price: 15000 }, { slug: 'regular', price: 30000 }, { slug: 'support', price: 50000 }, ]; -/** Queue a successful /api/graphql/ membership types response on the fetch mock. */ -export function mockMembershipTypesOnce(membershipTypes: MembershipTypeOption[] = testMembershipTypes) { - (global.fetch as jest.Mock).mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ data: { membershipTypes } }), - }); -} - interface RenderWithProvidersOptions { /** Auth token the app has stored; being set means the user is logged in. */ initialToken?: string | null; /** Already-fetched user data, as after a completed /api/me/ request. */ initialUser?: UserType | null; + /** Membership purchase options already in the cache; null leaves the query + * empty for tests that exercise its loading and error states. */ + initialMembershipTypes?: MembershipTypeOption[] | null; } /** @@ -51,7 +45,11 @@ interface RenderWithProvidersOptions { */ export function renderWithProviders( ui: ReactElement, - { initialToken = null, initialUser = null }: RenderWithProvidersOptions = {}, + { + initialToken = null, + initialUser = null, + initialMembershipTypes = testMembershipTypes, + }: RenderWithProvidersOptions = {}, ) { const queryClient = new QueryClient({ defaultOptions: { @@ -64,6 +62,9 @@ export function renderWithProviders( if (initialUser) { queryClient.setQueryData(queryKeys.user, initialUser); } + if (initialMembershipTypes) { + queryClient.setQueryData(queryKeys.membershipTypes, initialMembershipTypes); + } const result = render(