import asyncio
import os
import tempfile
import unittest
from olmocr.synth.cutoff_detection import (
CutoffElement,
RenderResult,
detect_cutoff_text,
extract_viewport_from_html,
has_significant_cutoff,
)
from olmocr.synth.mine_html_templates import render_pdf_with_playwright
class TestDetectCutoffText(unittest.TestCase):
"""Tests for detect_cutoff_text using Playwright to detect overflow-clipped text."""
def test_no_cutoff_simple_page(self):
"""All text fits within the viewport and no overflow:hidden clipping."""
html = """
Hello World
This is a simple paragraph that fits within the viewport.
"""
results = asyncio.run(detect_cutoff_text(html, 400, 300))
self.assertEqual(len(results), 0, f"Expected no cutoff, got: {results}")
def test_cutoff_overflow_hidden_horizontal(self):
"""Text inside a narrow overflow:hidden container is clipped horizontally."""
html = """
This is a very long sentence that will definitely overflow the 100px wide container and get clipped
"""
results = asyncio.run(detect_cutoff_text(html, 800, 600))
self.assertGreater(len(results), 0, "Should detect clipped text")
# The paragraph text should be the one flagged
clipped_texts = [r.text for r in results]
self.assertTrue(
any("very long sentence" in t for t in clipped_texts),
f"Expected to find the clipped sentence, got: {clipped_texts}",
)
# The clipping ancestor should be the container div
for r in results:
if "very long sentence" in r.text:
self.assertLess(r.visible_ratio, 0.9)
self.assertEqual(r.clipping_ancestor_tag, "DIV")
def test_cutoff_overflow_hidden_vertical(self):
"""Text inside a short overflow:hidden container is clipped vertically."""
html = """
Line one
Line two should be hidden
Line three should be hidden
"""
results = asyncio.run(detect_cutoff_text(html, 800, 600))
# At least the later lines should be detected as cut off
clipped_texts = [r.text for r in results]
self.assertTrue(
any("hidden" in t for t in clipped_texts),
f"Expected clipped lines, got: {clipped_texts}",
)
def test_cutoff_table_in_narrow_container(self):
"""A wide table inside a narrow overflow:hidden container - similar to the real issue."""
html = """
| Name |
Column A |
Column B |
Column C - This Should Be Cutoff |
Column D - Also Cutoff |
| Row 1 |
Value A1 |
Value B1 |
Value C1 |
Value D1 |
"""
results = asyncio.run(detect_cutoff_text(html, 800, 600))
clipped_texts = [r.text for r in results]
# The rightmost columns should be flagged
self.assertTrue(
any("Cutoff" in t for t in clipped_texts),
f"Expected table column cutoff, got: {clipped_texts}",
)
def test_no_cutoff_table_fits(self):
"""A small table that fits within its container should not be flagged."""
html = """
"""
results = asyncio.run(detect_cutoff_text(html, 800, 600))
self.assertEqual(len(results), 0, f"Expected no cutoff, got: {results}")
def test_cutoff_viewport_clip(self):
"""Text positioned outside the viewport should be detected."""
html = """
Visible text
This text is off screen to the right
"""
results = asyncio.run(detect_cutoff_text(html, 300, 200))
clipped_texts = [r.text for r in results]
self.assertTrue(
any("off screen" in t for t in clipped_texts),
f"Expected offscreen text detected, got: {clipped_texts}",
)
def test_completely_hidden_element(self):
"""An element fully hidden by overflow:hidden should have visible_ratio near 0."""
html = """
Visible line
Completely hidden text below
"""
results = asyncio.run(detect_cutoff_text(html, 800, 600))
hidden_elements = [r for r in results if "Completely hidden" in r.text]
self.assertGreater(len(hidden_elements), 0, "Should detect fully hidden element")
for el in hidden_elements:
self.assertAlmostEqual(el.visible_ratio, 0.0, places=1)
def test_visibility_threshold(self):
"""Lower threshold should return fewer results."""
html = """
Short text that is slightly clipped at the edge
"""
# With strict threshold (0.99), flag even slightly clipped elements
strict_results = asyncio.run(detect_cutoff_text(html, 800, 600, visibility_threshold=0.99))
# With lenient threshold (0.1), only flag severely clipped
lenient_results = asyncio.run(detect_cutoff_text(html, 800, 600, visibility_threshold=0.1))
self.assertGreaterEqual(len(strict_results), len(lenient_results))
def test_display_none_not_flagged(self):
"""Elements with display:none should not be flagged as cutoff."""
html = """
Visible text
This is intentionally hidden via display:none
"""
results = asyncio.run(detect_cutoff_text(html, 800, 600))
clipped_texts = [r.text for r in results]
self.assertFalse(
any("intentionally hidden" in t for t in clipped_texts),
f"display:none elements should not be flagged, got: {clipped_texts}",
)
def test_multicolumn_poster_layout(self):
"""Simulates the real poster issue: a wide table in a narrow grid column."""
html = """
| Outcome |
0-12 weeks OR (95% CI) |
>12 weeks OR (95% CI) |
All weeks OR (95% CI) |
| Unweighted |
Weighted |
Unweighted model |
Weighted model |
Unweighted model |
Weighted model |
| Death |
0.84 |
1.03 |
0.53 |
0.93 |
Hidden value 1 |
Hidden value 2 |
"""
results = asyncio.run(detect_cutoff_text(html, 1024, 724))
# At minimum, some of the wide table content should be flagged
self.assertGreater(
len(results),
0,
"Should detect cutoff in the wide table within narrow grid column",
)
def test_real_poster_html(self):
"""Test with the actual poster HTML file if it exists."""
import os
poster_path = "/home/ubuntu/olmocr/synth_testposter1/html/synthetic/pdf_00000_page1.html"
if not os.path.exists(poster_path):
self.skipTest("Poster HTML file not available")
with open(poster_path, "r") as f:
html_content = f.read()
results = asyncio.run(detect_cutoff_text(html_content, 1024, 724))
# We know this poster has cutoff table columns
self.assertGreater(len(results), 0, "Should detect cutoff in real poster HTML")
# At least some table-related content should be flagged
has_table_cutoff = any(r.tag in ("TH", "TD") and r.visible_ratio < 0.9 for r in results)
self.assertTrue(
has_table_cutoff,
f"Expected table cell cutoff detection. Clipped elements: " f"{[(r.tag, r.text[:50], r.visible_ratio) for r in results[:10]]}",
)
class TestHasSignificantCutoff(unittest.TestCase):
"""Tests for the has_significant_cutoff helper function."""
def test_no_elements(self):
self.assertFalse(has_significant_cutoff([]))
def test_short_text_not_significant(self):
"""Very short clipped text (e.g., a period or dash) is not significant."""
elements = [
CutoffElement(tag="SPAN", text=".", visible_ratio=0.0, horizontal_visible_ratio=0.0),
CutoffElement(tag="SPAN", text="-", visible_ratio=0.0, horizontal_visible_ratio=0.0),
]
self.assertFalse(has_significant_cutoff(elements, min_text_length=3))
def test_long_text_significantly_cutoff(self):
"""Long text that is mostly hidden horizontally is significant."""
elements = [
CutoffElement(
tag="TH",
text=">12 weeks OR (95% CI)",
visible_ratio=0.1,
horizontal_visible_ratio=0.1,
),
]
self.assertTrue(has_significant_cutoff(elements))
def test_long_text_vertical_only_not_significant(self):
"""Text clipped only vertically should NOT be flagged (PDF scaling handles it)."""
elements = [
CutoffElement(
tag="TD",
text="Required Units for the Major:",
visible_ratio=0.0,
horizontal_visible_ratio=1.0, # full width visible, just vertically clipped
),
]
self.assertFalse(has_significant_cutoff(elements))
def test_long_text_barely_cutoff_not_significant(self):
"""Long text that is mostly visible (e.g., 80%) is not significant at default threshold."""
elements = [
CutoffElement(
tag="P",
text="Some paragraph text that is mostly visible",
visible_ratio=0.8,
horizontal_visible_ratio=0.8,
),
]
# Default max_visible_ratio is 0.5, so 0.8 should not be significant
self.assertFalse(has_significant_cutoff(elements))
def test_custom_thresholds(self):
elements = [
CutoffElement(
tag="TD",
text="Value C1",
visible_ratio=0.6,
horizontal_visible_ratio=0.6,
),
]
# At default threshold (0.5), 0.6 is NOT significant
self.assertFalse(has_significant_cutoff(elements, max_visible_ratio=0.5))
# At stricter threshold (0.7), 0.6 IS significant
self.assertTrue(has_significant_cutoff(elements, max_visible_ratio=0.7))
def test_mixed_elements(self):
"""Only needs one significant element with horizontal cutoff to return True."""
elements = [
CutoffElement(tag="P", text="Visible", visible_ratio=0.95, horizontal_visible_ratio=0.95),
CutoffElement(tag="SPAN", text=".", visible_ratio=0.0, horizontal_visible_ratio=0.0), # too short
CutoffElement(tag="TH", text="All weeks OR (95% CI)", visible_ratio=0.0, horizontal_visible_ratio=0.0),
]
self.assertTrue(has_significant_cutoff(elements))
class TestExtractViewportFromHtml(unittest.TestCase):
"""Tests for extract_viewport_from_html."""
def test_explicit_width_and_height(self):
html = ''
w, h = extract_viewport_from_html(html)
self.assertEqual(w, 800)
self.assertEqual(h, 600)
def test_width_only_height_fallback_large(self):
"""When only width is set, height should fall back to 100000."""
html = ''
w, h = extract_viewport_from_html(html)
self.assertEqual(w, 1024)
self.assertEqual(h, 100000)
def test_content_before_name(self):
"""Meta tag with content attr before name attr (both orderings appear in the wild)."""
html = ''
w, h = extract_viewport_from_html(html)
self.assertEqual(w, 792)
self.assertEqual(h, 100000)
def test_no_viewport_meta(self):
"""No viewport meta tag — both width and height use defaults."""
html = "Hello
"
w, h = extract_viewport_from_html(html)
self.assertEqual(w, 1024)
self.assertEqual(h, 100000)
def test_no_false_positive_without_body_height(self):
"""Content at y>768 with no body height should NOT be flagged as viewport-clipped."""
html = """
Spacer
This footer text is below y=768 but should NOT be flagged
"""
vw, vh = extract_viewport_from_html(html)
# vh should be 100000, so the footer is well within viewport
results = asyncio.run(detect_cutoff_text(html, vw, vh))
flagged_texts = [r.text for r in results]
self.assertFalse(
any("footer" in t.lower() for t in flagged_texts),
f"Footer should NOT be flagged with large viewport height, got: {flagged_texts}",
)
class TestOcclusionDetection(unittest.TestCase):
"""Tests for detection of text occluded by opaque overlapping elements."""
def test_opaque_box_covering_text(self):
"""An opaque positioned element covering text should be detected."""
html = """
This important text is hidden behind an opaque box
"""
results = asyncio.run(detect_cutoff_text(html, 400, 300))
occluded = [r for r in results if r.is_occluded]
self.assertGreater(len(occluded), 0, f"Should detect occluded text, got: {results}")
self.assertTrue(
any("important text" in r.text for r in occluded),
f"Expected occluded text, got: {[r.text for r in occluded]}",
)
def test_transparent_watermark_not_flagged(self):
"""Semi-transparent watermark should NOT be flagged as occlusion."""
html = """
This text has a transparent watermark overlay but should still be visible
"""
results = asyncio.run(detect_cutoff_text(html, 400, 300))
occluded = [r for r in results if r.is_occluded]
self.assertEqual(
len(occluded),
0,
f"Transparent watermark should not flag occlusion, got: {[r.text for r in occluded]}",
)
def test_has_significant_cutoff_with_occlusion(self):
"""has_significant_cutoff should return True for occluded elements."""
elements = [
CutoffElement(
tag="P",
text="Important covered text",
visible_ratio=0.4,
is_occluded=True,
),
]
self.assertTrue(has_significant_cutoff(elements))
def test_has_significant_cutoff_short_occluded_text(self):
"""Short occluded text (< min_text_length) should not be significant."""
elements = [
CutoffElement(tag="SPAN", text="ab", visible_ratio=0.0, is_occluded=True),
]
self.assertFalse(has_significant_cutoff(elements, min_text_length=3))
class TestRenderWithCutoffDetection(unittest.TestCase):
"""Integration tests for render_pdf_with_playwright with cutoff detection."""
def test_html_with_overflow_cutoff_is_rejected(self):
"""HTML with significant overflow:hidden clipping should be rejected."""
html = """
This is a very long sentence that will definitely overflow the narrow container and get clipped by overflow hidden
"""
with tempfile.TemporaryDirectory() as td:
result = asyncio.run(render_pdf_with_playwright(html, os.path.join(td, "test.pdf"), 400, 600))
self.assertFalse(result.success)
self.assertTrue(result.has_cutoff)
self.assertGreater(len(result.cutoff_elements), 0)
def test_clean_html_renders_successfully(self):
"""Clean HTML without clipping should render to a single-page PDF."""
html = """
Hello World
This is a simple page with no overflow clipping issues.
"""
with tempfile.TemporaryDirectory() as td:
pdf_path = os.path.join(td, "test.pdf")
result = asyncio.run(render_pdf_with_playwright(html, pdf_path, 400, 600))
self.assertTrue(result.success)
self.assertFalse(result.has_cutoff)
self.assertIsNotNone(result.scale_used)
if __name__ == "__main__":
unittest.main()