Buckets:
sabaridsnfuji/bco-repro-artifacts / repro-bundle /scripts /posterly /tests /test_logo_integration.py
| """Chromium-gated integration test for the Gate E (header logo) geometry | |
| inside ``polish``'s ``_POLISH_JS``. | |
| The unit tests in ``test_polish_output.py`` feed the Python loop canned | |
| ``logos``/``qrs``/``headerBlocks`` data and never execute the JS, so the | |
| DOM collection itself -- the ``.logo-slot``/``.venue-badge`` scan, the QR | |
| height sampling, and the header-block rects the squeeze gate reads -- is | |
| only exercised here, against a real headless Chromium. | |
| Skipped when Playwright/Chromium isn't installed (e.g. the mocked unit | |
| suite runs under a plain interpreter with no browser). | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import pytest | |
| from _posterly import polish as _polish | |
| def _chromium_available() -> bool: | |
| try: | |
| from playwright.sync_api import sync_playwright | |
| with sync_playwright() as p: | |
| browser = p.chromium.launch() | |
| browser.close() | |
| return True | |
| except Exception: | |
| return False | |
| pytestmark = pytest.mark.skipif( | |
| not _chromium_available(), | |
| reason="playwright + chromium not available", | |
| ) | |
| _SVG = ("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' " | |
| "viewBox='0 0 10 10'><rect width='10' height='10' " | |
| "fill='%232D5F8B'/></svg>") | |
| # A header skeleton: flex right-block of logo slots + QR, grid width pinned | |
| # to 1000px so the gate fractions are exact. The ratio gates read rendered | |
| # widths, so the exact grid track sizing is immaterial to them (the shipped | |
| # templates centre the title with `1fr minmax(50%, auto) 1fr`; this fixture | |
| # keeps a simple auto | 1fr | auto so a short fixture title still fills the | |
| # title track). Tests that exercise the title-centre offset override the | |
| # grid inline (see test_title_offcenter_warns). | |
| _HEAD = """<!DOCTYPE html> | |
| <html><head><meta charset="utf-8"> | |
| <style> | |
| @page {{ size: 24in 36in; margin: 0; }} | |
| * {{ margin: 0; box-sizing: border-box; }} | |
| .header {{ width: 1000px; display: grid; | |
| grid-template-columns: auto 1fr auto; align-items: center; }} | |
| .title-block {{ min-width: 0; }} | |
| .right-block {{ display: flex; align-items: center; gap: 10px; }} | |
| .logo-slot img, .qr-block img {{ display: block; }} | |
| </style></head> | |
| <body> | |
| <div data-measure-role="poster"> | |
| <header class="header" data-measure-role="header"> | |
| {venue} | |
| <div class="title-block"><h1>Poster title</h1></div> | |
| <div class="right-block"> | |
| {slots} | |
| <div class="qr-block"><img src="{svg}" | |
| style="width:85px;height:85px"></div> | |
| </div> | |
| </header> | |
| <div data-measure-role="column"> | |
| <div data-measure-role="card"><p>body</p>{body}</div> | |
| </div> | |
| </div> | |
| </body></html> | |
| """ | |
| _TEXT_VENUE = '<div class="venue-badge">VENUE</div>' | |
| def _args(html) -> argparse.Namespace: | |
| return argparse.Namespace( | |
| html=str(html), canvas=None, settle_ms=200, | |
| mathjax_timeout_ms=5000, wide_min_ratio=0.65, | |
| tall_max_ratio=0.70, tall_min_ratio=0.36, square_min_ratio=0.55, | |
| max_space_between_fill=0.05, max_card_trailing=0.10, | |
| logo_max_width_ratio=0.22, logo_qr_tol=0.15, | |
| rightblock_max_ratio=0.32, title_min_ratio=0.45, strict=False, | |
| ) | |
| def test_logo_gates_end_to_end(tmp_path, capsys) -> None: | |
| """Three adversarial logos: an oversized wordmark (30% of the 1000px | |
| header -> LOGO/WIDE), a too-tall seal (140px vs 85px QR -> | |
| LOGO/QR-MISMATCH), and a 404'd raster (-> LOGO/BROKEN). The fat | |
| right-block also squeezes the title -> HEADER/TITLE-SQUEEZED.""" | |
| slots = f""" | |
| <div class="logo-slot"> | |
| <img src="{_SVG}" style="width:300px;height:80px"></div> | |
| <div class="logo-slot"> | |
| <img src="{_SVG}" style="width:100px;height:140px"></div> | |
| <div class="logo-slot"> | |
| <img src="missing-logo.png" style="width:80px;height:80px"></div> | |
| """ | |
| poster = tmp_path / "poster.html" | |
| poster.write_text( | |
| _HEAD.format(venue=_TEXT_VENUE, slots=slots, body="", svg=_SVG), | |
| encoding="utf-8") | |
| rc = _polish.cmd_polish(_args(poster)) | |
| combined = "".join(capsys.readouterr()) | |
| assert rc == 0 # warn-only, not --strict | |
| assert "header logos : 3" in combined # all three collected | |
| assert "LOGO/WIDE" in combined # 300/1000 = 30% > 22% | |
| assert "LOGO/QR-MISMATCH" in combined # 140 vs 85 = 65% off | |
| assert "LOGO/BROKEN" in combined # 404'd raster | |
| assert "missing-logo.png" in combined | |
| # ~595px right-block on a 1000px header steals the title's width. | |
| assert "HEADER/TITLE-SQUEEZED" in combined | |
| def test_healthy_header_is_silent(tmp_path, capsys) -> None: | |
| """A header following the template defaults -- one 2:1 logo at the QR's | |
| 85px height, 17% of header width -- must pass every Gate E check (the | |
| false-positive guard for the recommended size classes). Three traps | |
| prove the scoping rules: | |
| * a venue logo nested as ``.venue-badge > .logo-slot > img`` at 40px | |
| (vs the 85px QR) is collected ONCE and as venue -> no QR match | |
| (double-counting would emit a bogus LOGO/QR-MISMATCH); | |
| * a card-body ``.logo-slot`` at 300px and a card-body ``.qr-block`` | |
| at 40px are OUTSIDE the header -> not collected (they'd otherwise | |
| fire LOGO/WIDE and corrupt the QR height).""" | |
| venue = f"""<div class="venue-badge"><div class="logo-slot"> | |
| <img src="{_SVG}" style="width:40px;height:40px"></div></div>""" | |
| slots = f""" | |
| <div class="logo-slot"> | |
| <img src="{_SVG}" style="width:170px;height:85px"></div> | |
| """ | |
| body = f""" | |
| <div class="logo-slot"> | |
| <img src="{_SVG}" style="width:300px;height:85px"></div> | |
| <div class="qr-block"> | |
| <img src="{_SVG}" style="width:40px;height:40px"></div> | |
| """ | |
| poster = tmp_path / "poster.html" | |
| poster.write_text( | |
| _HEAD.format(venue=venue, slots=slots, body=body, svg=_SVG), | |
| encoding="utf-8") | |
| rc = _polish.cmd_polish(_args(poster)) | |
| combined = "".join(capsys.readouterr()) | |
| assert rc == 0 | |
| assert "header logos : 2" in combined # body decoys not counted | |
| assert "LOGO/" not in combined | |
| assert "HEADER/TITLE-SQUEEZED" not in combined | |
| def test_logo_stack_exempt_from_qr_match(tmp_path, capsys) -> None: | |
| """Two wordmarks in a ``.logo-row.logo-stack`` are normalized by WIDTH and | |
| intentionally NOT QR-height-matched: 60px and 110px tall vs an 85px QR are | |
| both >15% off, yet the width-normalized stack must NOT fire | |
| LOGO/QR-MISMATCH. A 404'd logo in the same stack still fires LOGO/BROKEN -- | |
| the exemption is scoped to the QR height match, not the broken/width | |
| checks.""" | |
| slots = f""" | |
| <div class="logo-slot"><div class="logo-row logo-stack"> | |
| <img src="{_SVG}" style="width:170px;height:60px"> | |
| <img src="{_SVG}" style="width:170px;height:110px"> | |
| <img src="stack-missing.png" style="width:170px;height:80px"> | |
| </div></div> | |
| """ | |
| poster = tmp_path / "poster.html" | |
| poster.write_text( | |
| _HEAD.format(venue=_TEXT_VENUE, slots=slots, body="", svg=_SVG), | |
| encoding="utf-8") | |
| rc = _polish.cmd_polish(_args(poster)) | |
| combined = "".join(capsys.readouterr()) | |
| assert rc == 0 | |
| assert "header logos : 3" in combined | |
| assert "LOGO/QR-MISMATCH" not in combined # width-normalized stack | |
| assert "LOGO/BROKEN" in combined # broken check still runs | |
| assert "stack-missing.png" in combined | |
| def _args_off(html) -> argparse.Namespace: | |
| a = _args(html) | |
| # Explicit threshold for these MECHANICS tests, intentionally NOT the shipped | |
| # 3% default: 0.06 brackets cleanly between the ~3% balanced-header residual | |
| # (test_header_overflow_warns) and the 14% unequal-side case | |
| # (test_title_offcenter_warns), so each case sits clear of the boundary. The | |
| # shipped default lives in DEFAULT_TITLE_OFFSET_MAX, not here. | |
| a.title_offset_max = 0.06 | |
| return a | |
| def test_title_offcenter_warns(tmp_path, capsys) -> None: | |
| """A header with unequal side tracks (80px left | 1fr | 360px right) | |
| centres the title BETWEEN the side blocks, not on the poster: the | |
| title-block centre lands at ~360px on a 1000px header (14% left of the | |
| 500px centre line) -> HEADER/TITLE-OFFCENTER. A soft nudge, not a hard | |
| rule -- logo/QR sizing still wins.""" | |
| html = _HEAD.format(venue=_TEXT_VENUE, slots="", body="", svg=_SVG).replace( | |
| "grid-template-columns: auto 1fr auto;", | |
| "grid-template-columns: 80px 1fr 360px;") | |
| poster = tmp_path / "poster.html" | |
| poster.write_text(html, encoding="utf-8") | |
| rc = _polish.cmd_polish(_args_off(poster)) | |
| combined = "".join(capsys.readouterr()) | |
| assert rc == 0 # soft, warn-only | |
| assert "HEADER/TITLE-OFFCENTER" in combined | |
| def test_centered_title_no_offcenter(tmp_path, capsys) -> None: | |
| """Equal side tracks (250px | 1fr | 250px) keep the title centred on the | |
| poster (centre at 500px on a 1000px header) -> no HEADER/TITLE-OFFCENTER.""" | |
| html = _HEAD.format(venue=_TEXT_VENUE, slots="", body="", svg=_SVG).replace( | |
| "grid-template-columns: auto 1fr auto;", | |
| "grid-template-columns: 250px 1fr 250px;") | |
| poster = tmp_path / "poster.html" | |
| poster.write_text(html, encoding="utf-8") | |
| rc = _polish.cmd_polish(_args_off(poster)) | |
| combined = "".join(capsys.readouterr()) | |
| assert rc == 0 | |
| assert "HEADER/TITLE-OFFCENTER" not in combined | |
| def test_header_overflow_warns(tmp_path, capsys) -> None: | |
| """Both side tracks fat but balanced (280px | minmax(50%,auto) | 280px on | |
| a 1000px header): the title stays ~centred (3% offset, under _args_off's | |
| explicit 6% test threshold -- the shipped default is 3%) | |
| and each side is 28% (< the 32% right-block limit), yet 280+500+280=1060px | |
| overflows the 1000px header by ~60px. Only HEADER/OVERFLOW catches it -- | |
| the gap the ratio and offset gates leave open.""" | |
| html = _HEAD.format(venue=_TEXT_VENUE, slots="", body="", svg=_SVG).replace( | |
| "grid-template-columns: auto 1fr auto;", | |
| "grid-template-columns: 280px minmax(50%, auto) 280px;") | |
| poster = tmp_path / "poster.html" | |
| poster.write_text(html, encoding="utf-8") | |
| rc = _polish.cmd_polish(_args_off(poster)) | |
| combined = "".join(capsys.readouterr()) | |
| assert rc == 0 # soft, warn-only | |
| assert "HEADER/OVERFLOW" in combined | |
| assert "HEADER/TITLE-OFFCENTER" not in combined # 3% offset < _args_off's explicit 6% | |
Xet Storage Details
- Size:
- 10.5 kB
- Xet hash:
- c283be8d34953945edc1269bd13a856a6eb59665e62de38e422ba1ef35304ae2
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.