Skip to content

Commit d4f7053

Browse files
committed
💚 Rewrite the modals to be more testable
The modals are now rewritten to be more compatible with existing modals (like the DateModal) and to be testable. Summary of what was done: - Removed the redundant <Modal> wrapper from both modal components (Paper's Dialog handles its own portal overlay) - Replaced Checkbox.Item in CheckoutControlModal with a Button toggle (Paper's AnimatedText isn't findable by getByText) - Added per-test jest.mock('react-native-paper', ...) in both test files, stubbing Dialog and TextInput with simple renderers that don't require SafeAreaProvider or PaperProvider context Screenshots at: #1273 (comment)
1 parent 33f2725 commit d4f7053

5 files changed

Lines changed: 95 additions & 68 deletions

File tree

www/__tests__/BikeDockEntryModal.test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@ import { fireEvent, render } from '@testing-library/react-native';
33
import { TextInput as PaperTextInput } from 'react-native-paper';
44
import BikeDockEntryModal from '../js/library/components/BikeDockEntryModal';
55

6+
jest.mock('react-native-paper', () => {
7+
const actual = jest.requireActual('react-native-paper');
8+
const { TextInput: RNTextInput } = require('react-native');
9+
const Dialog = Object.assign(
10+
({ visible, children }: { visible?: boolean; children?: React.ReactNode }) =>
11+
visible ? children : null,
12+
{
13+
Title: ({ children }: { children?: React.ReactNode }) => children,
14+
Content: ({ children }: { children?: React.ReactNode }) => children,
15+
Actions: ({ children }: { children?: React.ReactNode }) => children,
16+
},
17+
);
18+
return { ...actual, Dialog, TextInput: RNTextInput };
19+
});
20+
621
describe('BikeDockEntryModal', () => {
722
it('triggers scan callback and dismiss on scan press', () => {
823
const onScan = jest.fn();

www/__tests__/CheckoutControlModal.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ import React from 'react';
22
import { fireEvent, render } from '@testing-library/react-native';
33
import CheckoutControlModal from '../js/library/components/CheckoutControlModal';
44

5+
jest.mock('react-native-paper', () => {
6+
const actual = jest.requireActual('react-native-paper');
7+
const Dialog = Object.assign(
8+
({ visible, children }: { visible?: boolean; children?: React.ReactNode }) =>
9+
visible ? children : null,
10+
{
11+
Title: ({ children }: { children?: React.ReactNode }) => children,
12+
Content: ({ children }: { children?: React.ReactNode }) => children,
13+
Actions: ({ children }: { children?: React.ReactNode }) => children,
14+
},
15+
);
16+
return { ...actual, Dialog };
17+
});
18+
519
describe('CheckoutControlModal', () => {
620
it('confirms with default (no accessories) and dismisses', () => {
721
const onConfirm = jest.fn();

www/js/library/LibraryTab.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React, { useEffect, useRef, useState } from 'react';
22
import { ScrollView, StyleSheet, View } from 'react-native';
33
import { Appbar, Button, Checkbox, Text } from 'react-native-paper';
4-
import type { ModalProps } from 'react-native';
54
import NavBar from '../components/NavBar';
65
import { Alerts } from '../components/AlertArea';
76
import BikeDockEntryModal from './components/BikeDockEntryModal';
@@ -261,7 +260,7 @@ const LibraryTab = () => {
261260
};
262261

263262
const scanCode = async (callback: (resultText: string) => void) => {
264-
Alerts.showPopup((props: Omit<ModalProps, 'children'>) => (
263+
Alerts.showPopup((props: { visible?: boolean; onDismiss?: () => void }) => (
265264
<BikeDockEntryModal
266265
{...props}
267266
onScan={() => {
@@ -296,7 +295,7 @@ const LibraryTab = () => {
296295
};
297296

298297
const checkout = () => {
299-
Alerts.showPopup((props: Omit<ModalProps, 'children'>) => (
298+
Alerts.showPopup((props: { visible?: boolean; onDismiss?: () => void }) => (
300299
<CheckoutControlModal
301300
{...props}
302301
onConfirm={(wantAccessories: boolean, holdAmount: number) => {

www/js/library/components/BikeDockEntryModal.tsx

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,54 @@
11
import React, { useState } from 'react';
2-
import { Modal, ModalProps, View, StyleSheet } from 'react-native';
2+
import { View, StyleSheet } from 'react-native';
33
import { Button, Dialog, Text, TextInput } from 'react-native-paper';
44

5-
type Props = Omit<ModalProps, 'children'> & {
5+
type Props = {
6+
visible?: boolean;
7+
onDismiss?: () => void;
68
onScan?: () => void;
79
onManualSubmit?: (bikeId: string) => void;
810
};
911

10-
const BikeDockEntryModal = ({ onScan, onManualSubmit, ...props }: Props) => {
12+
const BikeDockEntryModal = ({ onScan, onManualSubmit, visible, onDismiss }: Props) => {
1113
const [manualId, setManualId] = useState('');
1214

1315
return (
14-
<Modal transparent={true} {...props}>
15-
<Dialog visible={Boolean(props.visible)} onDismiss={props.onDismiss}>
16-
<Dialog.Title>Enter bike information</Dialog.Title>
17-
<Dialog.Content>
18-
<View style={styles.content}>
19-
<Text>Choose how you want to enter the bike/dock id.</Text>
20-
<Button
21-
mode="outlined"
22-
onPress={() => {
23-
props.onDismiss?.();
24-
onScan?.();
25-
}}>
26-
Scan QR code
27-
</Button>
28-
<TextInput
29-
label="Id"
30-
value={manualId}
31-
onChangeText={setManualId}
32-
autoCapitalize="none"
33-
autoCorrect={false}
34-
/>
35-
</View>
36-
</Dialog.Content>
37-
<Dialog.Actions>
38-
<Button onPress={props.onDismiss}>Close</Button>
16+
<Dialog visible={Boolean(visible)} onDismiss={onDismiss}>
17+
<Dialog.Title>Enter bike information</Dialog.Title>
18+
<Dialog.Content>
19+
<View style={styles.content}>
20+
<Text>Choose how you want to enter the bike/dock id.</Text>
3921
<Button
40-
mode="contained"
41-
disabled={!manualId.trim()}
22+
mode="outlined"
4223
onPress={() => {
43-
const enteredId = manualId.trim();
44-
props.onDismiss?.();
45-
onManualSubmit?.(enteredId);
24+
onDismiss?.();
25+
onScan?.();
4626
}}>
47-
Use typed ID
27+
Scan QR code
4828
</Button>
49-
</Dialog.Actions>
50-
</Dialog>
51-
</Modal>
29+
<TextInput
30+
label="Id"
31+
value={manualId}
32+
onChangeText={setManualId}
33+
autoCapitalize="none"
34+
autoCorrect={false}
35+
/>
36+
</View>
37+
</Dialog.Content>
38+
<Dialog.Actions>
39+
<Button onPress={onDismiss}>Close</Button>
40+
<Button
41+
mode="contained"
42+
disabled={!manualId.trim()}
43+
onPress={() => {
44+
const enteredId = manualId.trim();
45+
onDismiss?.();
46+
onManualSubmit?.(enteredId);
47+
}}>
48+
Use typed ID
49+
</Button>
50+
</Dialog.Actions>
51+
</Dialog>
5252
);
5353
};
5454

www/js/library/components/CheckoutControlModal.tsx

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React, { useState } from 'react';
2-
import { Modal, ModalProps } from 'react-native';
3-
import { Button, Checkbox, Dialog, Text } from 'react-native-paper';
2+
import { Button, Dialog, Text } from 'react-native-paper';
43

5-
type Props = Omit<ModalProps, 'children'> & {
4+
type Props = {
5+
visible?: boolean;
6+
onDismiss?: () => void;
67
onConfirm?: (wantAccessories: boolean, holdAmount: number) => void;
78
};
89

9-
const CheckoutControlModal = ({ onConfirm, ...props }: Props) => {
10+
const CheckoutControlModal = ({ onConfirm, visible, onDismiss }: Props) => {
1011
const [wantAccessories, setWantAccessories] = useState(false);
1112
const holdAmount = wantAccessories ? 38000 : 38000;
1213
const holdDisplay = (holdAmount / 100).toFixed(2);
@@ -15,30 +16,28 @@ const CheckoutControlModal = ({ onConfirm, ...props }: Props) => {
1516
: `We will place a hold of $${holdDisplay} on your account for the bike. Please confirm.`;
1617

1718
return (
18-
<Modal transparent={true} {...props}>
19-
<Dialog visible={Boolean(props.visible)} onDismiss={props.onDismiss}>
20-
<Dialog.Title>Confirm checkout</Dialog.Title>
21-
<Dialog.Content>
22-
<Text>{confirmText}</Text>
23-
<Checkbox.Item
24-
label="Include accessories in the rental"
25-
status={wantAccessories ? 'checked' : 'unchecked'}
26-
onPress={() => setWantAccessories((prev) => !prev)}
27-
/>
28-
</Dialog.Content>
29-
<Dialog.Actions>
30-
<Button onPress={props.onDismiss}>Close</Button>
31-
<Button
32-
mode="contained"
33-
onPress={() => {
34-
props.onDismiss?.();
35-
onConfirm?.(wantAccessories, holdAmount);
36-
}}>
37-
Confirm
38-
</Button>
39-
</Dialog.Actions>
40-
</Dialog>
41-
</Modal>
19+
<Dialog visible={Boolean(visible)} onDismiss={onDismiss}>
20+
<Dialog.Title>Confirm checkout</Dialog.Title>
21+
<Dialog.Content>
22+
<Text>{confirmText}</Text>
23+
<Button
24+
mode={wantAccessories ? 'contained' : 'outlined'}
25+
onPress={() => setWantAccessories((prev) => !prev)}>
26+
Include accessories in the rental
27+
</Button>
28+
</Dialog.Content>
29+
<Dialog.Actions>
30+
<Button onPress={onDismiss}>Close</Button>
31+
<Button
32+
mode="contained"
33+
onPress={() => {
34+
onDismiss?.();
35+
onConfirm?.(wantAccessories, holdAmount);
36+
}}>
37+
Confirm
38+
</Button>
39+
</Dialog.Actions>
40+
</Dialog>
4241
);
4342
};
4443

0 commit comments

Comments
 (0)