Skip to content

Commit 609b554

Browse files
committed
test: add unit tests for all components
- Add Jest configuration with react-native preset - Add setup file with codegenNativeComponent mock - Add co-located tests for 6 components (113 tests total) - Test prop transformations, event handlers, and defaults - DatePicker/DateRangePicker: toEpochMillis conversion logic - TextField: minHeight calculation based on props - Picker/SheetPicker: prop mapping and event extraction - ModalBottomSheet: defaults and children rendering
1 parent 8d43424 commit 609b554

File tree

9 files changed

+1068
-2
lines changed

9 files changed

+1068
-2
lines changed

eslint.config.mjs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,24 @@ const config = [
88
},
99
},
1010
{
11-
ignores: [".idea/**", "example/**", "test/**"],
11+
ignores: [".idea/**", "example/**", "test/**", "jest.setup.js"],
1212
},
1313
{
1414
languageOptions: {
1515
parserOptions: {
1616
project: ["./tsconfig.node.json", "./tsconfig.json"],
17-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1817
tsconfigRootDir: import.meta.dirname,
1918
},
2019
},
2120
},
21+
{
22+
files: ["**/*.test.{ts,tsx}"],
23+
rules: {
24+
"testing-library/prefer-screen-queries": "off",
25+
"@typescript-eslint/no-unsafe-member-access": "off",
26+
"@typescript-eslint/no-unsafe-return": "off",
27+
},
28+
},
2229
];
2330

2431
export default config;

jest.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/** @type {import('jest').Config} */
2+
module.exports = {
3+
preset: "react-native",
4+
testMatch: ["<rootDir>/src/**/*.test.{ts,tsx}"],
5+
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
6+
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"],
7+
transformIgnorePatterns: [
8+
"node_modules/(?!(.pnpm/[^/]+/node_modules/)?(react-native|@react-native|@testing-library)/)",
9+
],
10+
};

jest.setup.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Mock codegenNativeComponent for all native components
2+
jest.mock("react-native/Libraries/Utilities/codegenNativeComponent", () => {
3+
const React = require("react");
4+
return {
5+
__esModule: true,
6+
default: (name) => {
7+
const MockComponent = React.forwardRef((props, ref) => {
8+
return React.createElement("mock-" + name, { ...props, ref });
9+
});
10+
MockComponent.displayName = `Mock${name}`;
11+
return MockComponent;
12+
},
13+
};
14+
});

src/native/DatePicker.test.tsx

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { describe, expect, it, jest } from "@jest/globals";
2+
import { render } from "@testing-library/react-native";
3+
import React from "react";
4+
import type { NativeSyntheticEvent } from "react-native";
5+
6+
import { DatePicker } from "./DatePicker";
7+
import type { CancelEvent, ConfirmEvent, DateChangeEvent } from "./DatePickerNativeComponent";
8+
9+
// Helper to create mock native events
10+
function createNativeEvent<T>(payload: T): NativeSyntheticEvent<T> {
11+
return { nativeEvent: payload } as NativeSyntheticEvent<T>;
12+
}
13+
14+
describe("DatePicker", () => {
15+
describe("toEpochMillis conversion (via value prop)", () => {
16+
it("handles undefined value", () => {
17+
const { getByTestId } = render(<DatePicker testID="picker" value={undefined} />);
18+
const picker = getByTestId("picker");
19+
expect(picker.props.selectedDateMillis).toBeUndefined();
20+
});
21+
22+
it("handles null value", () => {
23+
const { getByTestId } = render(<DatePicker testID="picker" value={null} />);
24+
const picker = getByTestId("picker");
25+
expect(picker.props.selectedDateMillis).toBeUndefined();
26+
});
27+
28+
it("handles number (epoch milliseconds)", () => {
29+
const epoch = 1704067200000; // 2024-01-01T00:00:00.000Z
30+
const { getByTestId } = render(<DatePicker testID="picker" value={epoch} />);
31+
const picker = getByTestId("picker");
32+
expect(picker.props.selectedDateMillis).toBe(epoch);
33+
});
34+
35+
it("handles ISO string", () => {
36+
const isoString = "2024-01-01T00:00:00.000Z";
37+
const expectedEpoch = new Date(isoString).getTime();
38+
const { getByTestId } = render(<DatePicker testID="picker" value={isoString} />);
39+
const picker = getByTestId("picker");
40+
expect(picker.props.selectedDateMillis).toBe(expectedEpoch);
41+
});
42+
43+
it("handles Date object", () => {
44+
const date = new Date("2024-01-01T00:00:00.000Z");
45+
const { getByTestId } = render(<DatePicker testID="picker" value={date} />);
46+
const picker = getByTestId("picker");
47+
expect(picker.props.selectedDateMillis).toBe(date.getTime());
48+
});
49+
});
50+
51+
describe("date constraint props", () => {
52+
it("converts minDate to milliseconds", () => {
53+
const minDate = new Date("2024-01-01");
54+
const { getByTestId } = render(<DatePicker testID="picker" minDate={minDate} />);
55+
const picker = getByTestId("picker");
56+
expect(picker.props.minDateMillis).toBe(minDate.getTime());
57+
});
58+
59+
it("converts maxDate to milliseconds", () => {
60+
const maxDate = new Date("2024-12-31");
61+
const { getByTestId } = render(<DatePicker testID="picker" maxDate={maxDate} />);
62+
const picker = getByTestId("picker");
63+
expect(picker.props.maxDateMillis).toBe(maxDate.getTime());
64+
});
65+
66+
it("converts initialDisplayedMonth to milliseconds", () => {
67+
const month = new Date("2024-06-15");
68+
const { getByTestId } = render(<DatePicker testID="picker" initialDisplayedMonth={month} />);
69+
const picker = getByTestId("picker");
70+
expect(picker.props.initialDisplayedMonthMillis).toBe(month.getTime());
71+
});
72+
73+
it("passes yearRange as separate props", () => {
74+
const yearRange = { start: 2020, end: 2030 };
75+
const { getByTestId } = render(<DatePicker testID="picker" yearRange={yearRange} />);
76+
const picker = getByTestId("picker");
77+
expect(picker.props.yearRangeStart).toBe(2020);
78+
expect(picker.props.yearRangeEnd).toBe(2030);
79+
});
80+
});
81+
82+
describe("event handlers", () => {
83+
it("onConfirm receives Date object from millis", () => {
84+
const onConfirm = jest.fn();
85+
const { getByTestId } = render(<DatePicker testID="picker" onConfirm={onConfirm} />);
86+
const picker = getByTestId("picker");
87+
88+
const epoch = 1704067200000;
89+
const event = createNativeEvent<ConfirmEvent>({ selectedDateMillis: epoch });
90+
picker.props.onConfirm(event);
91+
92+
expect(onConfirm).toHaveBeenCalledTimes(1);
93+
expect(onConfirm).toHaveBeenCalledWith(new Date(epoch));
94+
});
95+
96+
it("onConfirm receives null when millis is falsy", () => {
97+
const onConfirm = jest.fn();
98+
const { getByTestId } = render(<DatePicker testID="picker" onConfirm={onConfirm} />);
99+
const picker = getByTestId("picker");
100+
101+
const event = createNativeEvent<ConfirmEvent>({ selectedDateMillis: 0 });
102+
picker.props.onConfirm(event);
103+
104+
expect(onConfirm).toHaveBeenCalledWith(null);
105+
});
106+
107+
it("onCancel is called without arguments", () => {
108+
const onCancel = jest.fn();
109+
const { getByTestId } = render(<DatePicker testID="picker" onCancel={onCancel} />);
110+
const picker = getByTestId("picker");
111+
112+
const event = createNativeEvent<CancelEvent>({});
113+
picker.props.onCancel(event);
114+
115+
expect(onCancel).toHaveBeenCalledTimes(1);
116+
expect(onCancel).toHaveBeenCalledWith();
117+
});
118+
119+
it("onChange receives Date object from millis", () => {
120+
const onChange = jest.fn();
121+
const { getByTestId } = render(<DatePicker testID="picker" onChange={onChange} />);
122+
const picker = getByTestId("picker");
123+
124+
const epoch = 1704067200000;
125+
const event = createNativeEvent<DateChangeEvent>({ selectedDateMillis: epoch });
126+
picker.props.onDateChange(event);
127+
128+
expect(onChange).toHaveBeenCalledWith(new Date(epoch));
129+
});
130+
131+
it("onChange receives null when millis is falsy", () => {
132+
const onChange = jest.fn();
133+
const { getByTestId } = render(<DatePicker testID="picker" onChange={onChange} />);
134+
const picker = getByTestId("picker");
135+
136+
const event = createNativeEvent<DateChangeEvent>({ selectedDateMillis: 0 });
137+
picker.props.onDateChange(event);
138+
139+
expect(onChange).toHaveBeenCalledWith(null);
140+
});
141+
});
142+
143+
describe("default props", () => {
144+
it("initialDisplayMode defaults to picker", () => {
145+
const { getByTestId } = render(<DatePicker testID="picker" />);
146+
const picker = getByTestId("picker");
147+
expect(picker.props.initialDisplayMode).toBe("picker");
148+
});
149+
150+
it("showModeToggle defaults to true", () => {
151+
const { getByTestId } = render(<DatePicker testID="picker" />);
152+
const picker = getByTestId("picker");
153+
expect(picker.props.showModeToggle).toBe(true);
154+
});
155+
});
156+
157+
describe("prop passthrough", () => {
158+
it("passes title as titleText", () => {
159+
const { getByTestId } = render(<DatePicker testID="picker" title="Select Date" />);
160+
const picker = getByTestId("picker");
161+
expect(picker.props.titleText).toBe("Select Date");
162+
});
163+
164+
it("passes label prop", () => {
165+
const { getByTestId } = render(<DatePicker testID="picker" label="Birth Date" />);
166+
const picker = getByTestId("picker");
167+
expect(picker.props.label).toBe("Birth Date");
168+
});
169+
170+
it("passes placeholder prop", () => {
171+
const { getByTestId } = render(<DatePicker testID="picker" placeholder="Select a date" />);
172+
const picker = getByTestId("picker");
173+
expect(picker.props.placeholder).toBe("Select a date");
174+
});
175+
176+
it("passes disabled prop", () => {
177+
const { getByTestId } = render(<DatePicker testID="picker" disabled />);
178+
const picker = getByTestId("picker");
179+
expect(picker.props.disabled).toBe(true);
180+
});
181+
182+
it("passes confirmLabel prop", () => {
183+
const { getByTestId } = render(<DatePicker testID="picker" confirmLabel="Done" />);
184+
const picker = getByTestId("picker");
185+
expect(picker.props.confirmLabel).toBe("Done");
186+
});
187+
188+
it("passes cancelLabel prop", () => {
189+
const { getByTestId } = render(<DatePicker testID="picker" cancelLabel="Dismiss" />);
190+
const picker = getByTestId("picker");
191+
expect(picker.props.cancelLabel).toBe("Dismiss");
192+
});
193+
});
194+
195+
describe("style handling", () => {
196+
it("applies base minHeight style", () => {
197+
const { getByTestId } = render(<DatePicker testID="picker" />);
198+
const picker = getByTestId("picker");
199+
expect(picker.props.style).toEqual(expect.arrayContaining([{ minHeight: 64 }]));
200+
});
201+
202+
it("merges custom style with base style", () => {
203+
const customStyle = { backgroundColor: "red" };
204+
const { getByTestId } = render(<DatePicker testID="picker" style={customStyle} />);
205+
const picker = getByTestId("picker");
206+
expect(picker.props.style).toEqual(expect.arrayContaining([{ minHeight: 64 }, customStyle]));
207+
});
208+
});
209+
});

0 commit comments

Comments
 (0)