diff --git a/jest.setup.js b/jest.setup.js
index becc0d6..f0120d5 100644
--- a/jest.setup.js
+++ b/jest.setup.js
@@ -29,6 +29,12 @@ jest.mock('@react-native-async-storage/async-storage', () =>
jest.mock('@stripe/stripe-react-native', () => require('@stripe/stripe-react-native/jest/mock.js'));
+// The preset's RefreshControl mock drops its props, so onRefresh and testID never reach the host tree
+jest.mock('react-native/Libraries/Components/RefreshControl/RefreshControl', () => {
+ const React = require('react');
+ return { __esModule: true, default: (props) => React.createElement('RCTRefreshControl', props) };
+});
+
// Reanimated (even its mock) initializes the native side of worklets at import
// time; substitute the mock that ships with react-native-worklets.
jest.mock('react-native-worklets', () => require('react-native-worklets/src/mock'));
diff --git a/package-lock.json b/package-lock.json
index cf09b05..b2d1c82 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
},
"devDependencies": {
"@babel/core": "^7.24.9",
- "@testing-library/react-native": "^13.3.3",
+ "@testing-library/react-native": "^14.0.1",
"@types/jest": "^29.5.14",
"@types/react": "~19.2.10",
"eslint": "^9.39.5",
@@ -54,7 +54,7 @@
"jest": "^29.7.0",
"jest-expo": "~57.0.5",
"prettier": "^3.9.6",
- "react-test-renderer": "19.2.3",
+ "test-renderer": "1.2",
"typescript": "~6.0.3"
}
},
@@ -4551,25 +4551,25 @@
}
},
"node_modules/@testing-library/react-native": {
- "version": "13.3.3",
- "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.3.3.tgz",
- "integrity": "sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==",
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-14.0.1.tgz",
+ "integrity": "sha512-2r2e2y8SkUNRWKRXlUGqDYunB+AkbdXUPn49Bs5rpv9oxRb0K3USBVugbPbAKJjfj4w5Ba91NJ8LcQCZyopUZA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "jest-matcher-utils": "^30.0.5",
+ "jest-matcher-utils": "^30.4.1",
"picocolors": "^1.1.1",
- "pretty-format": "^30.0.5",
+ "pretty-format": "^30.4.1",
"redent": "^3.0.0"
},
"engines": {
- "node": ">=18"
+ "node": "^22.13.0 || >=24"
},
"peerDependencies": {
"jest": ">=29.0.0",
- "react": ">=18.2.0",
- "react-native": ">=0.71",
- "react-test-renderer": ">=18.2.0"
+ "react": ">=19.0.0",
+ "react-native": ">=0.78",
+ "test-renderer": "^1.0.0"
},
"peerDependenciesMeta": {
"jest": {
@@ -4780,6 +4780,16 @@
"csstype": "^3.2.2"
}
},
+ "node_modules/@types/react-reconciler": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.33.0.tgz",
+ "integrity": "sha512-HZOXsKT0tGI9LlUw2LuedXsVeB88wFa536vVL0M6vE8zN63nI+sSr1ByxmPToP5K5bukaVscyeCJcF9guVNJ1g==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/react-test-renderer": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
@@ -15657,6 +15667,36 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/test-renderer": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/test-renderer/-/test-renderer-1.2.0.tgz",
+ "integrity": "sha512-JYiEGbgBGtmHAWX8Kf99gGRL1HtjcRVHLrmJNTZL4vFs9XrnWcuL45Iszw/pO4094+JR7havSrN+ds6YTOpSQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "~0.33.0",
+ "react-reconciler": "~0.33.0"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
+ "node_modules/test-renderer/node_modules/react-reconciler": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz",
+ "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.0"
+ }
+ },
"node_modules/throat": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",
diff --git a/package.json b/package.json
index cb8b7a0..8d52506 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
},
"devDependencies": {
"@babel/core": "^7.24.9",
- "@testing-library/react-native": "^13.3.3",
+ "@testing-library/react-native": "^14.0.1",
"@types/jest": "^29.5.14",
"@types/react": "~19.2.10",
"eslint": "^9.39.5",
@@ -61,7 +61,7 @@
"jest": "^29.7.0",
"jest-expo": "~57.0.5",
"prettier": "^3.9.6",
- "react-test-renderer": "19.2.3",
+ "test-renderer": "~1.2.0",
"typescript": "~6.0.3"
},
"expo": {
diff --git a/src/App.test.tsx b/src/App.test.tsx
index 25270d8..c90f8ca 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -15,7 +15,7 @@ jest.mock('@stripe/stripe-react-native', () => ({
afterEach(() => appQueryClient.clear());
it('renders the whole app up to the navigation tabs', async () => {
- const { findByText } = render();
+ const { findByText } = await render();
// A tab label proves the auth bootstrap finished and Navigation mounted
expect(await findByText('Medlemskap')).toBeTruthy();
diff --git a/src/auth/AuthContext.test.tsx b/src/auth/AuthContext.test.tsx
index f2c62fb..3c7fa15 100644
--- a/src/auth/AuthContext.test.tsx
+++ b/src/auth/AuthContext.test.tsx
@@ -4,6 +4,7 @@ import { act, renderHook, waitFor } from '@testing-library/react-native';
import React from 'react';
import { AuthProvider, useAuth } from '@/auth/AuthContext';
+import * as tokenStorage from '@/auth/tokenStorage';
import { queryKeys } from '@/queries/queryKeys';
const TOKEN_KEY = 'cnapp.authToken';
@@ -13,7 +14,7 @@ const grisen = { username: 'grisen', email: 'grisen@example.test', is_member: tr
// gcTime: Infinity so no GC timers keep Jest alive
const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { gcTime: Infinity } } });
-const renderAuth = ({
+const renderAuth = async ({
initialToken,
queryClient = createQueryClient(),
}: { initialToken?: string | null; queryClient?: QueryClient } = {}) => {
@@ -22,7 +23,7 @@ const renderAuth = ({
{children}
);
- return { queryClient, ...renderHook(() => useAuth(), { wrapper }) };
+ return { queryClient, ...(await renderHook(() => useAuth(), { wrapper })) };
};
beforeEach(async () => {
@@ -32,19 +33,25 @@ beforeEach(async () => {
});
it('renders nothing until the stored session is restored', async () => {
- await AsyncStorage.setItem(TOKEN_KEY, 'token-123');
+ // Hold the token read so the closed gate is still observable after the async render
+ let releaseToken!: (token: string | null) => void;
+ jest
+ .spyOn(tokenStorage, 'getStoredToken')
+ .mockImplementationOnce(() => new Promise((resolve) => (releaseToken = resolve)));
- const { result } = renderAuth();
+ const { result } = await renderAuth();
// The gate: children (and thus the hook) have not rendered yet
expect(result.current).toBeNull();
- await waitFor(() => expect(result.current).not.toBeNull());
+ await act(async () => {
+ releaseToken('token-123');
+ });
expect(result.current.token).toBe('token-123');
expect(result.current.isAuthenticated).toBe(true);
});
it('starts logged out when nothing is stored', async () => {
- const { result } = renderAuth();
+ const { result } = await renderAuth();
await waitFor(() => expect(result.current).not.toBeNull());
expect(result.current.token).toBeNull();
@@ -57,7 +64,7 @@ it('runs the redux-persist migration and seeds the cached user', async () => {
JSON.stringify({ userToken: JSON.stringify('9a8b'), user: JSON.stringify(grisen) }),
);
- const { result, queryClient } = renderAuth();
+ const { result, queryClient } = await renderAuth();
await waitFor(() => expect(result.current?.token).toBe('9a8b'));
expect(queryClient.getQueryData(queryKeys.user)).toEqual(grisen);
@@ -65,9 +72,9 @@ it('runs the redux-persist migration and seeds the cached user', async () => {
});
it('signIn stores the token and flips isAuthenticated', async () => {
- const { result } = renderAuth({ initialToken: null });
+ const { result } = await renderAuth({ initialToken: null });
- act(() => result.current.signIn('token-1'));
+ await act(() => result.current.signIn('token-1'));
expect(result.current.token).toBe('token-1');
expect(result.current.isAuthenticated).toBe(true);
@@ -79,8 +86,8 @@ it('signOut clears the token and drops the cached user', async () => {
queryClient.setQueryData(queryKeys.user, grisen);
await AsyncStorage.setItem(TOKEN_KEY, 'token-1');
- const { result } = renderAuth({ initialToken: 'token-1', queryClient });
- act(() => result.current.signOut());
+ const { result } = await renderAuth({ initialToken: 'token-1', queryClient });
+ await act(() => result.current.signOut());
expect(result.current.token).toBeNull();
expect(result.current.isAuthenticated).toBe(false);
@@ -88,10 +95,10 @@ it('signOut clears the token and drops the cached user', async () => {
await waitFor(async () => expect(await AsyncStorage.getItem(TOKEN_KEY)).toBeNull());
});
-it('skips the storage bootstrap when given an initialToken', () => {
+it('skips the storage bootstrap when given an initialToken', async () => {
const getItemSpy = jest.spyOn(AsyncStorage, 'getItem');
- const { result } = renderAuth({ initialToken: 'token-x' });
+ const { result } = await renderAuth({ initialToken: 'token-x' });
// Synchronously ready: no gate, no storage reads
expect(result.current.token).toBe('token-x');
diff --git a/src/modules/about/About.test.tsx b/src/modules/about/About.test.tsx
index 5ae5b7f..57711eb 100644
--- a/src/modules/about/About.test.tsx
+++ b/src/modules/about/About.test.tsx
@@ -3,8 +3,8 @@ import React from 'react';
import About from '@/modules/about/About';
import { renderWithProviders } from '@/testUtils';
-it('renders the about screen', () => {
- const { getByText } = renderWithProviders();
+it('renders the about screen', async () => {
+ const { getByText } = await renderWithProviders();
expect(getByText('Åpningstider')).toBeTruthy();
expect(getByText('Ta kontakt')).toBeTruthy();
diff --git a/src/modules/events/EventList.test.tsx b/src/modules/events/EventList.test.tsx
index 1a6289c..5ed7bcc 100644
--- a/src/modules/events/EventList.test.tsx
+++ b/src/modules/events/EventList.test.tsx
@@ -47,7 +47,7 @@ const event = {
it('formats the next occurrence as a Norwegian date with the venue', async () => {
mockFetch.mockResolvedValue({ json: async () => ({ events: [event] }) });
- const { findByText } = renderWithProviders();
+ const { findByText } = await renderWithProviders();
expect(await findByText('lø. 1. aug. 2026 kl. 19:00 i Storsalen')).toBeTruthy();
});
@@ -59,7 +59,7 @@ it('formats weekday and month abbreviations across the year', async () => {
];
mockFetch.mockResolvedValue({ json: async () => ({ events }) });
- const { findByText } = renderWithProviders();
+ const { findByText } = await renderWithProviders();
expect(await findByText('on. 30. des. 2026 kl. 18:00 i Storsalen')).toBeTruthy();
expect(await findByText('ma. 17. mai 2027 kl. 13:00 i Storsalen')).toBeTruthy();
@@ -70,7 +70,7 @@ it('renders venue-local (Oslo) time whatever the API offset and device timezone'
const events = [{ ...event, nextOccurrence: { ...event.nextOccurrence, start: '2026-08-01T17:00:00Z' } }];
mockFetch.mockResolvedValue({ json: async () => ({ events }) });
- const { findByText } = renderWithProviders();
+ const { findByText } = await renderWithProviders();
expect(await findByText('lø. 1. aug. 2026 kl. 19:00 i Storsalen')).toBeTruthy();
});
@@ -78,7 +78,7 @@ it('renders venue-local (Oslo) time whatever the API offset and device timezone'
it('prefixes the time with Neste: when the event has more occurrences', async () => {
mockFetch.mockResolvedValue({ json: async () => ({ events: [{ ...event, futureOccurrencesCount: 3 }] }) });
- const { findByText } = renderWithProviders();
+ const { findByText } = await renderWithProviders();
expect(await findByText('Neste: lø. 1. aug. 2026 kl. 19:00 i Storsalen')).toBeTruthy();
expect(await findByText('Se flere tider (2)')).toBeTruthy();
@@ -87,7 +87,7 @@ it('prefixes the time with Neste: when the event has more occurrences', async ()
it('renders events from the API', async () => {
mockFetch.mockResolvedValue({ json: async () => ({ events: [event] }) });
- const { findByText } = renderWithProviders();
+ const { findByText } = await renderWithProviders();
expect(await findByText('Lørdagsquiz')).toBeTruthy();
});
@@ -95,7 +95,7 @@ it('renders events from the API', async () => {
it('renders an error state when the API is unreachable', async () => {
mockFetch.mockRejectedValue(new Error('network down'));
- const { findByText } = renderWithProviders();
+ const { findByText } = await renderWithProviders();
expect(await findByText('Kunne ikke hente programmet...')).toBeTruthy();
});
@@ -106,11 +106,11 @@ it('stops the refresh spinner and shows the error when refresh fails', async ()
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
);
- const { result } = renderHook(() => useEventList(), { wrapper });
+ const { result } = await renderHook(() => useEventList(), { wrapper });
await waitFor(() => expect(result.current.events).toHaveLength(1));
mockFetch.mockRejectedValue(new Error('network down'));
- act(() => result.current.handleRefresh());
+ await act(() => result.current.handleRefresh());
await waitFor(() => expect(result.current.error).toBeTruthy());
expect(result.current.refreshing).toBe(false);
diff --git a/src/modules/login/Login.test.tsx b/src/modules/login/Login.test.tsx
index d308970..c02fdb9 100644
--- a/src/modules/login/Login.test.tsx
+++ b/src/modules/login/Login.test.tsx
@@ -15,20 +15,20 @@ jest.mock('@react-navigation/native', () => {
return { ...actual, useNavigation: () => ({ goBack: mockGoBack }) };
});
-const fillForm = (
+const fillForm = async (
getByLabelText: RenderResult['getByLabelText'],
{ username = 'grisen@example.test', password = 'hunter2' } = {},
) => {
- fireEvent.changeText(getByLabelText('E-post / Brukernavn'), username);
- fireEvent.changeText(getByLabelText('Passord'), password);
+ await fireEvent.changeText(getByLabelText('E-post / Brukernavn'), username);
+ await fireEvent.changeText(getByLabelText('Passord'), password);
};
beforeEach(() => {
jest.clearAllMocks();
});
-it('renders the login form', () => {
- const { getByText } = renderWithProviders();
+it('renders the login form', async () => {
+ const { getByText } = await renderWithProviders();
expect(getByText('E-post / Brukernavn')).toBeTruthy();
expect(getByText('Passord')).toBeTruthy();
@@ -37,18 +37,18 @@ it('renders the login form', () => {
});
it('moves focus to the password field when the username is submitted from the keyboard', async () => {
- const { getByLabelText } = renderWithProviders();
+ const { getByLabelText } = await renderWithProviders();
- fireEvent(getByLabelText('E-post / Brukernavn'), 'submitEditing');
+ await 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();
+it('opens the forgot password page in the browser', async () => {
+ const { getByText } = await renderWithProviders();
- fireEvent.press(getByText('Glemt passord?'));
+ await 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/');
@@ -56,10 +56,10 @@ it('opens the forgot password page in the browser', () => {
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();
+ const { getByText, getByLabelText } = await renderWithProviders();
- fillForm(getByLabelText);
- fireEvent.press(getByText('Logg inn'));
+ await fillForm(getByLabelText);
+ await fireEvent.press(getByText('Logg inn'));
await waitFor(() => expect(mockGoBack).toHaveBeenCalled());
expect(login).toHaveBeenCalledWith('grisen@example.test', 'hunter2');
@@ -72,10 +72,10 @@ it('shows the server error when the credentials are rejected', async () => {
400,
);
jest.spyOn(api, 'login').mockRejectedValueOnce(invalidCredentials);
- const { getByText, getByLabelText, findByText } = renderWithProviders();
+ const { getByText, getByLabelText, findByText } = await renderWithProviders();
- fillForm(getByLabelText);
- fireEvent.press(getByText('Logg inn'));
+ await fillForm(getByLabelText);
+ await fireEvent.press(getByText('Logg inn'));
expect(await findByText('Kan ikke logge inn med angitt brukernavn og passord')).toBeTruthy();
expect(mockGoBack).not.toHaveBeenCalled();
@@ -84,20 +84,20 @@ 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(() => {});
jest.spyOn(api, 'login').mockRejectedValueOnce(new Error('network down'));
- const { getByText, getByLabelText, findByText } = renderWithProviders();
+ const { getByText, getByLabelText, findByText } = await renderWithProviders();
- fillForm(getByLabelText);
- fireEvent.press(getByText('Logg inn'));
+ await fillForm(getByLabelText);
+ await fireEvent.press(getByText('Logg inn'));
expect(await findByText('Kunne ikke sende inn skjemaet! Prøv igjen senere...')).toBeTruthy();
expect(mockGoBack).not.toHaveBeenCalled();
});
it('logs in a dev-fake user without hitting the network', async () => {
- const { getByText, getByLabelText } = renderWithProviders();
+ const { getByText, getByLabelText } = await renderWithProviders();
- fillForm(getByLabelText, { username: 'member', password: 'whatever' });
- fireEvent.press(getByText('Logg inn'));
+ await fillForm(getByLabelText, { username: 'member', password: 'whatever' });
+ await fireEvent.press(getByText('Logg inn'));
await waitFor(() => expect(mockGoBack).toHaveBeenCalled());
expect(global.fetch).not.toHaveBeenCalled();
diff --git a/src/modules/login/Login.tsx b/src/modules/login/Login.tsx
index 1c77722..b01d666 100644
--- a/src/modules/login/Login.tsx
+++ b/src/modules/login/Login.tsx
@@ -44,7 +44,9 @@ const Login = () => {
Logg inn
{loginError && isSubmitted && }
- {isLoggingIn && }
+ {isLoggingIn && (
+
+ )}
- {isDeletingUser && }
+ {isDeletingUser && (
+
+ )}
);
diff --git a/src/modules/register/UserRegister.test.tsx b/src/modules/register/UserRegister.test.tsx
index b286ab7..e13265f 100644
--- a/src/modules/register/UserRegister.test.tsx
+++ b/src/modules/register/UserRegister.test.tsx
@@ -1,6 +1,5 @@
import { act, fireEvent, waitFor, type RenderResult } from '@testing-library/react-native';
import React from 'react';
-import { ActivityIndicator } from 'react-native';
import * as api from '@/api';
import { ApiError } from '@/errors';
@@ -24,37 +23,33 @@ const validFormValues: Record = {
Passord: 'hunter2',
};
-const fillForm = (getByLabelText: RenderResult['getByLabelText'], overrides: Record = {}) => {
- Object.entries({ ...validFormValues, ...overrides }).forEach(([label, value]) => {
- fireEvent.changeText(getByLabelText(label), value);
- });
+const fillForm = async (getByLabelText: RenderResult['getByLabelText'], overrides: Record = {}) => {
+ for (const [label, value] of Object.entries({ ...validFormValues, ...overrides })) {
+ await fireEvent.changeText(getByLabelText(label), value);
+ }
};
beforeEach(() => {
jest.clearAllMocks();
});
-afterEach(() => {
- jest.useRealTimers();
-});
-
-it('renders the registration form', () => {
- const { getByText } = renderWithProviders();
+it('renders the registration form', async () => {
+ const { getByText } = await renderWithProviders();
expect(getByText('Fornavn')).toBeTruthy();
expect(getByText('E-post')).toBeTruthy();
expect(getByText('Registrer meg')).toBeTruthy();
});
-it('disables the submit button while the form is incomplete', () => {
+it('disables the submit button while the form is incomplete', async () => {
const registerUser = jest.spyOn(api, 'registerUser');
- const { getByText, getByRole, UNSAFE_queryByType } = renderWithProviders();
+ const { getByText, getByRole, queryByRole } = await renderWithProviders();
expect(getByRole('button', { name: 'Registrer meg' })).toBeDisabled();
- fireEvent.press(getByText('Registrer meg'));
+ await fireEvent.press(getByText('Registrer meg'));
expect(registerUser).not.toHaveBeenCalled();
- expect(UNSAFE_queryByType(ActivityIndicator)).toBeNull();
+ expect(queryByRole('progressbar')).toBeNull();
});
it('submits the form, shows a spinner while pending and navigates back on success', async () => {
@@ -62,14 +57,14 @@ it('submits the form, shows a spinner while pending and navigates back on succes
const registerUser = jest
.spyOn(api, 'registerUser')
.mockImplementationOnce(() => new Promise((resolve) => (resolveRegister = resolve)));
- const { getByText, getByRole, getByLabelText, UNSAFE_getByType } = renderWithProviders();
+ const { getByText, getByRole, getByLabelText } = await renderWithProviders();
- fillForm(getByLabelText);
+ await fillForm(getByLabelText);
await waitFor(() => expect(getByRole('button', { name: 'Registrer meg' })).toBeEnabled());
- fireEvent.press(getByText('Registrer meg'));
+ await fireEvent.press(getByText('Registrer meg'));
// the spinner shows while the request is in flight
- await waitFor(() => expect(UNSAFE_getByType(ActivityIndicator)).toBeTruthy());
+ await waitFor(() => expect(getByRole('progressbar')).toBeTruthy());
expect(registerUser).toHaveBeenCalledWith(
expect.objectContaining({
first_name: 'Grisen',
@@ -92,22 +87,22 @@ it('shows server field errors on the matching form field', async () => {
jest
.spyOn(api, 'registerUser')
.mockRejectedValueOnce(new ApiError({ email: ['Denne e-postadressen er allerede i bruk'] }, 400));
- const { getByText, getByRole, getByLabelText, findByText } = renderWithProviders();
+ const { getByText, getByRole, getByLabelText, findByText } = await renderWithProviders();
- fillForm(getByLabelText);
+ await fillForm(getByLabelText);
await waitFor(() => expect(getByRole('button', { name: 'Registrer meg' })).toBeEnabled());
- fireEvent.press(getByText('Registrer meg'));
+ await fireEvent.press(getByText('Registrer meg'));
expect(await findByText('Denne e-postadressen er allerede i bruk')).toBeTruthy();
expect(mockGoBack).not.toHaveBeenCalled();
});
it('moves focus down the form when a field is submitted from the keyboard', async () => {
- const { getByLabelText } = renderWithProviders();
+ const { getByLabelText } = await renderWithProviders();
const labels = ['Fornavn', 'Etternavn', 'E-post', 'Mobilnummer', 'Passord'];
for (const [i, label] of labels.slice(0, -1).entries()) {
- fireEvent(getByLabelText(label), 'submitEditing');
+ await 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]));
}
@@ -115,41 +110,23 @@ it('moves focus down the form when a field is submitted from the keyboard', asyn
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();
+ const { getByText, getByRole, getByLabelText, findByText } = await renderWithProviders();
- fillForm(getByLabelText);
+ await fillForm(getByLabelText);
await waitFor(() => expect(getByRole('button', { name: 'Registrer meg' })).toBeEnabled());
- fireEvent.press(getByText('Registrer meg'));
+ await 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();
-
- // 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();
+ const { getByLabelText, findByText } = await renderWithProviders();
- fillForm(getByLabelText, { 'E-post': 'not-an-email' });
+ await fillForm(getByLabelText, { 'E-post': 'not-an-email' });
// Enter on the last field submits too, bypassing the disabled button
- fireEvent(getByLabelText('Passord'), 'submitEditing');
+ await fireEvent(getByLabelText('Passord'), 'submitEditing');
expect(await findByText('er ikke en gyldig e-post')).toBeTruthy();
expect(registerUser).not.toHaveBeenCalled();
diff --git a/src/modules/register/UserRegister.tsx b/src/modules/register/UserRegister.tsx
index 3978065..584f218 100644
--- a/src/modules/register/UserRegister.tsx
+++ b/src/modules/register/UserRegister.tsx
@@ -6,24 +6,13 @@ import Button from '@/components/Button';
import FormErrors from '@/components/FormErrors';
import FormInput from '@/components/FormInput';
import PrivacyButton from '@/components/PrivacyButton';
-import Toast from '@/components/Toast';
import theme from '@/theme';
import useUserRegister from './useUserRegister';
const UserRegister = () => {
- const {
- isRegisteringUser,
- onSubmit,
- isValid,
- isSubmitted,
- control,
- errors,
- registerError,
- setFocus,
- toast,
- setToast,
- } = useUserRegister();
+ const { isRegisteringUser, onSubmit, isValid, isSubmitted, control, errors, registerError, setFocus } =
+ useUserRegister();
const isDisabled = isRegisteringUser || !isValid;
return (
@@ -78,21 +67,14 @@ const UserRegister = () => {
onSubmitEditing={onSubmit}
/>
{registerError && isSubmitted && }
- {isRegisteringUser && }
+ {isRegisteringUser && (
+
+ )}
Registrer meg
- {
- setToast(null);
- }}
- />
>
);
};
diff --git a/src/modules/register/useUserRegister.tsx b/src/modules/register/useUserRegister.tsx
index 78fbf0e..ad262c5 100644
--- a/src/modules/register/useUserRegister.tsx
+++ b/src/modules/register/useUserRegister.tsx
@@ -1,18 +1,15 @@
import { useNavigation } from '@react-navigation/native';
-import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { ApiError } from '@/errors';
import { useRegisterUserMutation } from '@/queries/useRegisterUserMutation';
import type { UserRegisterFormValues } from '@/types/forms';
-import { ToastProps } from '@/types/toast';
export type { UserRegisterFormValues };
const useUserRegister = () => {
const navigation = useNavigation();
const { mutate: register, isPending, error } = useRegisterUserMutation();
- const [toast, setToast] = useState(null);
const {
control,
@@ -25,16 +22,6 @@ const useUserRegister = () => {
const onSubmit = handleSubmit((values) => {
clearErrors();
- if (!isValid) {
- setToast({
- message: 'Noen av feltene er ikke fylt ut riktig',
- visible: true,
- preset: 'failure',
- });
- return;
- }
- setToast(null);
-
register(values, {
onSuccess: () => navigation.goBack(),
onError: (registerError) => {
@@ -60,8 +47,6 @@ const useUserRegister = () => {
control,
errors,
setFocus,
- toast,
- setToast,
};
};
export default useUserRegister;
diff --git a/src/testUtils.tsx b/src/testUtils.tsx
index 21d3c3c..3b7c449 100644
--- a/src/testUtils.tsx
+++ b/src/testUtils.tsx
@@ -48,7 +48,7 @@ interface RenderWithProvidersOptions {
* Renders a screen inside the providers it expects in the real app: a fresh
* query client (no persister), AuthProvider and a NavigationContainer.
*/
-export function renderWithProviders(
+export async function renderWithProviders(
ui: ReactElement,
{
initialToken = null,
@@ -71,7 +71,7 @@ export function renderWithProviders(
queryClient.setQueryData(queryKeys.membershipTypes, initialMembershipTypes);
}
- const result = render(
+ const result = await render(
{ui}