Skip to content

Commit 6399ff6

Browse files
committed
Add comprehensive tests for Home and Onboarding screens
Added new test suites for HomeScreen and OnboardingScreen, including validation UI, error flows, and data-layer mocking. Refactored onboarding tests into a dedicated folder. Updated Home and Onboarding components to improve testability by adding testIDs. Configured Jest to use a custom setup file for consistent mocks. Enhanced README with details about the QA automation setup.
1 parent 2bb045d commit 6399ff6

9 files changed

Lines changed: 304 additions & 8 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import React from "react";
2+
import { render, waitFor, act, fireEvent } from "@testing-library/react-native";
3+
4+
5+
// 1) Navigation hook mock
6+
const mockNavigate = jest.fn();
7+
8+
jest.mock("@react-navigation/native", () => ({
9+
useNavigation: () => ({ navigate: mockNavigate }),
10+
}));
11+
12+
13+
// 2) Mock Alert to avoid noisy test failures
14+
jest.mock("react-native/Libraries/Alert/Alert", () => ({
15+
alert: jest.fn(),
16+
}));
17+
18+
// 3) Mock SearchBar / CategoryFilter / HeroBanner (чтобы не зависеть от их UI)
19+
jest.mock("../utils/SearchBar", () => {
20+
const React = require("react");
21+
const { View, Pressable, Text } = require("react-native");
22+
return ({ onSearch }) => (
23+
<View>
24+
{/* “Кнопка” для теста поиска */}
25+
<Pressable testID="mock-search" onPress={() => onSearch("salad")}>
26+
<Text>MockSearch</Text>
27+
</Pressable>
28+
</View>
29+
);
30+
});
31+
32+
jest.mock("../utils/CategoryFilter", () => {
33+
const React = require("react");
34+
const { View, Pressable, Text } = require("react-native");
35+
return ({ onSelect }) => (
36+
<View>
37+
{/* “Кнопка” для теста категории */}
38+
<Pressable testID="mock-category" onPress={() => onSelect("starters")}>
39+
<Text>MockCategory</Text>
40+
</Pressable>
41+
</View>
42+
);
43+
});
44+
45+
jest.mock("../utils/HeroBanner", () => {
46+
const React = require("react");
47+
const { View, Text } = require("react-native");
48+
return () => (
49+
<View>
50+
<Text>HeroBanner</Text>
51+
</View>
52+
);
53+
});
54+
55+
// 4) Mock database module
56+
jest.mock("../database/database", () => ({
57+
initDatabase: jest.fn(),
58+
saveMenuItems: jest.fn(),
59+
getMenuItems: jest.fn(),
60+
filterByQueryAndCategories: jest.fn(),
61+
}));
62+
63+
import HomeScreen from "../screens/Home";
64+
import {
65+
initDatabase,
66+
saveMenuItems,
67+
getMenuItems,
68+
filterByQueryAndCategories,
69+
} from "../database/database";
70+
71+
describe("HomeScreen", () => {
72+
beforeEach(() => {
73+
jest.clearAllMocks();
74+
mockNavigate.mockReset();
75+
});
76+
77+
test("renders items from DB when getMenuItems returns data", async () => {
78+
initDatabase.mockResolvedValueOnce();
79+
getMenuItems.mockResolvedValueOnce([
80+
{
81+
id: 1,
82+
name: "Greek Salad",
83+
description: "Fresh and delicious",
84+
price: 12.99,
85+
category: "starters",
86+
image: "https://example.com/salad.png",
87+
},
88+
]);
89+
90+
const { queryByText, findByText } = render(
91+
<HomeScreen route={{ params: {} }} />
92+
);
93+
94+
// item appears
95+
expect(await findByText("Greek Salad")).toBeTruthy();
96+
97+
// empty state should NOT be visible once items exist
98+
expect(queryByText("No items found matching your criteria")).toBeNull();
99+
100+
// fetch не нужен, потому что DB не пустая
101+
expect(saveMenuItems).not.toHaveBeenCalled();
102+
});
103+
104+
test("fetches remote menu and saves to DB when DB is empty", async () => {
105+
initDatabase.mockResolvedValueOnce();
106+
getMenuItems.mockResolvedValueOnce([]); // DB empty => should fetch
107+
108+
global.fetch = jest.fn(async () => ({
109+
json: async () => ({
110+
menu: [
111+
{
112+
id: 10,
113+
name: "Lemon Dessert",
114+
description: "Sweet",
115+
price: 7.5,
116+
category: "desserts",
117+
image: "lemon-dessert.jpg",
118+
},
119+
],
120+
}),
121+
}));
122+
123+
const { findByText } = render(<HomeScreen route={{ params: {} }} />);
124+
125+
// after fetch+save, item appears
126+
expect(await findByText("Lemon Dessert")).toBeTruthy();
127+
128+
// saveMenuItems should have been called with mapped image urls
129+
await waitFor(() => {
130+
expect(saveMenuItems).toHaveBeenCalledTimes(1);
131+
const arg = saveMenuItems.mock.calls[0][0];
132+
expect(arg[0].name).toBe("Lemon Dessert");
133+
expect(String(arg[0].image)).toContain("raw=true");
134+
});
135+
});
136+
137+
test("applies filtering after debounce (500ms) when search/category changes", async () => {
138+
jest.useFakeTimers();
139+
140+
initDatabase.mockResolvedValueOnce();
141+
// initial load from DB
142+
getMenuItems.mockResolvedValueOnce([
143+
{
144+
id: 1,
145+
name: "Greek Salad",
146+
description: "Fresh",
147+
price: 12.99,
148+
category: "starters",
149+
image: "https://example.com/salad.png",
150+
},
151+
{
152+
id: 2,
153+
name: "Burger",
154+
description: "Big",
155+
price: 15.0,
156+
category: "mains",
157+
image: "https://example.com/burger.png",
158+
},
159+
]);
160+
161+
// filtered result when user searches “salad” and selects "starters"
162+
filterByQueryAndCategories.mockResolvedValueOnce([
163+
{
164+
id: 1,
165+
name: "Greek Salad",
166+
description: "Fresh",
167+
price: 12.99,
168+
category: "starters",
169+
image: "https://example.com/salad.png",
170+
},
171+
]);
172+
173+
const { getByTestId, queryByText, findByText } = render(
174+
<HomeScreen route={{ params: {} }} />
175+
);
176+
177+
// initial items appear
178+
expect(await findByText("Greek Salad")).toBeTruthy();
179+
expect(queryByText("Burger")).toBeTruthy();
180+
181+
// trigger search + category through mocked components
182+
act(() => {
183+
fireEvent.press(getByTestId("mock-search"));
184+
fireEvent.press(getByTestId("mock-category"));
185+
});
186+
187+
188+
// advance debounce
189+
await act(async () => {
190+
jest.advanceTimersByTime(500);
191+
});
192+
193+
await waitFor(() => {
194+
expect(filterByQueryAndCategories).toHaveBeenCalled();
195+
});
196+
197+
// after filter, burger should disappear, salad remains
198+
await waitFor(() => {
199+
expect(queryByText("Greek Salad")).toBeTruthy();
200+
expect(queryByText("Burger")).toBeNull();
201+
});
202+
203+
jest.useRealTimers();
204+
});
205+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from "react";
2+
import { render, fireEvent, waitFor } from "@testing-library/react-native";
3+
import { Alert } from "react-native";
4+
5+
import AsyncStorage from "@react-native-async-storage/async-storage";
6+
import OnboardingScreen from "../../screens/Onboarding";
7+
8+
describe("OnboardingScreen error flow", () => {
9+
test("shows alert if saving user info fails", async () => {
10+
const navigation = { navigate: jest.fn() };
11+
12+
// mock Alert
13+
const alertSpy = jest.spyOn(Alert, "alert").mockImplementation(() => {});
14+
15+
// make AsyncStorage fail
16+
AsyncStorage.setItem.mockRejectedValueOnce(new Error("storage failed"));
17+
18+
const { getByPlaceholderText, getByTestId } = render(
19+
<OnboardingScreen navigation={navigation} />
20+
);
21+
22+
fireEvent.changeText(getByPlaceholderText("John"), "John");
23+
fireEvent.changeText(getByPlaceholderText("your@email.com"), "john@example.com");
24+
25+
fireEvent.press(getByTestId("onboarding-submit"));
26+
27+
await waitFor(() => {
28+
expect(alertSpy).toHaveBeenCalled();
29+
});
30+
31+
// Ensure navigation didn't happen on failure
32+
expect(navigation.navigate).not.toHaveBeenCalled();
33+
34+
alertSpy.mockRestore();
35+
});
36+
});

MyProject/__tests__/Onboarding.test.js renamed to MyProject/__tests__/Onboarding/Onboarding.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jest.mock("@react-native-async-storage/async-storage", () =>
99
import AsyncStorage from "@react-native-async-storage/async-storage";
1010

1111
// Import your screen
12-
import OnboardingScreen from "../screens/Onboarding";
12+
import OnboardingScreen from "../../screens/Onboarding";
1313

1414
describe("OnboardingScreen", () => {
1515
beforeEach(() => {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from "react";
2+
import { render, fireEvent } from "@testing-library/react-native";
3+
4+
5+
6+
import OnboardingScreen from "../../screens/Onboarding";
7+
8+
describe("OnboardingScreen validation UI", () => {
9+
test("shows email error message for invalid email", () => {
10+
const navigation = { navigate: jest.fn() };
11+
12+
const { getByPlaceholderText, queryByTestId } = render(
13+
<OnboardingScreen navigation={navigation} />
14+
);
15+
16+
// Initially: no error
17+
expect(queryByTestId("error-email")).toBeNull();
18+
19+
fireEvent.changeText(getByPlaceholderText("your@email.com"), "bad-email");
20+
21+
// Now error should appear
22+
expect(queryByTestId("error-email")).not.toBeNull();
23+
});
24+
25+
test("shows first name error for non-letter characters", () => {
26+
const navigation = { navigate: jest.fn() };
27+
28+
const { getByPlaceholderText, queryByTestId } = render(
29+
<OnboardingScreen navigation={navigation} />
30+
);
31+
32+
expect(queryByTestId("error-firstName")).toBeNull();
33+
34+
fireEvent.changeText(getByPlaceholderText("John"), "John1");
35+
36+
expect(queryByTestId("error-firstName")).not.toBeNull();
37+
});
38+
});

MyProject/jest.setup.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import "@testing-library/jest-native/extend-expect";
2+
3+
jest.mock("@react-native-async-storage/async-storage", () =>
4+
require("@react-native-async-storage/async-storage/jest/async-storage-mock")
5+
);

MyProject/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"jest": {
1414
"preset": "jest-expo",
1515
"setupFilesAfterEnv": [
16-
"@testing-library/jest-native/extend-expect"
16+
"./jest.setup.js"
1717
]
1818
},
1919
"dependencies": {
@@ -44,4 +44,4 @@
4444
"react-test-renderer": "^19.1.0"
4545
},
4646
"private": true
47-
}
47+
}

MyProject/screens/Home.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,13 @@ const HomeScreen = ({ route }) => {
188188
return (
189189
<SafeAreaView style={styles.container}>
190190
<FlatList
191+
testID="home-menu-list"
191192
data={menuItems}
192193
renderItem={renderMenuItem}
193194
keyExtractor={item => item.id.toString()}
194195
ListHeaderComponent={memoizedHeader}
195196
ListEmptyComponent={
196-
<Text style={styles.emptyText}>No items found matching your criteria</Text>
197+
<Text testID="home-empty" style={styles.emptyText}>No items found matching your criteria</Text>
197198
}
198199
/>
199200
</SafeAreaView>

MyProject/screens/Onboarding.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const OnboardingScreen = ({ navigation }) => {
7979
editable={!isLoading}
8080
/>
8181
{!isValidFirstName(firstName) && firstName.length > 0 && (
82-
<Text style={styles.errorText}>
82+
<Text testID="error-firstName" style={styles.errorText}>
8383
<Text>⚠️ </Text>
8484
First name must contain only letters.
8585
</Text>
@@ -104,7 +104,7 @@ const OnboardingScreen = ({ navigation }) => {
104104
editable={!isLoading}
105105
/>
106106
{!isValidEmail(email) && email.length > 0 && (
107-
<Text style={styles.errorText}>
107+
<Text testID="error-email"style={styles.errorText}>
108108
<Text>⚠️ </Text>
109109
Please enter a valid email address.
110110
</Text>

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,20 @@ eas build --platform android
179179
## Testing
180180

181181
## QA / Test Automation
182+
183+
This project includes a complete QA automation setup for a React Native (Expo) application:
184+
182185
- Unit tests: validation utilities (Jest)
183-
- Component tests: Onboarding flow (React Native Testing Library)
184-
- CI: GitHub Actions runs tests on every push / PR
186+
- Component tests: onboarding flow, validation, and error handling
187+
- Data-layer mocking: SQLite/database and AsyncStorage
188+
- UI state testing: loading, empty state, filtering with debounce
189+
- CI: GitHub Actions running tests and coverage on every push / PR
190+
191+
Tools:
192+
- Jest
193+
- React Native Testing Library
194+
- GitHub Actions
195+
185196

186197

187198
### UI Testing Scenarios

0 commit comments

Comments
 (0)