Skip to content

Commit e0d5006

Browse files
authored
feat: add tests for useFeed and Tweet Component - [CU-869bgekwc] (#249)
1 parent 841c4be commit e0d5006

6 files changed

Lines changed: 452 additions & 12 deletions

File tree

src/__tests__/components/ui/Tweet.test.tsx

Lines changed: 348 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useUserStore } from '@/stores/userStore';
1111

1212
import type { Tweet as TweetType } from '@/types/tweet';
1313

14-
const { deleteTweet } = tweetsService;
14+
const { deleteTweet, getTweetSummary } = tweetsService;
1515

1616
const mockFollowMutation = {
1717
mutate: jest.fn(),
@@ -76,6 +76,7 @@ jest.mock('@/services/tweets', () => ({
7676
retweetTweet: jest.fn().mockResolvedValue({}),
7777
unretweetTweet: jest.fn().mockResolvedValue({}),
7878
deleteTweet: jest.fn(),
79+
getTweetSummary: jest.fn(),
7980
}));
8081

8182
jest.mock('@expo/vector-icons/FontAwesome6', () => 'FontAwesome6');
@@ -1666,3 +1667,349 @@ describe('follow button visibility in video viewer', () => {
16661667
expect(queryByTestId('viewer-follow-button')).toBeNull();
16671668
});
16681669
});
1670+
1671+
describe('AI summary functionality', () => {
1672+
beforeEach(() => {
1673+
act(() => {
1674+
const storeState = useUserStore.getState();
1675+
storeState.resetUser();
1676+
useUserStore.setState({
1677+
user: {
1678+
...useUserStore.getState().user,
1679+
username: 'viewer-user',
1680+
displayName: 'Viewer User',
1681+
},
1682+
});
1683+
});
1684+
});
1685+
1686+
afterEach(() => {
1687+
jest.restoreAllMocks();
1688+
jest.clearAllMocks();
1689+
if (testQueryClient) {
1690+
testQueryClient.clear();
1691+
}
1692+
1693+
act(() => {
1694+
useUserStore.getState().resetUser();
1695+
});
1696+
});
1697+
1698+
it('fetches and displays AI summary when grok button is pressed in detailed view', async () => {
1699+
(getTweetSummary as jest.Mock).mockResolvedValueOnce({
1700+
success: true,
1701+
data: { summary: 'This is an AI generated summary of the tweet.' },
1702+
});
1703+
1704+
const { getByTestId, getByText } = renderWithProv(<Tweet tweet={baseTweet} detailed />);
1705+
1706+
fireEvent.press(getByTestId('grok-summary-button'));
1707+
1708+
await waitFor(() => {
1709+
expect(getTweetSummary).toHaveBeenCalledWith(baseTweet.id);
1710+
});
1711+
1712+
await waitFor(() => {
1713+
getByText('This is an AI generated summary of the tweet.');
1714+
});
1715+
});
1716+
1717+
it('shows error state when AI summary fails', async () => {
1718+
(getTweetSummary as jest.Mock).mockResolvedValueOnce({
1719+
success: false,
1720+
data: null,
1721+
});
1722+
1723+
const { getByTestId, getByText } = renderWithProv(<Tweet tweet={baseTweet} detailed />);
1724+
1725+
fireEvent.press(getByTestId('grok-summary-button'));
1726+
1727+
await waitFor(() => {
1728+
getByText('Failed to generate summary');
1729+
});
1730+
});
1731+
1732+
it('shows error state when getTweetSummary throws', async () => {
1733+
(getTweetSummary as jest.Mock).mockRejectedValueOnce(new Error('Network error'));
1734+
1735+
const { getByTestId, getByText } = renderWithProv(<Tweet tweet={baseTweet} detailed />);
1736+
1737+
fireEvent.press(getByTestId('grok-summary-button'));
1738+
1739+
await waitFor(() => {
1740+
getByText('Failed to generate summary');
1741+
});
1742+
});
1743+
1744+
it('does not show grok button when tweet has empty content', () => {
1745+
const emptyContentTweet: TweetType = {
1746+
...baseTweet,
1747+
content: ' ',
1748+
};
1749+
1750+
const { queryByTestId } = renderWithProv(<Tweet tweet={emptyContentTweet} detailed />);
1751+
1752+
expect(queryByTestId('grok-summary-button')).toBeNull();
1753+
});
1754+
});
1755+
1756+
describe('Reply-to-tweet and deleted quoted tweet', () => {
1757+
beforeEach(() => {
1758+
act(() => {
1759+
const storeState = useUserStore.getState();
1760+
storeState.resetUser();
1761+
useUserStore.setState({
1762+
user: {
1763+
...useUserStore.getState().user,
1764+
username: 'viewer-user',
1765+
displayName: 'Viewer User',
1766+
},
1767+
});
1768+
});
1769+
});
1770+
1771+
afterEach(() => {
1772+
jest.restoreAllMocks();
1773+
jest.clearAllMocks();
1774+
if (testQueryClient) {
1775+
testQueryClient.clear();
1776+
}
1777+
1778+
act(() => {
1779+
useUserStore.getState().resetUser();
1780+
});
1781+
});
1782+
1783+
it('displays reply-to info in list view and navigates to author on press', () => {
1784+
const onPressAuthor = jest.fn();
1785+
const tweetWithReply: TweetType = {
1786+
...baseTweet,
1787+
replyToTweet: {
1788+
id: 'reply-parent',
1789+
author: {
1790+
username: 'original_author',
1791+
displayName: 'Original Author',
1792+
avatarUrl: null,
1793+
relationship: {
1794+
blocking: false,
1795+
blockedBy: false,
1796+
muted: false,
1797+
following: false,
1798+
follower: false,
1799+
},
1800+
},
1801+
content: 'Original tweet content',
1802+
createdAt: new Date().toISOString(),
1803+
replyCount: 0,
1804+
retweetCount: 0,
1805+
likeCount: 0,
1806+
isLiked: false,
1807+
isRetweeted: false,
1808+
entities: { mentions: null, hashtags: null },
1809+
media: null,
1810+
},
1811+
};
1812+
1813+
const { getByText } = renderWithProv(
1814+
<Tweet tweet={tweetWithReply} onPressAuthor={onPressAuthor} />
1815+
);
1816+
1817+
getByText('Replying to');
1818+
const replyAuthor = getByText('@original_author');
1819+
fireEvent.press(replyAuthor);
1820+
1821+
expect(onPressAuthor).toHaveBeenCalledWith('original_author');
1822+
});
1823+
1824+
it('renders deleted quoted tweet message in list view', () => {
1825+
const tweetWithDeletedQuote: TweetType = {
1826+
...baseTweet,
1827+
quotedTweet: { isDeleted: true },
1828+
};
1829+
1830+
const { getByText } = renderWithProv(<Tweet tweet={tweetWithDeletedQuote} />);
1831+
1832+
getByText('This tweet was deleted.');
1833+
});
1834+
1835+
it('renders deleted quoted tweet message in detailed view', () => {
1836+
const tweetWithDeletedQuote: TweetType = {
1837+
...baseTweet,
1838+
quotedTweet: { isDeleted: true },
1839+
};
1840+
1841+
const { getByText } = renderWithProv(<Tweet tweet={tweetWithDeletedQuote} detailed />);
1842+
1843+
getByText('This tweet was deleted.');
1844+
});
1845+
});
1846+
1847+
describe('Delete tweet error handling', () => {
1848+
beforeEach(() => {
1849+
act(() => {
1850+
const storeState = useUserStore.getState();
1851+
storeState.resetUser();
1852+
useUserStore.setState({
1853+
user: {
1854+
...useUserStore.getState().user,
1855+
username: 'viewer-user',
1856+
displayName: 'Viewer User',
1857+
},
1858+
});
1859+
});
1860+
});
1861+
1862+
afterEach(() => {
1863+
jest.restoreAllMocks();
1864+
jest.clearAllMocks();
1865+
if (testQueryClient) {
1866+
testQueryClient.clear();
1867+
}
1868+
1869+
act(() => {
1870+
useUserStore.getState().resetUser();
1871+
});
1872+
});
1873+
1874+
it('shows alert when delete tweet fails', async () => {
1875+
const alertSpy = jest.spyOn(Alert, 'alert').mockImplementation(() => {});
1876+
(deleteTweet as jest.Mock).mockRejectedValueOnce(new Error('Delete failed'));
1877+
1878+
const { getByTestId, getByText } = renderWithProv(
1879+
<Tweet
1880+
tweet={{
1881+
...baseTweet,
1882+
author: { ...baseTweet.author, username: 'viewer-user', displayName: 'Viewer User' },
1883+
}}
1884+
/>
1885+
);
1886+
1887+
const drawerButton = getByTestId('tweet-drawer');
1888+
fireEvent.press(drawerButton);
1889+
1890+
const deleteButton = getByText('Delete post');
1891+
act(() => {
1892+
fireEvent.press(deleteButton);
1893+
});
1894+
1895+
const confirmButton = getByTestId('dialog-confirm-button');
1896+
act(() => {
1897+
fireEvent.press(confirmButton);
1898+
});
1899+
1900+
await waitFor(() => {
1901+
expect(alertSpy).toHaveBeenCalledWith('Unable to delete tweet', 'Delete failed');
1902+
});
1903+
});
1904+
});
1905+
1906+
describe('Quoted tweet detailed view interactions', () => {
1907+
beforeEach(() => {
1908+
act(() => {
1909+
const storeState = useUserStore.getState();
1910+
storeState.resetUser();
1911+
useUserStore.setState({
1912+
user: {
1913+
...useUserStore.getState().user,
1914+
username: 'viewer-user',
1915+
displayName: 'Viewer User',
1916+
},
1917+
});
1918+
});
1919+
});
1920+
1921+
afterEach(() => {
1922+
jest.restoreAllMocks();
1923+
jest.clearAllMocks();
1924+
if (testQueryClient) {
1925+
testQueryClient.clear();
1926+
}
1927+
1928+
act(() => {
1929+
useUserStore.getState().resetUser();
1930+
});
1931+
});
1932+
1933+
it('navigates to quoted tweet when pressed in detailed view', () => {
1934+
const onPress = jest.fn();
1935+
const quotedTweet: NonNullable<TweetType['quotedTweet']> = {
1936+
id: 'qt-detailed-press',
1937+
author: {
1938+
username: 'quoted_author',
1939+
displayName: 'Quoted Author',
1940+
avatarUrl: null,
1941+
relationship: {
1942+
blocking: false,
1943+
blockedBy: false,
1944+
muted: false,
1945+
following: false,
1946+
follower: false,
1947+
},
1948+
},
1949+
content: 'Press this quoted tweet',
1950+
createdAt: new Date().toISOString(),
1951+
replyCount: 0,
1952+
retweetCount: 0,
1953+
likeCount: 0,
1954+
isLiked: false,
1955+
isRetweeted: false,
1956+
entities: { mentions: null, hashtags: null },
1957+
media: null,
1958+
};
1959+
1960+
const richTweet: TweetType = {
1961+
...baseTweet,
1962+
quotedTweet,
1963+
};
1964+
1965+
const { getByText } = renderWithProv(<Tweet tweet={richTweet} detailed onPress={onPress} />);
1966+
1967+
fireEvent.press(getByText('Press this quoted tweet'));
1968+
expect(onPress).toHaveBeenCalledWith('qt-detailed-press');
1969+
});
1970+
1971+
it('opens viewer for quoted tweet media in list view without custom handler', () => {
1972+
const quotedTweet: NonNullable<TweetType['quotedTweet']> = {
1973+
id: 'qt-media-list',
1974+
author: {
1975+
username: 'quoted',
1976+
displayName: 'Quoted',
1977+
avatarUrl: null,
1978+
relationship: {
1979+
blocking: false,
1980+
blockedBy: false,
1981+
muted: false,
1982+
following: false,
1983+
follower: false,
1984+
},
1985+
},
1986+
content: 'Quoted media tweet',
1987+
createdAt: new Date().toISOString(),
1988+
replyCount: 0,
1989+
retweetCount: 0,
1990+
likeCount: 0,
1991+
isLiked: false,
1992+
isRetweeted: false,
1993+
entities: { mentions: null, hashtags: null },
1994+
media: [
1995+
{
1996+
type: 'IMAGE',
1997+
url: 'https://example.com/quoted-media.jpg',
1998+
altText: 'Quoted media',
1999+
width: 800,
2000+
height: 600,
2001+
},
2002+
],
2003+
};
2004+
2005+
const tweetWithQuotedMedia: TweetType = {
2006+
...baseTweet,
2007+
quotedTweet,
2008+
};
2009+
2010+
const { getByTestId } = renderWithProv(<Tweet tweet={tweetWithQuotedMedia} />);
2011+
2012+
fireEvent.press(getByTestId('tweet-media-0'));
2013+
getByTestId('media-viewer');
2014+
});
2015+
});

0 commit comments

Comments
 (0)