import { getScraper } from './test-utils'; import { QueryTweetsResponse } from './timeline-v1'; import { Mention, Tweet } from './tweets'; import fs from 'fs'; import path from 'path'; let shouldSkipV2Tests = false; beforeAll(() => { const { TWITTER_API_KEY, TWITTER_API_SECRET_KEY, TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET, } = process.env; if ( !TWITTER_API_KEY || !TWITTER_API_SECRET_KEY || !TWITTER_ACCESS_TOKEN || !TWITTER_ACCESS_TOKEN_SECRET ) { console.warn( 'Skipping tests: Twitter API v2 keys are not available in environment variables.', ); shouldSkipV2Tests = true; } }); test('scraper can get tweet', async () => { const expected: Tweet = { conversationId: '1585338303800578049', html: `We’re updating Twitter’s sounds to help make them pleasing to more people, including those with sensory sensitivities. Here’s more on how we did it:
https://t.co/7FKWk7NzHM`, id: '1585338303800578049', hashtags: [], mentions: [], name: 'A11y', permanentUrl: 'https://twitter.com/XA11y/status/1585338303800578049', photos: [], text: 'We’re updating Twitter’s sounds to help make them pleasing to more people, including those with sensory sensitivities. Here’s more on how we did it:\nhttps://t.co/7FKWk7NzHM', thread: [], timeParsed: new Date(Date.UTC(2022, 9, 26, 18, 31, 20, 0)), timestamp: 1666809080, urls: [ 'https://blog.twitter.com/en_us/topics/product/2022/designing-accessible-sounds-story-behind-our-new-chirps', ], userId: '1631299117', username: 'XA11y', videos: [], isQuoted: false, isReply: false, isRetweet: false, isPin: false, sensitiveContent: false, }; const scraper = await getScraper(); const actual = await scraper.getTweet('1585338303800578049'); delete actual?.likes; delete actual?.replies; delete actual?.retweets; delete actual?.views; delete actual?.bookmarkCount; expect(actual).toEqual(expected); }); test('scraper can get tweets without logging in', async () => { const scraper = await getScraper({ authMethod: 'anonymous' }); let counter = 0; for await (const tweet of scraper.getTweets('elonmusk', 10)) { if (tweet) { counter++; } } expect(counter).toBeGreaterThanOrEqual(1); }); test('scraper can get tweets from list', async () => { const scraper = await getScraper(); let cursor: string | undefined = undefined; const maxTweets = 30; let nTweets = 0; while (nTweets < maxTweets) { const res: QueryTweetsResponse = await scraper.fetchListTweets( '1736495155002106192', maxTweets, cursor, ); expect(res.next).toBeTruthy(); nTweets += res.tweets.length; cursor = res.next; } }); test('scraper can get first tweet matching query', async () => { const scraper = await getScraper(); const timeline = scraper.getTweets('elonmusk'); const latestQuote = await scraper.getTweetWhere(timeline, { isQuoted: true }); expect(latestQuote?.isQuoted).toBeTruthy(); }); test('scraper can get all tweets matching query', async () => { const scraper = await getScraper(); // Sample size of 20 should be enough without taking long. const timeline = scraper.getTweets('elonmusk', 20); const retweets = await scraper.getTweetsWhere( timeline, (tweet) => tweet.isRetweet === true, ); expect(retweets).toBeTruthy(); for (const tweet of retweets) { expect(tweet.isRetweet).toBe(true); } }, 20000); test('scraper can get latest tweet', async () => { const scraper = await getScraper(); // OLD APPROACH (without retweet filtering) const tweets = scraper.getTweets('elonmusk', 1); const expected = (await tweets.next()).value; // NEW APPROACH const latest = (await scraper.getLatestTweet( 'elonmusk', expected?.isRetweet || false, )) as Tweet; expect(latest?.permanentUrl).toEqual(expected?.permanentUrl); }, 30000); test('scraper can get user mentions in tweets', async () => { const expected: Mention[] = [ { id: '7018222', username: 'davidmcraney', name: 'David McRaney', }, ]; const scraper = await getScraper(); const tweet = await scraper.getTweet('1554522888904101890'); expect(tweet?.mentions).toEqual(expected); }); test('scraper can get tweet quotes without logging in', async () => { const expected: Tweet = { conversationId: '1237110546383724547', html: `The Easiest Problem Everyone Gets Wrong

[new video] --> https://t.co/YdaeDYmPAU
`, id: '1237110546383724547', hashtags: [], mentions: [], name: 'Vsauce2', permanentUrl: 'https://twitter.com/VsauceTwo/status/1237110546383724547', photos: [ { id: '1237110473486729218', url: 'https://pbs.twimg.com/media/ESsZa9AXgAIAYnF.jpg', alt_text: undefined, }, ], text: 'The Easiest Problem Everyone Gets Wrong \n\n[new video] --> https://t.co/YdaeDYmPAU https://t.co/iKu4Xs6o2V', thread: [], timeParsed: new Date(Date.UTC(2020, 2, 9, 20, 18, 33, 0)), timestamp: 1583785113, urls: ['https://youtu.be/ytfCdqWhmdg'], userId: '978944851', username: 'VsauceTwo', videos: [], isQuoted: false, isReply: false, isRetweet: false, isPin: false, sensitiveContent: false, }; const scraper = await getScraper({ authMethod: 'anonymous' }); const quote = await scraper.getTweet('1237110897597976576'); expect(quote?.isQuoted).toBeTruthy(); delete quote?.quotedStatus?.likes; delete quote?.quotedStatus?.replies; delete quote?.quotedStatus?.retweets; delete quote?.quotedStatus?.views; delete quote?.quotedStatus?.bookmarkCount; expect(quote?.quotedStatus).toEqual(expected); }); test('scraper can get tweet quotes and replies', async () => { const expected: Tweet = { conversationId: '1237110546383724547', html: `The Easiest Problem Everyone Gets Wrong

[new video] --> https://t.co/YdaeDYmPAU
`, id: '1237110546383724547', hashtags: [], mentions: [], name: 'Vsauce2', permanentUrl: 'https://twitter.com/VsauceTwo/status/1237110546383724547', photos: [ { id: '1237110473486729218', url: 'https://pbs.twimg.com/media/ESsZa9AXgAIAYnF.jpg', alt_text: undefined, }, ], text: 'The Easiest Problem Everyone Gets Wrong \n\n[new video] --> https://t.co/YdaeDYmPAU https://t.co/iKu4Xs6o2V', thread: [], timeParsed: new Date(Date.UTC(2020, 2, 9, 20, 18, 33, 0)), timestamp: 1583785113, urls: ['https://youtu.be/ytfCdqWhmdg'], userId: '978944851', username: 'VsauceTwo', videos: [], isQuoted: false, isReply: false, isRetweet: false, isPin: false, sensitiveContent: false, }; const scraper = await getScraper(); const quote = await scraper.getTweet('1237110897597976576'); expect(quote?.isQuoted).toBeTruthy(); delete quote?.quotedStatus?.likes; delete quote?.quotedStatus?.replies; delete quote?.quotedStatus?.retweets; delete quote?.quotedStatus?.views; delete quote?.quotedStatus?.bookmarkCount; expect(quote?.quotedStatus).toEqual(expected); const reply = await scraper.getTweet('1237111868445134850'); expect(reply?.isReply).toBeTruthy(); if (reply != null) { reply.isReply = false; } delete reply?.inReplyToStatus?.likes; delete reply?.inReplyToStatus?.replies; delete reply?.inReplyToStatus?.retweets; delete reply?.inReplyToStatus?.views; delete reply?.inReplyToStatus?.bookmarkCount; expect(reply?.inReplyToStatus).toEqual(expected); }); test('scraper can get retweet', async () => { const expected: Tweet = { conversationId: '1776276954435481937', html: `
`, id: '1776276954435481937', hashtags: [], mentions: [], name: 'federico.', permanentUrl: 'https://twitter.com/federicosmos/status/1776276954435481937', photos: [], text: 'https://t.co/qqiu5ntffp', thread: [], timeParsed: new Date(Date.UTC(2024, 3, 5, 15, 53, 22, 0)), timestamp: 1712332402, urls: [], userId: '2376017065', username: 'federicosmos', videos: [ { id: '1776276900580622336', preview: 'https://pbs.twimg.com/amplify_video_thumb/1776276900580622336/img/UknAtyWSZ286nCD3.jpg', url: 'https://video.twimg.com/amplify_video/1776276900580622336/vid/avc1/640x360/uACt_egp_hmvPOZF.mp4?tag=14', }, ], isQuoted: false, isReply: false, isRetweet: false, isPin: false, sensitiveContent: false, }; const scraper = await getScraper(); const retweet = await scraper.getTweet('1776285549566808397'); expect(retweet?.isRetweet).toBeTruthy(); delete retweet?.retweetedStatus?.likes; delete retweet?.retweetedStatus?.replies; delete retweet?.retweetedStatus?.retweets; delete retweet?.retweetedStatus?.views; delete retweet?.retweetedStatus?.bookmarkCount; expect(retweet?.retweetedStatus).toEqual(expected); }); test('scraper can get tweet views', async () => { const expected: Tweet = { conversationId: '1606055187348688896', html: `Replies and likes don’t tell the whole story. We’re making it easier to tell *just* how many people have seen your Tweets with the addition of view counts, shown right next to likes. Now on iOS and Android, web coming soon.

https://t.co/hrlMQyXJfx`, id: '1606055187348688896', hashtags: [], mentions: [], name: 'Support', permanentUrl: 'https://twitter.com/Support/status/1606055187348688896', photos: [], text: 'Replies and likes don’t tell the whole story. We’re making it easier to tell *just* how many people have seen your Tweets with the addition of view counts, shown right next to likes. Now on iOS and Android, web coming soon.\n\nhttps://t.co/hrlMQyXJfx', thread: [], timeParsed: new Date(Date.UTC(2022, 11, 22, 22, 32, 50, 0)), timestamp: 1671748370, urls: ['https://help.twitter.com/using-twitter/view-counts'], userId: '17874544', username: 'Support', videos: [], isQuoted: false, isReply: false, isRetweet: false, isPin: false, sensitiveContent: false, }; const scraper = await getScraper(); const actual = await scraper.getTweet('1606055187348688896'); expect(actual?.views).toBeTruthy(); delete actual?.likes; delete actual?.replies; delete actual?.retweets; delete actual?.views; delete actual?.bookmarkCount; expect(actual).toEqual(expected); }); test('scraper can get tweet thread', async () => { const scraper = await getScraper(); const tweet = await scraper.getTweet('1665602315745673217'); expect(tweet).not.toBeNull(); expect(tweet?.isSelfThread).toBeTruthy(); expect(tweet?.thread.length).toStrictEqual(7); }); test('scraper can get user tweets', async () => { const scraper = await getScraper(); const userId = '1830340867737178112'; // Replace with a valid user ID const maxTweets = 200; const response = await scraper.getUserTweets(userId, maxTweets); expect(response.tweets).toBeDefined(); expect(response.tweets.length).toBeLessThanOrEqual(maxTweets); // Check if each object in the tweets array is a valid Tweet object response.tweets.forEach((tweet) => { expect(tweet.id).toBeDefined(); expect(tweet.text).toBeDefined(); }); expect(response.next).toBeDefined(); }, 30000); test('sendTweet successfully sends a tweet', async () => { const scraper = await getScraper(); const draftText = 'Core updated on ' + Date.now().toString(); const result = await scraper.sendTweet(draftText); console.log('Send tweet result:', result); const replyResult = await scraper.sendTweet( 'Ignore this', '1430277451452751874', ); console.log('Send reply result:', replyResult); }); test('scraper can get a tweet with getTweetV2', async () => { const scraper = await getScraper({ authMethod: 'api' }); if (shouldSkipV2Tests) { return console.warn("Skipping 'getTweetV2' test due to missing API keys."); } const tweetId = '1856441982811529619'; const tweet: Tweet | null = await scraper.getTweetV2(tweetId); expect(tweet).not.toBeNull(); expect(tweet?.id).toEqual(tweetId); expect(tweet?.text).toBeDefined(); }); test('scraper can get multiple tweets with getTweetsV2', async () => { if (shouldSkipV2Tests) { return console.warn("Skipping 'getTweetV2' test due to missing API keys."); } const scraper = await getScraper({ authMethod: 'api' }); const tweetIds = ['1856441982811529619', '1856429655215260130']; const tweets = await scraper.getTweetsV2(tweetIds); expect(tweets).toBeDefined(); expect(tweets.length).toBeGreaterThan(0); tweets.forEach((tweet) => { expect(tweet.id).toBeDefined(); expect(tweet.text).toBeDefined(); }); }); test('scraper can send a tweet with sendTweetV2', async () => { if (shouldSkipV2Tests) { return console.warn("Skipping 'getTweetV2' test due to missing API keys."); } const scraper = await getScraper({ authMethod: 'api' }); const tweetText = `Automated test tweet at ${Date.now()}`; const response = await scraper.sendTweetV2(tweetText); expect(response).not.toBeNull(); expect(response?.id).toBeDefined(); expect(response?.text).toEqual(tweetText); }); test('scraper can create a poll with sendTweetV2', async () => { if (shouldSkipV2Tests) { return console.warn("Skipping 'getTweetV2' test due to missing API keys."); } const scraper = await getScraper({ authMethod: 'api' }); const tweetText = `When do you think we'll achieve AGI (Artificial General Intelligence)? 🤖 Cast your prediction!`; const pollData = { poll: { options: [ { label: '2025 🗓️' }, { label: '2026 📅' }, { label: '2027 🛠️' }, { label: '2030+ 🚀' }, ], duration_minutes: 1440, }, }; const response = await scraper.sendTweetV2(tweetText, undefined, pollData); expect(response).not.toBeNull(); expect(response?.id).toBeDefined(); expect(response?.text).toEqual(tweetText); // Validate poll structure in response const poll = response?.poll; expect(poll).toBeDefined(); expect(poll?.options.map((option) => option.label)).toEqual( pollData?.poll.options.map((option) => option.label), ); }); test('scraper can send a tweet without media', async () => { const scraper = await getScraper(); const draftText = 'Test tweet without media ' + Date.now().toString(); // Send a tweet without any media attachments const result = await scraper.sendTweet(draftText); // Log and verify the result console.log('Send tweet without media result:', result); expect(result.ok).toBeTruthy(); }, 30000); test('scraper can send a tweet with image and video', async () => { const scraper = await getScraper(); const draftText = 'Test tweet with image and video ' + Date.now().toString(); // Read test image and video files from the test-assets directory const imageBuffer = fs.readFileSync( path.join(__dirname, '../test-assets/test-image.jpeg') ); const videoBuffer = fs.readFileSync( path.join(__dirname, '../test-assets/test-video.mp4') ); // Prepare media data array with both image and video const mediaData = [ { data: imageBuffer, mediaType: 'image/jpeg' }, { data: videoBuffer, mediaType: 'video/mp4' }, ]; // Send a tweet with both image and video attachments const result = await scraper.sendTweet(draftText, undefined, mediaData); // Log and verify the result console.log('Send tweet with image and video result:', result); expect(result.ok).toBeTruthy(); }, 30000); test('scraper can quote tweet without media', async () => { const scraper = await getScraper(); const quotedTweetId = '1776276954435481937'; const quoteText = `Testing quote tweet without media ${Date.now()}`; // Send a quote tweet without any media attachments const response = await scraper.sendQuoteTweet(quoteText, quotedTweetId); // Log and verify the response console.log('Quote tweet without media result:', response); expect(response.ok).toBeTruthy(); }, 30000); test('scraper can quote tweet with image and video', async () => { const scraper = await getScraper(); const quotedTweetId = '1776276954435481937'; const quoteText = `Testing quote tweet with image and video ${Date.now()}`; // Read test image and video files from the test-assets directory const imageBuffer = fs.readFileSync( path.join(__dirname, '../test-assets/test-image.jpeg') ); const videoBuffer = fs.readFileSync( path.join(__dirname, '../test-assets/test-video.mp4') ); // Prepare media data array with both image and video const mediaData = [ { data: imageBuffer, mediaType: 'image/jpeg' }, { data: videoBuffer, mediaType: 'video/mp4' }, ]; // Send a quote tweet with both image and video attachments const response = await scraper.sendQuoteTweet(quoteText, quotedTweetId, { mediaData: mediaData, }); // Log and verify the response console.log('Quote tweet with image and video result:', response); expect(response.ok).toBeTruthy(); }, 30000); test('scraper can quote tweet with media', async () => { const scraper = await getScraper(); const quotedTweetId = '1776276954435481937'; const quoteText = `Testing quote tweet with media ${Date.now()}`; // Read test image file const imageBuffer = fs.readFileSync( path.join(__dirname, '../test-assets/test-image.jpeg') ); // Prepare media data with the image const mediaData = [ { data: imageBuffer, mediaType: 'image/jpeg' }, ]; // Send a quote tweet with the image attachment const response = await scraper.sendQuoteTweet(quoteText, quotedTweetId, { mediaData: mediaData, }); // Log and verify the response console.log('Quote tweet with media result:', response); expect(response.ok).toBeTruthy(); }, 30000); test('sendTweetWithMedia successfully sends a tweet with media', async () => { const scraper = await getScraper(); const draftText = 'Test tweet with media ' + Date.now().toString(); // Read a test image file const imageBuffer = fs.readFileSync( path.join(__dirname, '../test-assets/test-image.jpeg') ); // Prepare media data with the image const mediaData = [ { data: imageBuffer, mediaType: 'image/jpeg' }, ]; // Send a tweet with the image attachment const result = await scraper.sendTweet(draftText, undefined, mediaData); // Log and verify the result console.log('Send tweet with media result:', result); expect(result.ok).toBeTruthy(); }, 30000); test('scraper can like a tweet', async () => { const scraper = await getScraper(); const tweetId = '1776276954435481937'; // Use a real tweet ID for testing // Test should not throw an error await expect(scraper.likeTweet(tweetId)).resolves.not.toThrow(); }); test('scraper can retweet', async () => { const scraper = await getScraper(); const tweetId = '1776276954435481937'; // Test should not throw an error await expect(scraper.retweet(tweetId)).resolves.not.toThrow(); }); test('scraper can follow user', async () => { const scraper = await getScraper(); const username = 'elonmusk'; // Use a real username for testing // Test should not throw an error await expect(scraper.followUser(username)).resolves.not.toThrow(); }, 30000);