feat(#90): IMP-56 u1-u19 catch-up before final close (post-u20 push fix)
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
u1: text_overrides axis in user_overrides_io u2: structure_overrides axis in user_overrides_io u3: vite allowlist for new endpoints u4: text_override_resolver u5: Step 12 text_overrides apply in phase_z2_pipeline u6: structure_override_resolver u7: text_path_stamper u8: SlideCanvas text-edit capture u9: SlideCanvas structure-edit overlay u10: userOverridesApi service extension u11: designAgent types extension u12: slidePlanUtils restore u13: user_overrides endpoint tests u14: user_overrides restore tests u15: pipeline fallback tests u16: edit-mode state + gating tests u17: slide_base print mode CSS u18: /api/connect endpoint (vite) u19: /api/export endpoint (vite) Recovery scope: 29 files (12 modified + 17 new). u20 already pushed in 9439575; this commit lands u1-u19 that were authored but not committed before #90 was externally closed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
151
tests/phase_z2/test_slide_base_print_mode.py
Normal file
151
tests/phase_z2/test_slide_base_print_mode.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""IMP-90 (#90) u17 — slide_base.html print-mode contract tests.
|
||||
|
||||
Stage 2 plan contract (unit u17):
|
||||
Step 22 user-edit + Export track. The Phase Z2 print path MUST
|
||||
auto-expand <details> popups so the FULL raw_content (MDX 원문 무손실
|
||||
보존) is included when the user prints / exports from the browser.
|
||||
|
||||
u17 introduces two coordinated surfaces in
|
||||
``templates/phase_z2/slide_base.html``:
|
||||
|
||||
1. ``@media print`` CSS block — neutralizes the on-screen-only body
|
||||
centering / box-shadow / 280px popup card clipping so the slide
|
||||
prints at 1280×720 with the expanded popup body in static flow.
|
||||
|
||||
2. ``beforeprint`` / ``afterprint`` JavaScript hook at body level —
|
||||
toggles ``details.open`` to ``true`` before the print snapshot
|
||||
and restores the user's prior open/closed state afterwards. Body
|
||||
level (outside any ``<details>...</details>`` block) preserves
|
||||
the IMP-35 u8 popup-render JS-free invariant
|
||||
(tests/phase_z2/test_slide_base_popup_render.py
|
||||
``test_popup_emits_no_javascript_on_render_path``).
|
||||
|
||||
Invariants locked here:
|
||||
P-1: ``@media print`` block is emitted exactly once in the render.
|
||||
P-2: ``@page`` size matches the 1280×720 slide canvas.
|
||||
P-3: ``.slide`` box-shadow + body padding/min-height neutralized at
|
||||
print time.
|
||||
P-4: ``.zone__popup-summary`` hidden, popup body switches from
|
||||
absolute to static flow with unconstrained height — the popup
|
||||
card chrome (border / shadow / 280px max-height) is unset.
|
||||
P-5: ``beforeprint`` + ``afterprint`` listeners are wired at body
|
||||
level (NOT inside the per-zone details block) so the popup
|
||||
render path stays JS-free.
|
||||
P-6: Restore semantics — the script preserves the user's prior
|
||||
open/closed state via a single ``dataset.imp90PrintRestore`` key
|
||||
(no global state, no event-bus mutation).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from src.phase_z2_pipeline import render_slide
|
||||
|
||||
|
||||
def _layout_css() -> dict:
|
||||
return {"areas": '"primary"', "cols": "1fr", "rows": "1fr"}
|
||||
|
||||
|
||||
def _zone(**overrides) -> dict:
|
||||
base = {
|
||||
"position": "primary",
|
||||
"template_id": "__empty__",
|
||||
"slot_payload": {},
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
def _render() -> str:
|
||||
return render_slide(
|
||||
slide_title="t",
|
||||
slide_footer=None,
|
||||
zones_data=[_zone()],
|
||||
layout_preset="single",
|
||||
layout_css=_layout_css(),
|
||||
gap_px=14,
|
||||
)
|
||||
|
||||
|
||||
# ─── P-1 ─ media print block presence ───────────────────────────────
|
||||
|
||||
|
||||
def test_media_print_block_emitted_once():
|
||||
html = _render()
|
||||
matches = re.findall(r"@media\s+print\s*\{", html)
|
||||
assert len(matches) == 1
|
||||
|
||||
|
||||
# ─── P-2 ─ @page size matches slide canvas ──────────────────────────
|
||||
|
||||
|
||||
def test_page_size_matches_slide_canvas():
|
||||
html = _render()
|
||||
flat = re.sub(r"\s+", " ", html)
|
||||
assert "@page { size: 1280px 720px; margin: 0; }" in flat
|
||||
|
||||
|
||||
# ─── P-3 ─ standalone chrome neutralized at print ───────────────────
|
||||
|
||||
|
||||
def test_slide_box_shadow_neutralized_at_print():
|
||||
html = _render()
|
||||
flat = re.sub(r"\s+", " ", html)
|
||||
print_block = re.search(r"@media\s+print\s*\{(.*?)\}\s*</style>", flat)
|
||||
assert print_block is not None
|
||||
body = print_block.group(1)
|
||||
assert "box-shadow: none !important" in body
|
||||
assert "padding: 0 !important" in body
|
||||
assert "min-height: 0 !important" in body
|
||||
|
||||
|
||||
# ─── P-4 ─ popup body switches to static flow, summary hidden ───────
|
||||
|
||||
|
||||
def test_popup_card_chrome_unset_at_print():
|
||||
html = _render()
|
||||
flat = re.sub(r"\s+", " ", html)
|
||||
print_block = re.search(r"@media\s+print\s*\{(.*?)\}\s*</style>", flat)
|
||||
assert print_block is not None
|
||||
body = print_block.group(1)
|
||||
assert ".zone__popup-summary { display: none !important; }" in body
|
||||
assert "position: static !important" in body
|
||||
assert "max-height: none !important" in body
|
||||
assert "overflow: visible !important" in body
|
||||
|
||||
|
||||
# ─── P-5 ─ beforeprint hook is body-level (NOT inside <details>) ────
|
||||
|
||||
|
||||
def test_beforeprint_and_afterprint_listeners_present():
|
||||
html = _render()
|
||||
assert "addEventListener('beforeprint'" in html
|
||||
assert "addEventListener('afterprint'" in html
|
||||
|
||||
|
||||
def test_print_script_is_outside_any_details_block():
|
||||
"""The IMP-35 u8 popup render path is JS-free. Our print script
|
||||
sits at body level after the slide div, so no <script> appears
|
||||
inside a <details>...</details> popup block."""
|
||||
html = _render(
|
||||
)
|
||||
# No <details> in the no-popup baseline — but the assertion still
|
||||
# holds defensively: locate every <details>...</details> block (if
|
||||
# any) and confirm no <script> tag appears inside.
|
||||
for block in re.findall(r"<details[\s>].*?</details>", html, re.DOTALL):
|
||||
assert "<script" not in block
|
||||
assert "addEventListener" not in block
|
||||
|
||||
|
||||
# ─── P-6 ─ restore semantics ────────────────────────────────────────
|
||||
|
||||
|
||||
def test_restore_uses_single_dataset_key():
|
||||
"""Restore strategy uses one dataset key
|
||||
(``dataset.imp90PrintRestore``) — no global Set/Map, no mutation
|
||||
of any other DOM attribute. Locks the minimal-surface contract."""
|
||||
html = _render()
|
||||
assert "imp90PrintRestore" in html
|
||||
# Restore branch only sets open=false when the prior state was '0'.
|
||||
assert "imp90PrintRestore === '0'" in html
|
||||
assert "d.open = true" in html
|
||||
Reference in New Issue
Block a user