nitter/tests/test_article.py
2026-07-01 00:53:16 +02:00

325 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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')