Bump react-hook-form, add more covrage for login and register forms
This commit is contained in:
5 files changed
+76
-7
No files matched your search
Generated
+4
-4
@@ -31,7 +31,7 @@
|
||||
"expo-splash-screen": "~57.0.8",
|
||||
"expo-status-bar": "~57.0.1",
|
||||
"react": "19.2.3",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-hook-form": "^7.87.0",
|
||||
"react-native": "0.86.3",
|
||||
"react-native-fast-confetti": "^2.0.2",
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
@@ -13663,9 +13663,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.83.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.83.0.tgz",
|
||||
"integrity": "sha512-AXt8cMCmx5a7u4uvpb2uRFVrWQhllI4pV+LSykxIac/hjt44TnQkmX9BKuQi2i+LDC62esmiLpilkav+kjVf/A==",
|
||||
"version": "7.87.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.87.0.tgz",
|
||||
"integrity": "sha512-zhFzWvLxNHH+8839OnZcUxgMZw88ah2jZWDWvKWgF3Tpbnd0vKL+dlcuU3nZVWESZQjd81EW8K+wU+cYfYAc0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@
|
||||
"expo-splash-screen": "~57.0.8",
|
||||
"expo-status-bar": "~57.0.1",
|
||||
"react": "19.2.3",
|
||||
"react-hook-form": "^7.52.1",
|
||||
"react-hook-form": "^7.87.0",
|
||||
"react-native": "0.86.3",
|
||||
"react-native-fast-confetti": "^2.0.2",
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { fireEvent, waitFor, type RenderResult } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
import * as api from '@/api';
|
||||
import { ApiError } from '@/errors';
|
||||
import Login from '@/modules/login/Login';
|
||||
import { renderWithProviders } from '@/testUtils';
|
||||
import { focusedFieldLabel, renderWithProviders } from '@/testUtils';
|
||||
|
||||
// Login is pushed on top of Membership and closes itself with goBack() once
|
||||
// authentication succeeds; stub the navigation object so we can assert on that.
|
||||
@@ -35,6 +36,24 @@ it('renders the login form', () => {
|
||||
expect(getByText('Glemt passord?')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('moves focus to the password field when the username is submitted from the keyboard', async () => {
|
||||
const { getByLabelText } = renderWithProviders(<Login />);
|
||||
|
||||
fireEvent(getByLabelText('E-post / Brukernavn'), 'submitEditing');
|
||||
|
||||
// react-hook-form's setFocus defers the focus call to the next tick
|
||||
await waitFor(() => expect(focusedFieldLabel()).toBe('Passord'));
|
||||
});
|
||||
|
||||
it('opens the forgot password page in the browser', () => {
|
||||
const { getByText } = renderWithProviders(<Login />);
|
||||
|
||||
fireEvent.press(getByText('Glemt passord?'));
|
||||
|
||||
// Not one of our own domains in the test env, so no UTM tagging (covered in utils.test.ts)
|
||||
expect(Linking.openURL).toHaveBeenCalledWith('https://dusken.example.test/auth/password_reset/');
|
||||
});
|
||||
|
||||
it('logs in against the API and returns to the previous screen', async () => {
|
||||
const login = jest.spyOn(api, 'login').mockResolvedValue({ token: 'token-123' });
|
||||
const { getByText, getByLabelText } = renderWithProviders(<Login />);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ActivityIndicator } from 'react-native';
|
||||
import * as api from '@/api';
|
||||
import { ApiError } from '@/errors';
|
||||
import UserRegister from '@/modules/register/UserRegister';
|
||||
import { renderWithProviders, testUser } from '@/testUtils';
|
||||
import { focusedFieldLabel, renderWithProviders, testUser } from '@/testUtils';
|
||||
import type { UserType } from '@/types/data';
|
||||
|
||||
// The screen is pushed on top of Login and closes itself with goBack() once
|
||||
@@ -34,6 +34,10 @@ beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('renders the registration form', () => {
|
||||
const { getByText } = renderWithProviders(<UserRegister />);
|
||||
|
||||
@@ -98,6 +102,47 @@ it('shows server field errors on the matching form field', async () => {
|
||||
expect(mockGoBack).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('moves focus down the form when a field is submitted from the keyboard', async () => {
|
||||
const { getByLabelText } = renderWithProviders(<UserRegister />);
|
||||
const labels = ['Fornavn', 'Etternavn', 'E-post', 'Mobilnummer', 'Passord'];
|
||||
|
||||
for (const [i, label] of labels.slice(0, -1).entries()) {
|
||||
fireEvent(getByLabelText(label), 'submitEditing');
|
||||
// react-hook-form's setFocus defers the focus call to the next tick
|
||||
await waitFor(() => expect(focusedFieldLabel()).toBe(labels[i + 1]));
|
||||
}
|
||||
});
|
||||
|
||||
it('shows a generic error when registration fails on the network', async () => {
|
||||
jest.spyOn(api, 'registerUser').mockRejectedValueOnce(new Error('network down'));
|
||||
const { getByText, getByRole, getByLabelText, findByText } = renderWithProviders(<UserRegister />);
|
||||
|
||||
fillForm(getByLabelText);
|
||||
await waitFor(() => expect(getByRole('button', { name: 'Registrer meg' })).toBeEnabled());
|
||||
fireEvent.press(getByText('Registrer meg'));
|
||||
|
||||
expect(await findByText('Kunne ikke sende inn skjemaet! Prøv igjen senere...')).toBeTruthy();
|
||||
expect(mockGoBack).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('warns instead of submitting when the keyboard submits before validation has caught up', async () => {
|
||||
jest.useFakeTimers();
|
||||
const registerUser = jest.spyOn(api, 'registerUser');
|
||||
const { getByLabelText, findByText, queryByText } = renderWithProviders(<UserRegister />);
|
||||
|
||||
// Filling in and submitting in the same tick reaches the submit handler while isValid still reflects the empty form
|
||||
fillForm(getByLabelText);
|
||||
fireEvent(getByLabelText('Passord'), 'submitEditing');
|
||||
|
||||
const message = 'Noen av feltene er ikke fylt ut riktig';
|
||||
expect(await findByText(message)).toBeTruthy();
|
||||
expect(registerUser).not.toHaveBeenCalled();
|
||||
|
||||
// The toast dismisses itself after five seconds
|
||||
act(() => jest.advanceTimersByTime(5000));
|
||||
expect(queryByText(message)).toBeNull();
|
||||
});
|
||||
|
||||
it('rejects an invalid e-mail address without submitting', async () => {
|
||||
const registerUser = jest.spyOn(api, 'registerUser');
|
||||
const { getByLabelText, findByText } = renderWithProviders(<UserRegister />);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { TextInput } from 'react-native';
|
||||
|
||||
import { AuthProvider } from '@/auth/AuthContext';
|
||||
import { queryKeys } from '@/queries/queryKeys';
|
||||
@@ -29,6 +30,10 @@ export const testMembershipTypes: MembershipTypeOption[] = [
|
||||
{ slug: 'support', price: 50000 },
|
||||
];
|
||||
|
||||
/** Label of the field focused last; the mocked TextInput shares a single focus() mock across instances. */
|
||||
export const focusedFieldLabel = () =>
|
||||
(TextInput.prototype.focus as jest.Mock).mock.contexts.at(-1)?.props.accessibilityLabel;
|
||||
|
||||
interface RenderWithProvidersOptions {
|
||||
/** Auth token the app has stored; being set means the user is logged in. */
|
||||
initialToken?: string | null;
|
||||
|
||||
Reference in new issue
Block a user