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

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:
2026-05-26 06:12:13 +09:00
parent 943957562f
commit 4da22adb43
29 changed files with 4937 additions and 78 deletions

View 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