"""Parser tests.""" import time from lxml import html from openbb_news import parser def test_strip_html_empty(): assert parser.strip_html("") == "" def test_strip_html_plain(): assert parser.strip_html("hello world") == "hello world" def test_strip_html_tags_and_whitespace(): assert parser.strip_html("

Hello world\n!

") == "Hello world !" def test_html_to_markdown_empty(): assert parser.html_to_markdown("") == "" def test_html_to_markdown_preserves_anchor_links(): out = parser.html_to_markdown( 'NYT headline ' 'BBC headline' ) assert "[NYT headline](https://nyt.test/x)" in out assert "[BBC headline](https://bbc.test/x)" in out def test_html_to_markdown_strips_non_anchor_tags(): out = parser.html_to_markdown("

Hello bold world

") assert "bold" in out assert "<" not in out def test_html_to_markdown_anchor_without_href(): out = parser.html_to_markdown('just text') assert "just text" in out assert "[" not in out def test_html_to_markdown_anchor_without_text(): out = parser.html_to_markdown('') assert "[" not in out def test_html_to_markdown_handles_paragraphs_and_breaks(): out = parser.html_to_markdown("

Line one

Line two

") assert "Line one" in out assert "Line two" in out assert "Line one" in out.split("\n")[0] def test_html_to_markdown_handles_br(): out = parser.html_to_markdown("First
Second") assert "First" in out assert "Second" in out def test_html_to_markdown_paragraph_after_text(): out = parser.html_to_markdown("intro text

paragraph

") assert "intro text" in out assert "paragraph" in out lines = [line for line in out.split("\n") if line.strip()] assert lines.index("intro text") < lines.index("paragraph") def test_html_to_markdown_lxml_parse_failure_falls_back(monkeypatch): from lxml import html as _html def boom(*_a, **_kw): raise ValueError("synthetic parse failure") monkeypatch.setattr(_html, "fragment_fromstring", boom) out = parser.html_to_markdown('link') assert out == "link" def test_truncate_short(): assert parser.truncate("abc", limit=10) == "abc" def test_truncate_long(): text = "word " * 100 truncated = parser.truncate(text, limit=50) assert len(truncated) <= 60 assert truncated.endswith("…") def test_struct_time_to_iso_none(): iso = parser.struct_time_to_iso(None) assert "T" in iso and iso.endswith("+00:00") def test_struct_time_to_iso_value(): st = time.struct_time((2026, 5, 19, 12, 0, 0, 0, 139, 0)) assert parser.struct_time_to_iso(st) == "2026-05-19T12:00:00+00:00" def test_article_markdown_joins_with_blank_lines(sample_article_html): doc = html.fromstring(sample_article_html) article = doc.xpath("//article")[0] text = parser._article_markdown(article) assert "First paragraph of the article." in text assert text.count("\n\n") == 2 def test_article_markdown_requires_two_paragraphs(): doc = html.fromstring(b"

only one paragraph

") node = doc.xpath("//div")[0] assert parser._article_markdown(node) == "" def test_article_markdown_filters_video_chapter_timestamps(): doc = html.fromstring( b"
" b"

Real opening paragraph with content.

" b"

2:26

" b"

12:44

" b"

0:58

" b"

1:30:00

" b"

Real closing paragraph.

" b"
" ) text = parser._article_markdown(doc.xpath("//div")[0]) assert "2:26" not in text assert "12:44" not in text assert "0:58" not in text assert "1:30:00" not in text assert "Real opening paragraph" in text assert "Real closing paragraph" in text def test_article_markdown_keeps_timestamp_inside_sentence(): doc = html.fromstring( b"
" b"

The video begins at 2:26 with the host introducing.

" b"

By 12:44 the segment closes.

" b"
" ) text = parser._article_markdown(doc.xpath("//div")[0]) assert "2:26" in text assert "12:44" in text def test_article_markdown_ignores_non_paragraph_text(): doc = html.fromstring(b"
plain wrapper text here
") node = doc.xpath("//div")[0] assert parser._article_markdown(node) == "" def test_article_markdown_skips_figure_without_img(): doc = html.fromstring( b"
" b"

One.

" b"
No image here
" b"

Two.

" b"
" ) text = parser._article_markdown(doc.xpath("//article")[0]) assert "![" not in text assert "One." in text assert "Two." in text def test_article_markdown_skips_img_without_src(): doc = html.fromstring(b"

One.

Two.

") text = parser._article_markdown(doc.xpath("//article")[0]) assert "![" not in text def test_article_markdown_figure_without_caption(): doc = html.fromstring( b"
" b"

One.

" b'
' b"

Two.

" b"
" ) text = parser._article_markdown(doc.xpath("//article")[0]) assert "![](https://x.test/p.jpg)" in text def test_strip_chrome_removes_chrome_tags(): doc = html.fromstring( b"" b"
h
" b"
fm
" b"sv" b"

kept one

kept two

" b"" ) parser._strip_chrome(doc) leftover_tags = {el.tag for el in doc.iter() if isinstance(el.tag, str)} for tag in parser._CHROME_TAGS: assert tag not in leftover_tags assert "article" in leftover_tags def test_looks_clean_rejects_templates_and_empty(): assert parser._looks_clean("Some real text.") assert not parser._looks_clean("") assert not parser._looks_clean("[[ type === 'moc' ]] body text") assert not parser._looks_clean("body with {{ binding }} marker") async def test_fetch_feed_parses(stub_session_factory, sample_rss): session = stub_session_factory({"http://feed.test/rss": sample_rss}) parsed = await parser.fetch_feed(session, "http://feed.test/rss") assert len(parsed.entries) == 2 assert parsed.entries[0].title == "First Article" async def test_fetch_feed_client_error(stub_session_factory): import aiohttp session = stub_session_factory( {"http://feed.test/rss": aiohttp.ClientError("boom")} ) parsed = await parser.fetch_feed(session, "http://feed.test/rss") assert len(parsed.entries) == 0 async def test_fetch_feed_http_error(stub_session_factory): session = stub_session_factory({"http://feed.test/rss": (b"", 500)}) parsed = await parser.fetch_feed(session, "http://feed.test/rss") assert len(parsed.entries) == 0 def test_extract_body_jsonld_top_level(sample_jsonld_html): assert parser._extract_body(sample_jsonld_html) == "JSON-LD top-level body." def test_extract_body_jsonld_graph(): payload = ( b'" ) assert parser._extract_body(payload) == "Body from graph." def test_extract_body_jsonld_list(): payload = ( b'" ) assert parser._extract_body(payload) == "Found in list." def test_extract_body_jsonld_invalid_then_article(): payload = ( b"" b'' b"

First sentence.

Second sentence.

" b"" ) assert parser._extract_body(payload) == "First sentence.\n\nSecond sentence." def test_extract_body_article_selector(sample_article_html): body = parser._extract_body(sample_article_html) assert body is not None assert "First paragraph" in body assert body.count("\n\n") == 2 def test_extract_body_picks_largest_article(): payload = ( b"" b"

Related card one.

Related card two.

" b"
" b"

Main story paragraph one with more substance.

" b"

Main story paragraph two with detail.

" b"

Main story paragraph three closing the piece.

" b"
" b"" ) body = parser._extract_body(payload) assert body is not None assert "Main story paragraph one" in body assert "Related card" not in body def test_extract_body_main_fallback_when_no_article(): payload = ( b"" b"" b"
" b"

Story paragraph one with content.

" b"

Story paragraph two with more detail.

" b"
" b"" ) assert ( parser._extract_body(payload) == "Story paragraph one with content.\n\nStory paragraph two with more detail." ) def test_extract_body_main_requires_two_paragraphs(): payload = b"

Only one paragraph here.

" assert parser._extract_body(payload) is None def test_extract_body_article_wins_over_main(): payload = ( b"" b"
" b"

Main paragraph one.

Main paragraph two.

" b"

Main paragraph three.

Main paragraph four.

" b"
" b"

Article paragraph one.

Article paragraph two.

" b"" ) assert ( parser._extract_body(payload) == "Article paragraph one.\n\nArticle paragraph two." ) def test_extract_body_rejects_unrendered_templates(): payload = ( b"" b"
" b"

[[ type === 'moc' ? 'MARKET ON CLOSE' : 'FREE WEBINAR' ]]

" b"

[[ timeLabel ]] WATCH LIVE

" b"
" b"" ) assert parser._extract_body(payload) is None def test_extract_body_strips_chrome_before_extracting(): payload = ( b"" b"" b"

banner text

banner two

" b"

Real story paragraph.

Story continues here.

" b"" b"" ) assert ( parser._extract_body(payload) == "Real story paragraph.\n\nStory continues here." ) def test_extract_body_no_container(): assert parser._extract_body(b"nothing") is None def test_extract_body_html_parse_error(): assert parser._extract_body(b"") is None def test_extract_body_jsonld_non_dict_entries(): payload = ( b"" b'' b"

Fallback one.

Fallback two.

" b"" ) assert parser._extract_body(payload) == "Fallback one.\n\nFallback two." def test_extract_body_jsonld_graph_no_body(): payload = ( b"" b'" b"

Graph fallback one.

Graph fallback two.

" b"" ) assert parser._extract_body(payload) == "Graph fallback one.\n\nGraph fallback two." def test_extract_body_jsonld_graph_non_dict_entry(): payload = ( b"" b'" ) assert parser._extract_body(payload) == "Graph article body." def test_extract_body_jsonld_template_falls_through_to_article(): payload = ( b"" b'" b"

Clean paragraph one.

Clean paragraph two.

" b"" ) assert ( parser._extract_body(payload) == "Clean paragraph one.\n\nClean paragraph two." ) def test_extract_body_preserves_utf8_smart_punctuation(): payload = ( "
" "

The Court’s ruling — issued Tuesday — was clear.

" "

Dominion’s lawyers said “we won”.

" "
" ).encode() body = parser._extract_body(payload) assert body is not None assert "Court’s" in body assert "Dominion’s" in body assert "“we won”" in body assert "—" in body for artifact in ("’", "–", " "): assert artifact not in body async def test_fetch_article_body_end_to_end(stub_session_factory, sample_article_html): session = stub_session_factory({"https://x.test/a": sample_article_html}) body = await parser.fetch_article_body(session, "https://x.test/a") assert body is not None assert "First paragraph" in body async def test_fetch_article_body_client_error(stub_session_factory): import aiohttp session = stub_session_factory({"https://x.test/a": aiohttp.ClientError("boom")}) assert await parser.fetch_article_body(session, "https://x.test/a") is None async def test_fetch_article_body_timeout(stub_session_factory): session = stub_session_factory( {"https://x.test/a": TimeoutError("simulated timeout")} ) assert await parser.fetch_article_body(session, "https://x.test/a") is None async def test_fetch_article_body_http_error(stub_session_factory): session = stub_session_factory({"https://x.test/a": (b"", 403)}) assert await parser.fetch_article_body(session, "https://x.test/a") is None async def test_fetch_article_body_no_article_returns_none(stub_session_factory): session = stub_session_factory( {"https://x.test/a": b"nothing"} ) assert await parser.fetch_article_body(session, "https://x.test/a") is None def test_extract_body_preserves_inline_img(): payload = ( b"
" b"

Opening paragraph.

" b'caption text' b"

Closing paragraph.

" b"
" ) body = parser._extract_body(payload) assert body is not None lines = body.split("\n\n") assert lines == [ "Opening paragraph.", "![caption text](https://x.test/inline.jpg)", "Closing paragraph.", ] def test_extract_body_preserves_figure_with_caption(): payload = ( b"
" b"

Lead in.

" b'
' b"
A long caption describing the photo.
" b"
" b"

After the photo.

" b"
" ) body = parser._extract_body(payload) assert body is not None assert "![A long caption describing the photo.](https://x.test/photo.jpg)" in body assert "Lead in." in body assert "After the photo." in body def test_extract_body_prepends_og_image_when_no_inline_image(): payload = ( b"" b'' b"
" b"

Paragraph one.

Paragraph two.

" b"
" ) body = parser._extract_body(payload) assert body is not None assert body.split("\n\n")[0] == "![](https://x.test/hero.jpg)" assert "Paragraph one." in body def test_extract_body_skips_hero_when_inline_image_present(): payload = ( b"" b'' b"
" b"

Lead.

" b'' b"

Body.

" b"
" ) body = parser._extract_body(payload) assert body is not None assert "https://x.test/hero.jpg" not in body assert "![](https://x.test/inline.jpg)" in body def test_extract_body_jsonld_articlebody_with_og_hero(): payload = ( b"" b'' b"" b'" ) body = parser._extract_body(payload) assert body == "![](https://x.test/hero.jpg)\n\nPure text body." def test_html_to_markdown_preserves_inline_img(): out = parser.html_to_markdown( '

Lead

alt text' ) assert "![alt text](https://x.test/inline.jpg)" in out assert "Lead" in out def test_html_to_markdown_img_tail_text(): out = parser.html_to_markdown( ' trailing text after img' ) assert "![](https://x.test/inline.jpg)" in out assert "trailing text after img" in out def test_jsonld_field_list_of_strings(): payload = ( b"" b'" ) body = parser._extract_body(payload) assert body is None or body.startswith("![](https://x.test/first.jpg)") def test_jsonld_field_list_of_image_objects(): payload = ( b"" b'" b"

One.

Two.

" b"" ) body = parser._extract_body(payload) assert body is not None assert body.startswith("![](https://x.test/o.jpg)") def test_jsonld_field_image_object_with_url(): payload = ( b"" b'" b"

One.

Two.

" b"" ) body = parser._extract_body(payload) assert body is not None assert body.startswith("![](https://x.test/obj.jpg)") def test_extract_image_jsonld_fallback_path(): payload = ( b"" b'" b"

One.

Two.

" b"" ) body = parser._extract_body(payload) assert body is not None assert body.startswith("![](https://x.test/ld.jpg)")