@@ -11,7 +11,7 @@ import { useUserStore } from '@/stores/userStore';
1111
1212import type { Tweet as TweetType } from '@/types/tweet' ;
1313
14- const { deleteTweet } = tweetsService ;
14+ const { deleteTweet, getTweetSummary } = tweetsService ;
1515
1616const 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
8182jest . 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