| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- from base import BaseTestCase
- from parameterized import parameterized
- class ArticleSelectors:
- page = '.article-page'
- cover = '.article-cover'
- body = '.article-body'
- title = '.article-title'
- author = '.article-author'
- fullname = '.article-author .fullname'
- username = '.article-author .username'
- date = '.article-author .article-date'
- avatar = '.article-author img.avatar'
- verified = '.article-author .verified-icon'
- media = '.article-media'
- caption = '.article-media-caption'
- divider = '.article-divider'
- articles = [
- ['2064166507438059759',
- '1s,秒杀一切,开源一个 X 文章发布 Skill【重磅升级】',
- 'punk2898', 'Punk'],
- ['2064689664213041529',
- 'SpaceX Thesis & Valuation Memorandum',
- 'Dialectic_Group', 'Dialectic'],
- ['2064691088636424322',
- 'Consciousness and AI: The Problem of Inner Experience',
- 'CosmicOrFun', 'Cosmic Orphan'],
- ['2064755789391110154',
- 'DeFi Markets Update 2026-06-10',
- 'SteakhouseFi', 'Steakhouse Financial'],
- ['2064755231901319527',
- 'The machine economy has a killswitch and somebody just pulled it.',
- '1914ad', 'Justin Bechler HMP-028'],
- ['2062858677149675788',
- 'Yakshinis',
- 'CosmicOrFun', 'Cosmic Orphan'],
- ]
- articles_with_media = [
- ['2064166507438059759', 6],
- ['2064689664213041529', 11],
- ['2064755789391110154', 5],
- ]
- articles_with_dividers = [
- ['2064166507438059759', 1],
- ['2064689664213041529', 6],
- ]
- class ArticleBasicTest(BaseTestCase):
- @parameterized.expand(articles)
- def test_article_loads(self, tweet_id, title, username, fullname):
- self.open_nitter(f'i/article/{tweet_id}')
- self.assert_element_visible(ArticleSelectors.page)
- self.assert_element_visible(ArticleSelectors.body)
- self.assert_text(title, ArticleSelectors.title)
- @parameterized.expand(articles)
- def test_article_author(self, tweet_id, title, username, fullname):
- self.open_nitter(f'i/article/{tweet_id}')
- self.assert_element_visible(ArticleSelectors.author)
- self.assert_text(f'@{username}', ArticleSelectors.username)
- @parameterized.expand(articles)
- def test_article_has_cover(self, tweet_id, title, username, fullname):
- self.open_nitter(f'i/article/{tweet_id}')
- self.assert_element_visible(ArticleSelectors.cover)
- src = self.get_attribute(ArticleSelectors.cover, 'src')
- self.assertIn('/pic/', src)
- @parameterized.expand(articles)
- def test_article_has_date(self, tweet_id, title, username, fullname):
- self.open_nitter(f'i/article/{tweet_id}')
- date_text = self.get_text(ArticleSelectors.date)
- self.assertTrue(len(date_text) > 3)
- @parameterized.expand(articles)
- def test_article_author_avatar(self, tweet_id, title, username, fullname):
- self.open_nitter(f'i/article/{tweet_id}')
- self.assert_element_visible(ArticleSelectors.avatar)
- src = self.get_attribute(ArticleSelectors.avatar, 'src')
- self.assertIn('/pic/', src)
- self.assertGreater(len(src), len('/pic/'))
- @parameterized.expand(articles)
- def test_article_author_verified(self, tweet_id, title, username, fullname):
- self.open_nitter(f'i/article/{tweet_id}')
- self.assert_element_visible(ArticleSelectors.verified)
- def test_article_author_verified_business(self):
- self.open_nitter('i/article/2064755789391110154')
- self.assert_element_visible('.article-author .verified-icon.business')
- class ArticleContentTest(BaseTestCase):
- def test_article_has_paragraphs(self):
- self.open_nitter('i/article/2064689664213041529')
- paragraphs = self.find_elements('.article-body p')
- self.assertGreater(len(paragraphs), 10)
- def test_article_has_headers(self):
- self.open_nitter('i/article/2064689664213041529')
- headers = self.find_elements('.article-body h1, .article-body h2')
- self.assertGreater(len(headers), 5)
- def test_article_has_bold_text(self):
- self.open_nitter('i/article/2064166507438059759')
- bold = self.find_elements('.article-body strong')
- self.assertGreater(len(bold), 0)
- def test_article_has_italic_text(self):
- self.open_nitter('i/article/2064166507438059759')
- italic = self.find_elements('.article-body em')
- self.assertGreater(len(italic), 0)
- def test_article_has_blockquotes(self):
- self.open_nitter('i/article/2064166507438059759')
- self.assert_element_visible('.article-body blockquote')
- def test_article_has_lists(self):
- self.open_nitter('i/article/2064166507438059759')
- self.assert_element_visible('.article-body ul')
- def test_article_has_emoji_text(self):
- self.open_nitter('i/article/2064166507438059759')
- body = self.get_text(ArticleSelectors.body)
- self.assertTrue(any(ord(c) > 0x1F000 for c in body))
- def test_article_has_links(self):
- self.open_nitter('i/article/2064691088636424322')
- links = self.find_elements('.article-body a[href]')
- self.assertGreater(len(links), 0)
- def test_article_twitter_links_localized(self):
- self.open_nitter('i/article/2064755789391110154')
- links = self.find_elements('.article-body a[href^="https://x.com"]')
- self.assertEqual(len(links), 0, 'x.com links should be converted to local paths')
- @parameterized.expand(articles_with_media)
- def test_article_media_count(self, tweet_id, expected_count):
- self.open_nitter(f'i/article/{tweet_id}')
- media = self.find_elements(ArticleSelectors.media)
- self.assertEqual(len(media), expected_count)
- @parameterized.expand(articles_with_dividers)
- def test_article_divider_count(self, tweet_id, expected_count):
- self.open_nitter(f'i/article/{tweet_id}')
- dividers = self.find_elements(ArticleSelectors.divider)
- self.assertEqual(len(dividers), expected_count)
- class ArticleMediaTest(BaseTestCase):
- def test_media_images_proxied(self):
- self.open_nitter('i/article/2064689664213041529')
- self.assert_element_visible(ArticleSelectors.media)
- img = self.find_element(f'{ArticleSelectors.media} img')
- src = img.get_attribute('src')
- self.assertIn('/pic/', src)
- self.assertFalse(src.startswith('https://pbs.twimg.com'))
- def test_cover_image_proxied(self):
- self.open_nitter('i/article/2064689664213041529')
- self.assert_element_visible(ArticleSelectors.cover)
- src = self.get_attribute(ArticleSelectors.cover, 'src')
- self.assertIn('/pic/', src)
- self.assertFalse(src.startswith('https://pbs.twimg.com'))
- def test_embedded_tweet(self):
- self.open_nitter('i/article/2064755789391110154')
- self.assert_element_visible('.article-body .timeline-item')
- def test_multiple_embedded_tweets(self):
- self.open_nitter('i/article/2064755231901319527')
- tweets = self.find_elements('.article-body .timeline-item')
- self.assertGreaterEqual(len(tweets), 3)
- def test_media_caption_displayed(self):
- self.open_nitter('i/article/2064689664213041529')
- self.assert_element_visible(ArticleSelectors.caption)
- captions = self.find_elements(ArticleSelectors.caption)
- self.assertGreaterEqual(len(captions), 5)
- def test_media_caption_text(self):
- self.open_nitter('i/article/2064689664213041529')
- self.assert_text_visible('FIGURE 1', ArticleSelectors.caption)
- def test_media_caption_alt_attribute(self):
- self.open_nitter('i/article/2064689664213041529')
- img = self.find_element(f'{ArticleSelectors.media} img')
- alt = img.get_attribute('alt')
- self.assertGreater(len(alt), 0)
- def test_no_caption_when_absent(self):
- self.open_nitter('i/article/2062858677149675788')
- captions = self.find_elements(ArticleSelectors.caption)
- self.assertEqual(len(captions), 0)
- class ArticleMentionTest(BaseTestCase):
- def test_mention_linkified(self):
- self.open_nitter('i/article/2064755231901319527')
- link = self.find_element('.article-body a[href="/ZachXBT"]')
- self.assertEqual(link.text, '@ZachXBT')
- def test_multiple_mentions_linkified(self):
- self.open_nitter('i/article/2064755231901319527')
- links = self.find_elements('.article-body a[href^="/"]')
- mention_hrefs = [l.get_attribute('href') for l in links
- if l.text.startswith('@')]
- usernames = [h.split('/')[-1] for h in mention_hrefs]
- self.assertIn('ZachXBT', usernames)
- self.assertIn('River', usernames)
- def test_mention_in_different_article(self):
- self.open_nitter('i/article/2064689664213041529')
- link = self.find_element('.article-body a[href="/FutureJurvetson"]')
- self.assertEqual(link.text, '@FutureJurvetson')
- def test_no_spurious_whitespace_in_styled_paragraph(self):
- """Styled paragraphs should not have extra whitespace from VNode serialization."""
- self.open_nitter('i/article/2064166507438059759')
- source = self.get_page_source()
- self.assertNotIn('white-space: pre-wrap', source)
- self.assertNotIn('white-space:pre-wrap', source)
- class ArticleCardTest(BaseTestCase):
- @parameterized.expand(articles)
- def test_status_page_shows_article_card(self, tweet_id, title, username, fullname):
- self.open_nitter(f'{username}/status/{tweet_id}')
- self.assert_element_visible('.article-card')
- self.assert_text(title, '.article-card .card-title')
- def test_article_card_has_cover_image(self):
- self.open_nitter('Dialectic_Group/status/2064689664213041529')
- self.assert_element_visible('.article-card .card-image img')
- src = self.get_attribute('.article-card .card-image img', 'src')
- self.assertIn('/pic/', src)
- def test_article_card_has_badge(self):
- self.open_nitter('Dialectic_Group/status/2064689664213041529')
- self.assert_element_visible('.article-card-badge')
- self.assert_text('Article', '.article-card-badge')
- def test_article_card_has_preview_text(self):
- self.open_nitter('CosmicOrFun/status/2064691088636424322')
- self.assert_element_visible('.article-card .card-description')
- def test_article_card_links_to_article(self):
- self.open_nitter('punk2898/status/2064166507438059759')
- href = self.get_attribute('.article-card .card-container', 'href')
- self.assertIn('/article/', href)
- def test_article_url_stripped_from_tweet_text(self):
- self.open_nitter('punk2898/status/2064166507438059759')
- self.assert_element_visible('.article-card')
- source = self.get_page_source()
- # Main tweet text should not contain article URL
- import re
- main = re.search(r'id="m".*?tweet-content[^>]*>(.*?)</div>', source, re.DOTALL)
- self.assertIsNotNone(main)
- self.assertNotIn('/article/', main.group(1))
- class ArticleQuotedCardTest(BaseTestCase):
- """Article cards inside quoted tweets (1914ad quoting own article)."""
- quoted_tweet = '1914ad/status/2064789532071891085'
- quoted_article_id = '2063677483548102688'
- def test_quoted_card_visible(self):
- self.open_nitter(self.quoted_tweet)
- self.assert_element_visible('.quote .article-card')
- def test_quoted_card_has_title(self):
- self.open_nitter(self.quoted_tweet)
- self.assert_text('David Bailey Already Won', '.quote .article-card .card-title')
- def test_quoted_card_has_badge(self):
- self.open_nitter(self.quoted_tweet)
- self.assert_element_visible('.quote .article-card-badge')
- self.assert_text('Article', '.quote .article-card-badge')
- def test_quoted_card_has_cover_image(self):
- self.open_nitter(self.quoted_tweet)
- self.assert_element_visible('.quote .article-card .card-image img')
- src = self.get_attribute('.quote .article-card .card-image img', 'src')
- self.assertIn('/pic/', src)
- def test_quoted_card_has_description(self):
- self.open_nitter(self.quoted_tweet)
- self.assert_element_visible('.quote .article-card .card-description')
- def test_quoted_card_links_to_article(self):
- self.open_nitter(self.quoted_tweet)
- href = self.get_attribute('.quote .article-card .card-container', 'href')
- self.assertIn(f'/article/{self.quoted_article_id}', href)
- class ArticleRoutingTest(BaseTestCase):
- def test_username_article_route_redirects(self):
- self.open_nitter('punk2898/article/2064166507438059759')
- self.assert_element_visible(ArticleSelectors.page)
- self.assert_text('1s', ArticleSelectors.title)
- def test_status_article_route_redirects(self):
- self.open_nitter('punk2898/status/2064166507438059759/article')
- self.assert_element_visible(ArticleSelectors.page)
- self.assert_text('1s', ArticleSelectors.title)
- def test_invalid_id_returns_404(self):
- self.open_nitter('i/article/notanumber')
- self.assert_element_not_visible(ArticleSelectors.page)
- def test_nonexistent_article(self):
- self.open_nitter('i/article/1')
- self.assert_element_visible('.error-panel')
|