Improve test coverage and fix various issues
This commit is contained in:
8 files changed
+484
-6
No files matched your search
+200
-1
@@ -1,11 +1,26 @@
|
||||
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
|
||||
import { thunk } from 'redux-thunk';
|
||||
|
||||
import { requestLogin, requestUserData } from '@/api';
|
||||
import {
|
||||
requestDeleteUser,
|
||||
requestLogin,
|
||||
requestRegisterUser,
|
||||
requestStripePaymentSheet,
|
||||
requestUserData,
|
||||
} from '@/api';
|
||||
import rootReducer from '@/reducers';
|
||||
|
||||
const createTestStore = () => createStore(rootReducer, undefined, applyMiddleware(thunk));
|
||||
|
||||
const fetchMock = global.fetch as jest.Mock;
|
||||
|
||||
const mockResponseOnce = (json: unknown, ok = true) =>
|
||||
fetchMock.mockResolvedValueOnce({ ok, status: ok ? 200 : 400, json: async () => json });
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
});
|
||||
|
||||
describe('dev fake users (__DEV__ only)', () => {
|
||||
it.each([
|
||||
['nonmember', false],
|
||||
@@ -22,3 +37,187 @@ describe('dev fake users (__DEV__ only)', () => {
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestLogin', () => {
|
||||
it('posts credentials and stores the token on success', async () => {
|
||||
const store = createTestStore();
|
||||
mockResponseOnce({ token: 'token-123' });
|
||||
|
||||
await store.dispatch(requestLogin('grisen', 'hunter2'));
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://dusken.example.test/auth/obtain-token/',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username: 'grisen', password: 'hunter2' }),
|
||||
}),
|
||||
);
|
||||
expect(store.getState()).toMatchObject({
|
||||
isAuthenticated: true,
|
||||
isLoggingIn: false,
|
||||
userToken: 'token-123',
|
||||
loginError: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('stores the API error body on a rejected login', async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const store = createTestStore();
|
||||
const apiError = { non_field_errors: ['Kan ikke logge inn med disse opplysningene'] };
|
||||
mockResponseOnce(apiError, false);
|
||||
|
||||
await store.dispatch(requestLogin('grisen', 'wrong-password'));
|
||||
|
||||
expect(store.getState()).toMatchObject({
|
||||
isAuthenticated: false,
|
||||
isLoggingIn: false,
|
||||
userToken: null,
|
||||
loginError: apiError,
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to a generic error message when the API returns an empty body', async () => {
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const store = createTestStore();
|
||||
mockResponseOnce(null, false);
|
||||
|
||||
await store.dispatch(requestLogin('grisen', 'hunter2'));
|
||||
|
||||
expect(store.getState().loginError).toEqual({ non_field_errors: ['Kunne ikke logge inn, prøv igjen'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestUserData', () => {
|
||||
it('fetches /api/me/ with the auth token and stores the user', async () => {
|
||||
const store = createTestStore();
|
||||
const user = { username: 'grisen', email: 'grisen@example.test', is_member: true };
|
||||
mockResponseOnce(user);
|
||||
|
||||
await store.dispatch(requestUserData('token-123'));
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://dusken.example.test/api/me/',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: expect.objectContaining({ Authorization: 'Token token-123' }),
|
||||
}),
|
||||
);
|
||||
expect(store.getState()).toMatchObject({ isFetchingUserData: false, user });
|
||||
});
|
||||
|
||||
it('stores the API error body when fetching user data fails', async () => {
|
||||
const store = createTestStore();
|
||||
const apiError = { detail: 'Ugyldig token.' };
|
||||
mockResponseOnce(apiError, false);
|
||||
|
||||
await store.dispatch(requestUserData('expired-token'));
|
||||
|
||||
expect(store.getState()).toMatchObject({ isFetchingUserData: false, user: null, userError: apiError });
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestRegisterUser', () => {
|
||||
const formValues = {
|
||||
first_name: 'Grisen',
|
||||
last_name: 'Testersen',
|
||||
email: 'grisen@example.test',
|
||||
phone_number: '+4712345678',
|
||||
password: 'hunter2',
|
||||
};
|
||||
|
||||
it('authenticates with the returned auth_token and stores the user without it', async () => {
|
||||
const store = createTestStore();
|
||||
mockResponseOnce({ username: 'grisen', email: 'grisen@example.test', auth_token: 'token-456' });
|
||||
|
||||
await store.dispatch(requestRegisterUser(formValues));
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://dusken.example.test/api/user/register/',
|
||||
expect.objectContaining({ method: 'POST', body: JSON.stringify(formValues) }),
|
||||
);
|
||||
expect(store.getState()).toMatchObject({
|
||||
isRegisteringUser: false,
|
||||
isAuthenticated: true,
|
||||
userToken: 'token-456',
|
||||
user: { username: 'grisen', email: 'grisen@example.test' },
|
||||
});
|
||||
expect(store.getState().user).not.toHaveProperty('auth_token');
|
||||
});
|
||||
|
||||
it('stores field errors from the API when registration fails', async () => {
|
||||
const store = createTestStore();
|
||||
const apiError = { email: ['Denne e-postadressen er allerede i bruk'] };
|
||||
mockResponseOnce(apiError, false);
|
||||
|
||||
await store.dispatch(requestRegisterUser(formValues));
|
||||
|
||||
expect(store.getState()).toMatchObject({
|
||||
isRegisteringUser: false,
|
||||
isAuthenticated: false,
|
||||
registerError: apiError,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestDeleteUser', () => {
|
||||
it('resets the store to its initial state on success', async () => {
|
||||
const store = createTestStore();
|
||||
// Authenticate first (via the fake-user shortcut) so the reset is observable
|
||||
await store.dispatch(requestLogin('member', 'any-password'));
|
||||
expect(store.getState().isAuthenticated).toBe(true);
|
||||
|
||||
mockResponseOnce({});
|
||||
await store.dispatch(requestDeleteUser('token-123'));
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://dusken.example.test/api/me/delete/',
|
||||
expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
headers: expect.objectContaining({ Authorization: 'Token token-123' }),
|
||||
}),
|
||||
);
|
||||
expect(store.getState()).toMatchObject({ isAuthenticated: false, userToken: null, user: null });
|
||||
});
|
||||
|
||||
it('stores the API error body when deletion fails', async () => {
|
||||
const store = createTestStore();
|
||||
const apiError = { detail: 'Kan ikke slette bruker med aktivt medlemskap' };
|
||||
mockResponseOnce(apiError, false);
|
||||
|
||||
await store.dispatch(requestDeleteUser('token-123'));
|
||||
|
||||
expect(store.getState()).toMatchObject({ isDeletingUser: false, deleteUserError: apiError });
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestStripePaymentSheet', () => {
|
||||
it('posts the membership type and returns the payment sheet parameters', async () => {
|
||||
const sheet = { paymentIntent: 'pi_secret', ephemeralKey: 'ek_secret', customer: 'cus_123' };
|
||||
mockResponseOnce(sheet);
|
||||
|
||||
await expect(requestStripePaymentSheet('token-123', 'standard')).resolves.toEqual(sheet);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://dusken.example.test/api/stripe/payment-sheet/',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ membership_type: 'standard' }),
|
||||
headers: expect.objectContaining({ Authorization: 'Token token-123' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects with the API detail message on failure', async () => {
|
||||
mockResponseOnce({ detail: 'Ugyldig medlemskapstype' }, false);
|
||||
|
||||
await expect(requestStripePaymentSheet('token-123', 'bogus')).rejects.toThrow('Ugyldig medlemskapstype');
|
||||
});
|
||||
|
||||
it('rejects with a generic message when the API gives no detail', async () => {
|
||||
mockResponseOnce({}, false);
|
||||
|
||||
await expect(requestStripePaymentSheet('token-123', 'standard')).rejects.toThrow(
|
||||
'Kunne ikke belaste bankkortet, prøv igjen',
|
||||
);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -212,7 +212,7 @@ export async function requestStripePaymentSheet(authToken: string, membershipTyp
|
||||
return data;
|
||||
},
|
||||
(data) => {
|
||||
return data.detail || 'Kunne ikke belaste bankkortet, prøv igjen';
|
||||
throw new Error(data?.detail || 'Kunne ikke belaste bankkortet, prøv igjen');
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,7 @@ const FormInput = ({
|
||||
<TextField
|
||||
ref={ref}
|
||||
label={<Text style={theme.text}>{label}</Text>}
|
||||
accessibilityLabel={label}
|
||||
returnKeyType={returnKeyType}
|
||||
onChangeText={onChange}
|
||||
onBlur={onBlur}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { fireEvent, waitFor } from '@testing-library/react-native';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import React from 'react';
|
||||
|
||||
import Proof from '@/modules/membership/Proof';
|
||||
import { renderWithProviders, testUser } from '@/testUtils';
|
||||
import type { UserType } from '@/types/data';
|
||||
|
||||
// The setup-wide stripe mock resolves initPaymentSheet/presentPaymentSheet
|
||||
// with { error: null }; useStripe() hands out these same jest.fn instances.
|
||||
const { initPaymentSheet, presentPaymentSheet } = jest.requireMock('@stripe/stripe-react-native') as {
|
||||
initPaymentSheet: jest.Mock;
|
||||
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' }),
|
||||
});
|
||||
|
||||
// Must match PROOF_QUERY in useProof.tsx
|
||||
const PROOF_QUERY = gql`
|
||||
{
|
||||
membershipTypes {
|
||||
slug
|
||||
price
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const apolloMocks = [
|
||||
{
|
||||
request: { query: PROOF_QUERY },
|
||||
result: {
|
||||
data: {
|
||||
membershipTypes: [
|
||||
{ __typename: 'MembershipType', slug: 'student', price: 15000 },
|
||||
{ __typename: 'MembershipType', slug: 'regular', price: 30000 },
|
||||
{ __typename: 'MembershipType', slug: 'support', price: 50000 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const nonMember: UserType = { ...testUser, is_member: false, last_membership: null };
|
||||
|
||||
const expiredMember: UserType = {
|
||||
...testUser,
|
||||
is_member: false,
|
||||
last_membership: { membership_type: 'standard', end_date: '2020-01-01' },
|
||||
};
|
||||
|
||||
const renderProof = ({ user = nonMember, fetchUser = jest.fn() }: { user?: UserType; fetchUser?: () => void } = {}) =>
|
||||
renderWithProviders(
|
||||
<Proof onLogoutPress={jest.fn()} fetchUser={fetchUser} isFetchingUserData={false} onDeletePress={jest.fn()} />,
|
||||
{
|
||||
preloadedState: { isAuthenticated: true, userToken: 'test-token', user },
|
||||
apolloMocks,
|
||||
},
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(Notifications.getPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'undetermined' });
|
||||
});
|
||||
|
||||
it('purchases the preselected membership through the stripe payment sheet', async () => {
|
||||
mockPaymentSheetResponse();
|
||||
const { findByText } = renderProof();
|
||||
|
||||
// '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(initPaymentSheet).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
paymentIntentClientSecret: 'pi_secret',
|
||||
customerEphemeralKeySecret: 'ek_secret',
|
||||
customerId: 'cus_123',
|
||||
defaultBillingDetails: expect.objectContaining({
|
||||
email: testUser.email,
|
||||
name: 'Test Testesen',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(presentPaymentSheet).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('purchases the selected membership type, not the default', async () => {
|
||||
mockPaymentSheetResponse();
|
||||
const { findByText, getByText } = renderProof();
|
||||
|
||||
fireEvent.press(await findByText('Student'));
|
||||
fireEvent.press(getByText('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' }) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the stripe error when the payment sheet is not completed', async () => {
|
||||
mockPaymentSheetResponse();
|
||||
presentPaymentSheet.mockResolvedValueOnce({
|
||||
error: { code: 'Canceled', message: 'The payment flow has been canceled' },
|
||||
});
|
||||
const { findByText, queryByText } = renderProof();
|
||||
|
||||
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
||||
|
||||
expect(await findByText('Error code: Canceled The payment flow has been canceled')).toBeTruthy();
|
||||
expect(queryByText('Your order is confirmed!')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows an error and does not present the sheet when the backend rejects the purchase', async () => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 400,
|
||||
json: async () => ({ detail: 'Ugyldig medlemskapstype' }),
|
||||
});
|
||||
const { findByText, getByRole } = renderProof();
|
||||
|
||||
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
||||
|
||||
expect(await findByText('Oops! Could not initialise payment')).toBeTruthy();
|
||||
expect(initPaymentSheet).not.toHaveBeenCalled();
|
||||
expect(presentPaymentSheet).not.toHaveBeenCalled();
|
||||
// The purchase button recovers so the user can retry
|
||||
expect(getByRole('button', { name: 'Kjøp medlemskap (300 NOK)' })).toBeEnabled();
|
||||
});
|
||||
|
||||
it('shows an error and does not present the sheet when stripe fails to initialise it', async () => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
mockPaymentSheetResponse();
|
||||
initPaymentSheet.mockResolvedValueOnce({ error: { code: 'Failed', message: 'Invalid client secret' } });
|
||||
const { findByText } = renderProof();
|
||||
|
||||
fireEvent.press(await findByText('Kjøp medlemskap (300 NOK)'));
|
||||
|
||||
expect(await findByText('Oops! Could not initialise payment')).toBeTruthy();
|
||||
expect(presentPaymentSheet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refetches user data when the stored membership has expired', async () => {
|
||||
const fetchUser = jest.fn();
|
||||
renderProof({ user: expiredMember, fetchUser });
|
||||
|
||||
await waitFor(() => expect(fetchUser).toHaveBeenCalled());
|
||||
// Notification permission was never granted, so nothing is scheduled
|
||||
expect(Notifications.scheduleNotificationAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancels stale expiry notifications when permission is granted, without scheduling one in the past', async () => {
|
||||
(Notifications.getPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted' });
|
||||
const fetchUser = jest.fn();
|
||||
renderProof({ user: expiredMember, fetchUser });
|
||||
|
||||
// Once the granted permission resolves, the expiry effect runs the cleanup
|
||||
await waitFor(() => expect(Notifications.cancelAllScheduledNotificationsAsync).toHaveBeenCalled());
|
||||
expect(fetchUser).toHaveBeenCalled();
|
||||
// The expiry date is in the past, so no new notification is scheduled
|
||||
expect(Notifications.scheduleNotificationAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -38,11 +38,14 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
)?.price ?? 25000;
|
||||
|
||||
const openPaymentSheet = async () => {
|
||||
setPaymentLoading(true);
|
||||
try {
|
||||
await initializePaymentSheet();
|
||||
} catch (err) {
|
||||
console.error('🔥', err);
|
||||
setToast({ message: 'Oops! Could not initialise payment', visible: true, preset: 'failure' });
|
||||
setPaymentLoading(false);
|
||||
return;
|
||||
}
|
||||
const { error } = await presentPaymentSheet();
|
||||
const message = error ? `Error code: ${error.code} ${error.message}` : 'Your order is confirmed!';
|
||||
@@ -51,7 +54,6 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
};
|
||||
|
||||
const initializePaymentSheet = async () => {
|
||||
setPaymentLoading(true);
|
||||
const { paymentIntent, ephemeralKey, customer } = await requestStripePaymentSheet(
|
||||
userToken as string,
|
||||
membershipTypeSlug,
|
||||
@@ -71,8 +73,8 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
},
|
||||
returnURL: `${Constants.expoConfig?.scheme}://stripe-redirect`,
|
||||
});
|
||||
if (!error) {
|
||||
setPaymentLoading(true);
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,40 @@
|
||||
import { fireEvent, waitFor, type RenderResult } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import UserRegister from '@/modules/register/UserRegister';
|
||||
import { renderWithProviders } from '@/testUtils';
|
||||
|
||||
// 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.
|
||||
const mockGoBack = jest.fn();
|
||||
jest.mock('@react-navigation/native', () => {
|
||||
const actual = jest.requireActual('@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<string, string> = {
|
||||
Fornavn: 'Grisen',
|
||||
Etternavn: 'Testesen',
|
||||
'E-post': 'grisen@example.test',
|
||||
Mobilnummer: '+4712345678',
|
||||
Passord: 'hunter2',
|
||||
};
|
||||
|
||||
const fillForm = (getByLabelText: RenderResult['getByLabelText'], overrides: Record<string, string> = {}) => {
|
||||
Object.entries({ ...validFormValues, ...overrides }).forEach(([label, value]) => {
|
||||
fireEvent.changeText(getByLabelText(label), value);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the registration form', () => {
|
||||
const { getByText } = renderWithProviders(<UserRegister />);
|
||||
|
||||
@@ -10,3 +42,63 @@ it('renders the registration form', () => {
|
||||
expect(getByText('E-post')).toBeTruthy();
|
||||
expect(getByText('Registrer meg')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('disables the submit button while the form is incomplete', () => {
|
||||
const { getByText, getByRole, store } = renderWithProviders(<UserRegister />);
|
||||
|
||||
expect(getByRole('button', { name: 'Registrer meg' })).toBeDisabled();
|
||||
fireEvent.press(getByText('Registrer meg'));
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(store.getState().isRegisteringUser).toBe(false);
|
||||
});
|
||||
|
||||
it('submits the form, authenticates and navigates back on success', async () => {
|
||||
mockResponseOnce({ username: 'grisen', email: 'grisen@example.test', auth_token: 'token-456' });
|
||||
const { getByText, getByRole, getByLabelText, store } = renderWithProviders(<UserRegister />);
|
||||
|
||||
fillForm(getByLabelText);
|
||||
await waitFor(() => expect(getByRole('button', { name: 'Registrer meg' })).toBeEnabled());
|
||||
fireEvent.press(getByText('Registrer meg'));
|
||||
|
||||
await waitFor(() => expect(store.getState().isAuthenticated).toBe(true));
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'https://dusken.example.test/api/user/register/',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
first_name: 'Grisen',
|
||||
last_name: 'Testesen',
|
||||
email: 'grisen@example.test',
|
||||
phone_number: '+4712345678',
|
||||
password: 'hunter2',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(store.getState().userToken).toBe('token-456');
|
||||
expect(mockGoBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows server field errors on the matching form field', async () => {
|
||||
mockResponseOnce({ email: ['Denne e-postadressen er allerede i bruk'] }, false);
|
||||
const { getByText, getByRole, getByLabelText, findByText, store } = renderWithProviders(<UserRegister />);
|
||||
|
||||
fillForm(getByLabelText);
|
||||
await waitFor(() => expect(getByRole('button', { name: 'Registrer meg' })).toBeEnabled());
|
||||
fireEvent.press(getByText('Registrer meg'));
|
||||
|
||||
expect(await findByText('Denne e-postadressen er allerede i bruk')).toBeTruthy();
|
||||
expect(store.getState().isAuthenticated).toBe(false);
|
||||
expect(mockGoBack).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects an invalid e-mail address without hitting the network', async () => {
|
||||
const { getByLabelText, findByText } = renderWithProviders(<UserRegister />);
|
||||
|
||||
fillForm(getByLabelText, { 'E-post': 'not-an-email' });
|
||||
// Enter on the last field submits too, bypassing the disabled button
|
||||
fireEvent(getByLabelText('Passord'), 'submitEditing');
|
||||
|
||||
expect(await findByText('er ikke en gyldig e-post')).toBeTruthy();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -64,8 +64,8 @@ const useUserRegister = () => {
|
||||
}, [isAuthenticatedPrev, isAuthenticated, navigation]);
|
||||
|
||||
useEffect(() => {
|
||||
clearErrors();
|
||||
if (!isSubmitted || !registerError) return;
|
||||
clearErrors();
|
||||
Object.keys(registerError).forEach((key) => {
|
||||
const errorMessages = registerError[key];
|
||||
if (!errorMessages || !Array.isArray(errorMessages)) {
|
||||
|
||||
@@ -9,6 +9,10 @@ const persistConfig = {
|
||||
key: 'root',
|
||||
storage: AsyncStorage,
|
||||
blacklist: ['isLoggingIn', 'isFetchingUserData', 'isRegisteringUser', 'loginError'],
|
||||
// redux-persist never clears its 5s rehydration-watchdog timer, which keeps
|
||||
// Jest workers alive after tests finish; 0 disables it. The mocked
|
||||
// AsyncStorage rehydrates instantly, so the watchdog protects nothing there.
|
||||
timeout: process.env.NODE_ENV === 'test' ? 0 : undefined,
|
||||
};
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||
|
||||
Reference in new issue
Block a user