mirror of
https://github.com/zedeus/nitter
synced 2026-09-05 14:49:32 +00:00
parent
bd9d492d36
commit
35882ed88d
16 changed files with 1192 additions and 24 deletions
307
tests/test_article.py
Normal file
307
tests/test_article.py
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
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'
|
||||
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'],
|
||||
|
||||
['2064696491948777658',
|
||||
'NC Push for Data Centers + Stablecoin Crypto= Data Centers are defacto BAILOUT OF Fed Reserve System',
|
||||
'June_12_1776', 'June_12_1776'],
|
||||
|
||||
['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/2064696491948777658')
|
||||
self.assert_element_visible('.article-body blockquote')
|
||||
|
||||
def test_article_has_lists(self):
|
||||
self.open_nitter('i/article/2064696491948777658')
|
||||
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)
|
||||
|
||||
|
||||
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/2064696491948777658')
|
||||
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')
|
||||
Loading…
Reference in a new issue