Skip to content

Commit e7bb703

Browse files
authored
Merge branch 'dev' into test/explore-e2e
2 parents d79da87 + 22d1950 commit e7bb703

3 files changed

Lines changed: 233 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2+
import { renderHook, waitFor } from '@testing-library/react-native';
3+
4+
import { useTweetDetail } from '@/hooks/tweets/useTweetDetail';
5+
import { getTweetCache } from '@/libs/tweetCache';
6+
import { getTweet } from '@/services/tweets';
7+
8+
// Mock dependencies
9+
jest.mock('@/services/tweets', () => ({
10+
getTweet: jest.fn(),
11+
}));
12+
13+
jest.mock('@/libs/tweetCache', () => ({
14+
getTweetCache: jest.fn(() => ({
15+
setTweet: jest.fn(),
16+
setTweets: jest.fn(),
17+
})),
18+
}));
19+
20+
describe('useTweetDetail', () => {
21+
let queryClient: QueryClient;
22+
const mockTweetCache = {
23+
setTweet: jest.fn(),
24+
setTweets: jest.fn(),
25+
};
26+
27+
beforeEach(() => {
28+
queryClient = new QueryClient({
29+
defaultOptions: {
30+
queries: {
31+
retry: false,
32+
retryDelay: 0, // No delay between retries for fast testing
33+
},
34+
},
35+
});
36+
jest.clearAllMocks();
37+
(getTweetCache as jest.Mock).mockReturnValue(mockTweetCache);
38+
});
39+
40+
const wrapper = ({ children }: { children: React.ReactNode }) => (
41+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
42+
);
43+
44+
it('fetches and caches tweet successfully', async () => {
45+
const mockTweet = {
46+
id: '123',
47+
content: 'test tweet',
48+
author: { id: 'u1', username: 'user1' },
49+
rootTweet: { id: 'root1', content: 'root tweet' },
50+
parentTweets: [{ id: 'p1', content: 'parent tweet' }],
51+
};
52+
53+
(getTweet as jest.Mock).mockResolvedValue({
54+
success: true,
55+
data: mockTweet,
56+
});
57+
58+
const { result } = renderHook(() => useTweetDetail('123'), { wrapper });
59+
60+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
61+
62+
expect(result.current.data).toEqual(mockTweet);
63+
expect(mockTweetCache.setTweet).toHaveBeenCalledWith(mockTweet);
64+
expect(mockTweetCache.setTweet).toHaveBeenCalledWith(mockTweet.rootTweet);
65+
expect(mockTweetCache.setTweets).toHaveBeenCalledWith(mockTweet.parentTweets);
66+
});
67+
68+
it('does not cache deleted root/parent tweets', async () => {
69+
const mockTweet = {
70+
id: '123',
71+
content: 'test tweet',
72+
rootTweet: { id: 'root1', isDeleted: true },
73+
parentTweets: [
74+
{ id: 'p1', content: 'parent tweet' },
75+
{ id: 'p2', isDeleted: true },
76+
],
77+
};
78+
79+
(getTweet as jest.Mock).mockResolvedValue({
80+
success: true,
81+
data: mockTweet,
82+
});
83+
84+
const { result } = renderHook(() => useTweetDetail('123'), { wrapper });
85+
86+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
87+
88+
expect(mockTweetCache.setTweet).toHaveBeenCalledWith(mockTweet);
89+
expect(mockTweetCache.setTweet).not.toHaveBeenCalledWith(
90+
expect.objectContaining({ id: 'root1' })
91+
);
92+
expect(mockTweetCache.setTweets).toHaveBeenCalledWith([{ id: 'p1', content: 'parent tweet' }]);
93+
});
94+
95+
it('handles 404 error by preventing retry', async () => {
96+
interface ErrorWithStatus extends Error {
97+
status: number;
98+
}
99+
const error = new Error('Not found') as ErrorWithStatus;
100+
error.status = 404;
101+
(getTweet as jest.Mock).mockRejectedValue(error);
102+
103+
const { result } = renderHook(() => useTweetDetail('123'), { wrapper });
104+
105+
await waitFor(() => expect(result.current.isError).toBe(true));
106+
expect(result.current.failureCount).toBe(1); // Should fail once and not retry
107+
});
108+
109+
it('throws error when response is not success', async () => {
110+
(getTweet as jest.Mock).mockResolvedValue({
111+
success: false,
112+
});
113+
114+
const { result } = renderHook(() => useTweetDetail('123'), { wrapper });
115+
116+
await waitFor(() => expect(result.current.isError).toBe(true));
117+
expect(result.current.error).toEqual(new Error('Failed to fetch tweet'));
118+
});
119+
});
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2+
import { renderHook, waitFor } from '@testing-library/react-native';
3+
4+
import { useTweetReplies } from '@/hooks/tweets/useTweetReplies';
5+
import { getTweetCache } from '@/libs/tweetCache';
6+
import { getTweetReplies } from '@/services/tweets';
7+
8+
// Mock dependencies
9+
jest.mock('@/services/tweets', () => ({
10+
getTweetReplies: jest.fn(),
11+
}));
12+
13+
jest.mock('@/libs/tweetCache', () => ({
14+
getTweetCache: jest.fn(() => ({
15+
setTweets: jest.fn(),
16+
})),
17+
}));
18+
19+
describe('useTweetReplies', () => {
20+
let queryClient: QueryClient;
21+
const mockTweetCache = {
22+
setTweets: jest.fn(),
23+
};
24+
25+
beforeEach(() => {
26+
queryClient = new QueryClient({
27+
defaultOptions: {
28+
queries: {
29+
retry: false,
30+
},
31+
},
32+
});
33+
jest.clearAllMocks();
34+
(getTweetCache as jest.Mock).mockReturnValue(mockTweetCache);
35+
});
36+
37+
const wrapper = ({ children }: { children: React.ReactNode }) => (
38+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
39+
);
40+
41+
it('fetches and caches first page of replies', async () => {
42+
const mockReplies = [{ id: 'r1', content: 'reply 1' }];
43+
(getTweetReplies as jest.Mock).mockResolvedValue({
44+
data: mockReplies,
45+
pagination: { hasNextPage: true, nextCursor: 'next-cursor' },
46+
});
47+
48+
const { result } = renderHook(() => useTweetReplies('123'), { wrapper });
49+
50+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
51+
52+
expect(result.current.data?.pages[0].data).toEqual(mockReplies);
53+
expect(mockTweetCache.setTweets).toHaveBeenCalledWith(mockReplies);
54+
});
55+
56+
it('fetches next page with cursor', async () => {
57+
// Setup mocks for sequential calls
58+
(getTweetReplies as jest.Mock)
59+
.mockResolvedValueOnce({
60+
data: [{ id: 'r1', content: 'reply 1' }],
61+
pagination: { hasNextPage: true, nextCursor: 'page2' },
62+
})
63+
.mockResolvedValueOnce({
64+
data: [{ id: 'r2', content: 'reply 2' }],
65+
pagination: { hasNextPage: false },
66+
});
67+
68+
const { result } = renderHook(() => useTweetReplies('123'), { wrapper });
69+
70+
// Wait for first page
71+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
72+
expect(result.current.data?.pages).toHaveLength(1);
73+
74+
// Trigger next page
75+
await result.current.fetchNextPage();
76+
77+
// Wait for the update
78+
await waitFor(() => expect(result.current.data?.pages).toHaveLength(2));
79+
80+
// Verify calls
81+
expect(getTweetReplies).toHaveBeenCalledTimes(2);
82+
expect(getTweetReplies).toHaveBeenLastCalledWith(
83+
'123',
84+
expect.objectContaining({ cursor: 'page2' })
85+
);
86+
});
87+
88+
it('handles empty response gracefully', async () => {
89+
(getTweetReplies as jest.Mock).mockResolvedValue({
90+
data: [],
91+
pagination: { hasNextPage: false },
92+
});
93+
94+
const { result } = renderHook(() => useTweetReplies('123'), { wrapper });
95+
96+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
97+
expect(result.current.data?.pages[0].data).toEqual([]);
98+
});
99+
});

src/__tests__/screens/settings/SettingsScreen.test.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const mockNavigate = jest.fn();
77
jest.mock('@react-navigation/native', () => ({
88
useNavigation: () => ({
99
push: mockNavigate,
10+
navigate: mockNavigate,
1011
}),
1112
}));
1213

@@ -47,4 +48,18 @@ describe('SettingsScreen', () => {
4748
fireEvent.press(getByText('Appearance'));
4849
expect(mockNavigate).toHaveBeenCalledWith('Appearance');
4950
});
51+
52+
it('navigates to Terms of Service screen when ToS card is pressed', () => {
53+
const { getByText } = renderWithTheme(<SettingsScreen />);
54+
55+
fireEvent.press(getByText('Terms of Service'));
56+
expect(mockNavigate).toHaveBeenCalledWith('TermsOfService');
57+
});
58+
59+
it('navigates to Privacy Policy screen when Privacy Policy card is pressed', () => {
60+
const { getByText } = renderWithTheme(<SettingsScreen />);
61+
62+
fireEvent.press(getByText('Privacy Policy'));
63+
expect(mockNavigate).toHaveBeenCalledWith('PrivacyPolicy');
64+
});
5065
});

0 commit comments

Comments
 (0)