6 files changed
+94
-144
No files matched your search
@@ -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(<Login />);
|
||||
|
||||
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(<Login />);
|
||||
|
||||
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(<Login />);
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -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(<Membership />);
|
||||
|
||||
@@ -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(<Membership />, {
|
||||
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(<Membership />, {
|
||||
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(<Membership />, {
|
||||
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(<Membership />, {
|
||||
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(<Membership />, {
|
||||
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(<Membership />, {
|
||||
initialToken: 'test-token',
|
||||
initialUser: testUser,
|
||||
|
||||
@@ -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(
|
||||
<Proof onLogoutPress={jest.fn()} fetchUser={fetchUser} isRefetching={false} onDeletePress={jest.fn()} />,
|
||||
{
|
||||
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)'));
|
||||
|
||||
|
||||
@@ -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(<UserDelete />, {
|
||||
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(<UserDelete />, {
|
||||
initialToken: 'test-token',
|
||||
initialUser: testUser,
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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(<UserRegister />);
|
||||
|
||||
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<unknown> }) => 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(<UserRegister />);
|
||||
|
||||
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(<UserRegister />);
|
||||
|
||||
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(<UserRegister />);
|
||||
|
||||
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();
|
||||
});
|
||||
+12
-11
@@ -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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
Reference in new issue
Block a user