Add some tests
This commit is contained in:
23 files changed
+2737
-104
No files matched your search
+20
-1
@@ -1,5 +1,6 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
ignorePatterns: ['coverage/'],
|
||||
extends: [
|
||||
'@react-native',
|
||||
'airbnb-base',
|
||||
@@ -29,7 +30,15 @@ module.exports = {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: ['**/*.test.js', '**/*.spec.js', '**/__tests__/**'],
|
||||
devDependencies: [
|
||||
'**/*.test.{js,jsx,ts,tsx}',
|
||||
'**/*.spec.{js,jsx,ts,tsx}',
|
||||
'**/__tests__/**',
|
||||
'src/testUtils.tsx',
|
||||
'jest.config.js',
|
||||
'jest.setup.js',
|
||||
'__mocks__/**',
|
||||
],
|
||||
},
|
||||
],
|
||||
'import/prefer-default-export': 'off',
|
||||
@@ -62,5 +71,15 @@ module.exports = {
|
||||
extends: ['plugin:@typescript-eslint/disable-type-checked'],
|
||||
files: ['./**/*.js'],
|
||||
},
|
||||
{
|
||||
// Jest infrastructure is CommonJS by nature
|
||||
files: ['jest.config.js', 'jest.setup.js', '__mocks__/**'],
|
||||
rules: {
|
||||
'global-require': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'import/extensions': 'off',
|
||||
'class-methods-use-this': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -9,6 +9,7 @@ dist/
|
||||
web-build/
|
||||
android/
|
||||
ios/
|
||||
credentials.json
|
||||
|
||||
# Native
|
||||
*.orig.*
|
||||
@@ -35,3 +36,4 @@ yarn-error.*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
coverage/
|
||||
@@ -28,6 +28,8 @@ Install [Expo Go](https://expo.dev/client) to run the app on your phone.
|
||||
- `npm run lint` - Check code with ESLint
|
||||
- `npm run format` - Fix code formatting with ESLint
|
||||
- `npm run check-types` - Run TypeScript type checking
|
||||
- `npm test` - Run the test suite
|
||||
- `npm run test:watch` - Run the test suite in watch mode
|
||||
|
||||
## History
|
||||
|
||||
@@ -41,7 +43,20 @@ history, code and binary blobs related to React Native functionality filtered ou
|
||||
- privacy policy
|
||||
- delete my account
|
||||
- ???
|
||||
- add some basic unit tests
|
||||
- test in android emulator? (`npm run android`)
|
||||
- test in ios emulator? (`npm run ios`, requires macOS)
|
||||
- use [Expo Application Services](https://expo.dev/eas) to build and submit app to stores
|
||||
- replace redux (`redux` v5 + `redux-thunk`, hand-written actions and the deprecated
|
||||
`createStore`): nearly everything in the store is server state (user data) plus one auth
|
||||
token, so drop redux for [TanStack Query](https://tanstack.com/query/latest) for
|
||||
fetching/caching, with the token in React context + AsyncStorage — replaces
|
||||
`src/reducers.ts`, the fetch thunks in `src/api.ts` and the `isFetching*`/error flags
|
||||
- replace [moment](https://momentjs.com/docs/#/-project-status/) (maintenance mode) with
|
||||
`date-fns`
|
||||
- replace unmaintained UI packages:
|
||||
- `react-native-confetti` (last release 2017, mocked out in tests)
|
||||
- `react-native-simple-radio-button` (react-native-ui-lib already ships RadioButton/RadioGroup)
|
||||
- remove unused dependencies: `styled-system`, `react-native-render-html`,
|
||||
`metro-react-native-babel-preset`
|
||||
- migrate ESLint 8 + airbnb config (both EOL) to ESLint 9 flat config or
|
||||
[eslint-config-expo](https://docs.expo.dev/guides/using-eslint/)
|
||||
@@ -0,0 +1,3 @@
|
||||
// Stand-in for SVG files imported as components (react-native-svg-transformer).
|
||||
module.exports = 'SvgMock';
|
||||
module.exports.ReactComponent = 'SvgMock';
|
||||
@@ -0,0 +1,18 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
preset: 'jest-expo',
|
||||
setupFiles: ['react-native-gesture-handler/jestSetup', '<rootDir>/jest.setup.js'],
|
||||
moduleNameMapper: {
|
||||
// Metro resolves SVGs to components via react-native-svg-transformer; Jest gets a stub instead
|
||||
'\\.svg$': '<rootDir>/__mocks__/svgMock.js',
|
||||
// Absolute imports relative to the repo root (tsconfig baseUrl)
|
||||
'^src/(.*)$': '<rootDir>/src/$1',
|
||||
'^assets/(.*)$': '<rootDir>/assets/$1',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
// jest-expo's default list, extended with untranspiled libraries this app uses
|
||||
'node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|react-native-ui-lib|uilib-native|react-redux))',
|
||||
'/node_modules/react-native-reanimated/plugin/',
|
||||
],
|
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.test.{ts,tsx}', '!src/types/**', '!src/testUtils.tsx'],
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
process.env.EXPO_PUBLIC_EVENT_API_URL = 'https://events.example.test/api/v1/events';
|
||||
process.env.EXPO_PUBLIC_DUSKEN_API_URL = 'https://dusken.example.test';
|
||||
process.env.EXPO_PUBLIC_DUSKEN_FORGOT_PASSWORD_URL = 'https://dusken.example.test/auth/password_reset/';
|
||||
process.env.EXPO_PUBLIC_DUSKEN_PRIVACY_POLICY_URL = 'https://dusken.example.test/nb/personvern/';
|
||||
process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY = 'pk_test_fake';
|
||||
|
||||
// No test may hit the network. Individual tests override this (or mock
|
||||
// fetchWithTimeout in src/utils) when they need specific responses.
|
||||
global.fetch = jest.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ events: [] }),
|
||||
}));
|
||||
|
||||
// react-native-ui-lib calls StatusBarManager.getHeight at import time,
|
||||
// which the react-native jest mock does not provide.
|
||||
const { NativeModules } = require('react-native');
|
||||
|
||||
NativeModules.StatusBarManager = {
|
||||
HEIGHT: 42,
|
||||
...NativeModules.StatusBarManager,
|
||||
getHeight: (callback) => callback({ height: 42 }),
|
||||
};
|
||||
|
||||
jest.mock('@react-native-async-storage/async-storage', () =>
|
||||
require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
|
||||
);
|
||||
|
||||
jest.mock('@stripe/stripe-react-native', () => require('@stripe/stripe-react-native/jest/mock.js'));
|
||||
|
||||
jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'));
|
||||
|
||||
// jest-expo stubs the native side of expo-notifications with functions that
|
||||
// resolve to undefined, which crashes destructuring like `const { status } = ...`.
|
||||
jest.mock('expo-notifications', () => ({
|
||||
SchedulableTriggerInputTypes: { DATE: 'date' },
|
||||
setNotificationHandler: jest.fn(),
|
||||
getPermissionsAsync: jest.fn(async () => ({ status: 'undetermined' })),
|
||||
requestPermissionsAsync: jest.fn(async () => ({ status: 'denied' })),
|
||||
cancelAllScheduledNotificationsAsync: jest.fn(async () => {}),
|
||||
scheduleNotificationAsync: jest.fn(async () => 'notification-id'),
|
||||
}));
|
||||
|
||||
// Visual only, and old enough that it fights with the test renderer.
|
||||
jest.mock('react-native-confetti', () => {
|
||||
const React = require('react');
|
||||
return class Confetti extends React.Component {
|
||||
startConfetti() {}
|
||||
|
||||
stopConfetti() {}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
});
|
||||
Generated
+2304
-12
File diff suppressed because it is too large.
Load diff
+9
-3
@@ -10,10 +10,12 @@
|
||||
"ios": "expo run:ios",
|
||||
"lint": "eslint .",
|
||||
"start": "expo start --port 19001",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watchAll",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.10.8",
|
||||
"@apollo/client": "^3.14.1",
|
||||
"@babel/plugin-transform-private-methods": "^7.24.7",
|
||||
"@expo/vector-icons": "^14.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.1.2",
|
||||
@@ -54,7 +56,8 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.9",
|
||||
"@react-native/eslint-config": "^0.74.85",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@testing-library/react-native": "^13.3.3",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/react": "~19.1.12",
|
||||
"@types/react-native-confetti": "^0.1.4",
|
||||
"@types/react-native-simple-radio-button": "^2.7.7",
|
||||
@@ -69,7 +72,10 @@
|
||||
"eslint-plugin-react": "^7.34.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"expo-dev-client": "~5.0.20",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~53.0.10",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "~5.3.3"
|
||||
"react-test-renderer": "19.0.0",
|
||||
"typescript": "~5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { render, waitFor } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import App from 'src/App';
|
||||
|
||||
it('renders the whole app without crashing', async () => {
|
||||
const view = render(<App />);
|
||||
await waitFor(() => expect(view.toJSON()).toBeTruthy());
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import 'react-native';
|
||||
import { render } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import App from 'src/App';
|
||||
|
||||
it('renders correctly', () => {
|
||||
const { toJSON } = render(<App />);
|
||||
expect(toJSON()).toBeTruthy();
|
||||
});
|
||||
@@ -1,66 +1,70 @@
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
export const useNotifications = () => {
|
||||
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkPermission = async () => {
|
||||
const { status } = await Notifications.getPermissionsAsync();
|
||||
setHasPermission(status === 'granted');
|
||||
};
|
||||
checkPermission();
|
||||
}, []);
|
||||
|
||||
const checkPermission = async () => {
|
||||
const { status } = await Notifications.getPermissionsAsync();
|
||||
setHasPermission(status === 'granted');
|
||||
};
|
||||
|
||||
const requestPermission = async () => {
|
||||
const requestPermission = useCallback(async () => {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
const granted = status === 'granted';
|
||||
setHasPermission(granted);
|
||||
return granted;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scheduleMembershipExpiryNotification = async (expiryDate: string) => {
|
||||
if (!hasPermission) {
|
||||
const granted = await requestPermission();
|
||||
if (!granted) return;
|
||||
}
|
||||
const scheduleMembershipExpiryNotification = useCallback(
|
||||
async (expiryDate: string) => {
|
||||
if (!hasPermission) {
|
||||
const granted = await requestPermission();
|
||||
if (!granted) return;
|
||||
}
|
||||
|
||||
// Cancel any existing notifications before scheduling a new one
|
||||
await Notifications.cancelAllScheduledNotificationsAsync();
|
||||
// Cancel any existing notifications before scheduling a new one
|
||||
await Notifications.cancelAllScheduledNotificationsAsync();
|
||||
|
||||
const expiryDateObj = new Date(expiryDate);
|
||||
const notificationDate = new Date(expiryDateObj);
|
||||
notificationDate.setDate(notificationDate.getDate() + 1); // 1 day after expiry
|
||||
const expiryDateObj = new Date(expiryDate);
|
||||
const notificationDate = new Date(expiryDateObj);
|
||||
notificationDate.setDate(notificationDate.getDate() + 1); // 1 day after expiry
|
||||
|
||||
// Don't schedule if the notification date is in the past
|
||||
if (notificationDate <= new Date()) return;
|
||||
// Don't schedule if the notification date is in the past
|
||||
if (notificationDate <= new Date()) return;
|
||||
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: 'Medlemskapet ditt har utløpt!',
|
||||
body: 'Ditt medlemskap i Det Norske Studentersamfund har utløpt! Vi håper du ønsker å fornye det :)',
|
||||
data: { type: 'membership_expiry' },
|
||||
},
|
||||
trigger: {
|
||||
type: 'date',
|
||||
date: notificationDate,
|
||||
},
|
||||
});
|
||||
};
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: 'Medlemskapet ditt har utløpt!',
|
||||
body: 'Ditt medlemskap i Det Norske Studentersamfund har utløpt! Vi håper du ønsker å fornye det :)',
|
||||
data: { type: 'membership_expiry' },
|
||||
},
|
||||
trigger: {
|
||||
type: Notifications.SchedulableTriggerInputTypes.DATE,
|
||||
date: notificationDate,
|
||||
},
|
||||
});
|
||||
},
|
||||
[hasPermission, requestPermission],
|
||||
);
|
||||
|
||||
const toggleNotifications = async (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
// When enabling, request permission
|
||||
const granted = await requestPermission();
|
||||
return { success: granted, needsPermission: !granted };
|
||||
} else {
|
||||
const toggleNotifications = useCallback(
|
||||
async (enabled: boolean) => {
|
||||
if (enabled) {
|
||||
// When enabling, request permission
|
||||
const granted = await requestPermission();
|
||||
return { success: granted, needsPermission: !granted };
|
||||
}
|
||||
// When disabling, cancel any existing notifications and update state
|
||||
await Notifications.cancelAllScheduledNotificationsAsync();
|
||||
setHasPermission(false);
|
||||
return { success: true, needsPermission: false };
|
||||
}
|
||||
};
|
||||
},
|
||||
[requestPermission],
|
||||
);
|
||||
|
||||
return {
|
||||
hasPermission,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import About from 'src/modules/about/About';
|
||||
import { renderWithProviders } from 'src/testUtils';
|
||||
|
||||
it('renders the about screen', () => {
|
||||
const { getByText } = renderWithProviders(<About />);
|
||||
|
||||
expect(getByText('Åpningstider')).toBeTruthy();
|
||||
expect(getByText('Ta kontakt')).toBeTruthy();
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
import EventList from 'src/modules/events/EventList';
|
||||
import { renderWithProviders } from 'src/testUtils';
|
||||
import { fetchWithTimeout } from 'src/utils';
|
||||
|
||||
jest.mock('src/utils', () => ({
|
||||
...jest.requireActual('src/utils'),
|
||||
fetchWithTimeout: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockFetch = fetchWithTimeout as jest.Mock;
|
||||
|
||||
const event = {
|
||||
id: '1',
|
||||
title: 'Lørdagsquiz',
|
||||
subtitle: 'Quiz med premier',
|
||||
url: 'https://example.test/events/1',
|
||||
futureOccurrencesCount: 1,
|
||||
nextOccurrence: {
|
||||
id: '11',
|
||||
start: '2026-08-01T19:00:00+02:00',
|
||||
venue: {
|
||||
id: '2',
|
||||
slug: 'storsalen',
|
||||
title: 'Storsalen',
|
||||
preposition: 'i',
|
||||
url: 'https://example.test/venues/storsalen',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('renders events from the API', async () => {
|
||||
mockFetch.mockResolvedValue({ json: async () => ({ events: [event] }) });
|
||||
|
||||
const { findByText } = renderWithProviders(<EventList />);
|
||||
|
||||
expect(await findByText('Lørdagsquiz')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders an error state when the API is unreachable', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('network down'));
|
||||
|
||||
const { findByText } = renderWithProviders(<EventList />);
|
||||
|
||||
expect(await findByText('Kunne ikke hente programmet...')).toBeTruthy();
|
||||
});
|
||||
@@ -26,7 +26,7 @@ const EventList = () => {
|
||||
onPress={() => {
|
||||
return Linking.openURL(item.url);
|
||||
}}
|
||||
style={[styles.listItem, ...(index === (events.length -1) ? [styles.lastListItem] : [])]}
|
||||
style={[styles.listItem, ...(index === events.length - 1 ? [styles.lastListItem] : [])]}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<View style={{ flex: 5 }}>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import moment from 'moment';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -8,18 +7,8 @@ import { fetchWithTimeout } from 'src/utils';
|
||||
|
||||
moment.locale('nb');
|
||||
|
||||
const formatDate = (time: string) => {
|
||||
const m = moment(time);
|
||||
return moment().year() === m.year() ? m.format('dddd D. MMMM') : m.format('ll');
|
||||
};
|
||||
type SectionedEvents = {
|
||||
data: EventType[];
|
||||
title: string;
|
||||
};
|
||||
/* Ref: https://medium.com/react-native-development/how-to-use-the-flatlist-component-react-native-basics-92c482816fe6 */
|
||||
const useEventList = () => {
|
||||
const { navigate } = useNavigation();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [events, setEvents] = useState<EventType[]>([]);
|
||||
const [error, setError] = useState<Error | unknown | null>(null);
|
||||
@@ -35,17 +24,17 @@ const useEventList = () => {
|
||||
setLoading(true);
|
||||
|
||||
let response;
|
||||
let events;
|
||||
let fetchedEvents;
|
||||
try {
|
||||
const res = await fetchWithTimeout(url);
|
||||
response = await res.json();
|
||||
events = response.events;
|
||||
fetchedEvents = response.events;
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setEvents(events);
|
||||
setEvents(fetchedEvents);
|
||||
setError(response.error || null);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
@@ -53,7 +42,6 @@ const useEventList = () => {
|
||||
|
||||
useEffect(() => {
|
||||
fetchEvents();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import Login from 'src/modules/login/Login';
|
||||
import { renderWithProviders } from 'src/testUtils';
|
||||
|
||||
it('renders the login form', () => {
|
||||
const { getByText } = renderWithProviders(<Login />);
|
||||
|
||||
expect(getByText('E-post / Brukernavn')).toBeTruthy();
|
||||
expect(getByText('Passord')).toBeTruthy();
|
||||
expect(getByText('Logg inn')).toBeTruthy();
|
||||
expect(getByText('Glemt passord?')).toBeTruthy();
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import React from 'react';
|
||||
|
||||
import Membership from 'src/modules/membership/Membership';
|
||||
import { renderWithProviders, testUser } from 'src/testUtils';
|
||||
|
||||
// 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: 'standard', price: 25000 }] },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('shows the membership pitch when logged out', async () => {
|
||||
const { findByText } = renderWithProviders(<Membership />);
|
||||
|
||||
expect(await findByText('Bli medlem')).toBeTruthy();
|
||||
expect(await findByText('Logg inn')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('shows the membership proof when logged in', async () => {
|
||||
// Mounting Membership with a token triggers requestUserData; serve the
|
||||
// logged in user from the fake API so the refetch does not clobber state.
|
||||
(global.fetch as jest.Mock).mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => testUser,
|
||||
});
|
||||
|
||||
const { findByText } = renderWithProviders(<Membership />, {
|
||||
preloadedState: {
|
||||
isAuthenticated: true,
|
||||
userToken: 'test-token',
|
||||
user: testUser,
|
||||
},
|
||||
apolloMocks,
|
||||
});
|
||||
|
||||
expect(await findByText('MEDLEM')).toBeTruthy();
|
||||
expect(await findByText('Test Testesen')).toBeTruthy();
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
|
||||
import UserDelete from 'src/modules/membership/UserDelete';
|
||||
import { renderWithProviders, testUser } from 'src/testUtils';
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
jest.mock('@react-navigation/native', () => ({
|
||||
...jest.requireActual('@react-navigation/native'),
|
||||
useNavigation: () => ({ navigate: mockNavigate, goBack: jest.fn() }),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
mockNavigate.mockClear();
|
||||
});
|
||||
|
||||
it('renders the delete confirmation for a logged in user', () => {
|
||||
const { getByText } = renderWithProviders(<UserDelete />, {
|
||||
preloadedState: { isAuthenticated: true, userToken: 'test-token', user: testUser },
|
||||
});
|
||||
|
||||
expect(getByText('Slett meg')).toBeTruthy();
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders nothing and navigates back to Membership when not logged in', () => {
|
||||
const { queryByText } = renderWithProviders(<UserDelete />);
|
||||
|
||||
expect(queryByText('Slett meg')).toBeNull();
|
||||
expect(mockNavigate).toHaveBeenCalledWith('UserStack', { screen: 'Membership' });
|
||||
});
|
||||
@@ -71,27 +71,25 @@ const useProof = ({ fetchUser }: UseProofProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
/* Refetech user data if memberships has expired */
|
||||
/* Refetch user data if membership has expired */
|
||||
const membershipType = user?.last_membership?.membership_type;
|
||||
const membershipEndDate = user?.last_membership?.end_date;
|
||||
useEffect(() => {
|
||||
const todaysDate = new Date().toISOString().substring(0, 10);
|
||||
if (
|
||||
user?.lastMembership &&
|
||||
user.lastMembership.membership_type !== 'lifelong' &&
|
||||
user.lastMembership.end_date < todaysDate
|
||||
) {
|
||||
if (membershipEndDate && membershipType !== 'lifelong' && membershipEndDate < todaysDate) {
|
||||
fetchUser();
|
||||
// If payment was successful and notifications are enabled, schedule the notification
|
||||
if (user?.last_membership?.end_date && hasPermission) {
|
||||
scheduleMembershipExpiryNotification(user.last_membership.end_date);
|
||||
if (hasPermission) {
|
||||
scheduleMembershipExpiryNotification(membershipEndDate);
|
||||
}
|
||||
}
|
||||
}, [user?.last_membership]);
|
||||
}, [membershipType, membershipEndDate, fetchUser, hasPermission, scheduleMembershipExpiryNotification]);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const membershipType of data?.membershipTypes ?? []) {
|
||||
if (membershipType.slug === membershipTypeSlug) {
|
||||
setMembershipPrice(membershipType.price);
|
||||
for (const membershipTypeOption of data?.membershipTypes ?? []) {
|
||||
if (membershipTypeOption.slug === membershipTypeSlug) {
|
||||
setMembershipPrice(membershipTypeOption.price);
|
||||
}
|
||||
}
|
||||
}, [membershipTypeSlug, data?.membershipTypes]);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import UserRegister from 'src/modules/register/UserRegister';
|
||||
import { renderWithProviders } from 'src/testUtils';
|
||||
|
||||
it('renders the registration form', () => {
|
||||
const { getByText } = renderWithProviders(<UserRegister />);
|
||||
|
||||
expect(getByText('Fornavn')).toBeTruthy();
|
||||
expect(getByText('E-post')).toBeTruthy();
|
||||
expect(getByText('Registrer meg')).toBeTruthy();
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { InMemoryCache } from '@apollo/client';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { render } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
|
||||
import { thunk } from 'redux-thunk';
|
||||
|
||||
import rootReducer from 'src/reducers';
|
||||
import type { UserType } from 'src/types/data';
|
||||
|
||||
type AppState = ReturnType<typeof rootReducer>;
|
||||
|
||||
const initialState: AppState = rootReducer(undefined, { type: '@@TEST_INIT' });
|
||||
|
||||
/** A member with an active membership, for screens that require a logged in user. */
|
||||
export const testUser: UserType = {
|
||||
username: 'testbruker',
|
||||
email: 'test@example.test',
|
||||
first_name: 'Test',
|
||||
last_name: 'Testesen',
|
||||
is_member: true,
|
||||
is_volunteer: false,
|
||||
last_membership: {
|
||||
membership_type: 'standard',
|
||||
end_date: '2099-08-01',
|
||||
},
|
||||
};
|
||||
|
||||
interface RenderWithProvidersOptions {
|
||||
preloadedState?: Partial<AppState>;
|
||||
apolloMocks?: ReadonlyArray<MockedResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a screen inside the providers it expects in the real app:
|
||||
* a fresh (non-persisted) Redux store, Apollo's MockedProvider and a
|
||||
* NavigationContainer.
|
||||
*/
|
||||
export function renderWithProviders(
|
||||
ui: ReactElement,
|
||||
{ preloadedState, apolloMocks = [] }: RenderWithProvidersOptions = {},
|
||||
) {
|
||||
const store = createStore(rootReducer, { ...initialState, ...preloadedState }, applyMiddleware(thunk));
|
||||
|
||||
const result = render(
|
||||
<MockedProvider mocks={apolloMocks} cache={new InMemoryCache()}>
|
||||
<Provider store={store}>
|
||||
<NavigationContainer>{ui}</NavigationContainer>
|
||||
</Provider>
|
||||
</MockedProvider>,
|
||||
);
|
||||
|
||||
return { store, ...result };
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { NavigatorScreenParams } from '@react-navigation/native';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
|
||||
import { EventType } from './data';
|
||||
|
||||
|
||||
+16
-8
@@ -5,12 +5,20 @@ export function snakeToCamelCase(val: string): string {
|
||||
export function fetchWithTimeout(url: string, options?: RequestInit, timeout = 10000) {
|
||||
/** Wraps fetch to allow it to time out */
|
||||
// FIXME: show timeout message in UI
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
new Promise<Response>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Fetch request timed out'));
|
||||
}, timeout);
|
||||
}),
|
||||
]);
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error('Fetch request timed out'));
|
||||
}, timeout);
|
||||
|
||||
fetch(url, options).then(
|
||||
(response) => {
|
||||
clearTimeout(timer);
|
||||
resolve(response);
|
||||
},
|
||||
(error) => {
|
||||
clearTimeout(timer);
|
||||
reject(error);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user