feat(#84): IMP-84 u1~u3 silent automation policy enforcement (FramePanel reject confirm + slide_base provisional badge/outline + IMP-30 visual assertions inverted)
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 21s

- u1 FramePanel.tsx: extract `applyFrameSelection(candidate, onFrameSelect)`
  pure helper; collapse `handleFrameSelect` to direct onFrameSelect for every
  V4 label; drop `window.confirm` reject popup (IMP-47B u11 regression noise
  per `feedback_auto_pipeline_first`). New vitest pin `imp84_framepanel_reject_silent.test.ts`
  covers helper invocation across all 4 V4 labels + source-presence pins.
- u2 templates/phase_z2/slide_base.html: delete `.zone--provisional` CSS,
  `.zone__needs-adaptation-badge` CSS, the zone--provisional class fragment
  in the zone div, and the badge `<span>` render at the provisional zone.
  Preserve `data-provisional="1"` attribute as silent telemetry. New pytest
  `tests/phase_z2/test_imp84_provisional_silent_render.py` pins the silent
  contract independently of the IMP-30 first-render file.
- u3 tests/test_phase_z2_imp30_first_render.py: invert the three IMP-30 u5
  positive provisional-visual assertions to IMP-84 silent-contract negatives
  (no class, no badge, no CSS selectors); preserve positive `data-provisional`
  telemetry assertions. Docstrings updated to IMP-84 silent contract.

Out of scope (Round #4 + #92 contract): Home.tsx `toast.error(aiReviewMsg)`
call line, designAgentApi.ts `api_error_kinds`/`api_error_kind` schema and
operational-only formatter, FramePanel reject badge/tooltip read-only labels
(L102/L147/L156), and backend `zone.provisional` flag emission.

Stage 4 PASS: u1 vitest 10/10, u2 pytest 5/5, u3 pytest 29/29 (incl. 3
IMP-84 inverted assertions: `test_imp84_provisional_zone_silent_no_class_no_badge`,
`test_imp84_provisional_badge_never_rendered_in_mixed_zones`,
`test_imp84_slide_base_css_strips_provisional_visual_selectors`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 14:15:02 +09:00
parent f0d4494409
commit b9747c2f4a
5 changed files with 449 additions and 89 deletions

View File

@@ -20,6 +20,19 @@ interface FramePanelProps {
onNoDesignToggle: () => void;
}
// IMP-#84 u1 — silent-automation contract: frame selection delegates directly
// to onFrameSelect for every V4 label (use_as_is / light_edit / restructure /
// reject). Prior IMP-47B u11 surfaced a window.confirm popup on reject; that
// popup is informational UI noise per `feedback_auto_pipeline_first` and is
// removed. Frame identity is preserved on reject (AI 재구성 = content-only,
// per AI 격리 contract); the popup never gated that contract.
export function applyFrameSelection(
candidate: FrameCandidate,
onFrameSelect: (frameId: string) => void,
): void {
onFrameSelect(candidate.id);
}
export default function FramePanel({
slidePlan,
selectedZone,
@@ -49,17 +62,9 @@ export default function FramePanel({
const handleFrameSelect = React.useCallback(
(candidate: FrameCandidate) => {
const isReject = candidate.label === "reject";
const alreadyApplied = currentFrameId === candidate.id;
if (isReject && !alreadyApplied) {
const ok = window.confirm(
`"${candidate.name}" 은 V4 reject 라벨입니다.\n선택 시 frame 은 유지되고 AI 가 콘텐츠를 frame 구조에 맞게 재구성합니다.\n계속하시겠습니까?`,
);
if (!ok) return;
}
onFrameSelect(candidate.id);
applyFrameSelection(candidate, onFrameSelect);
},
[currentFrameId, onFrameSelect],
[onFrameSelect],
);
if (!selectedZone) {

View File

@@ -0,0 +1,122 @@
// IMP-#84 u1 — FramePanel reject silent-automation contract.
//
// Stage 2 unit u1 scope:
// 1) `applyFrameSelection(candidate, onFrameSelect)` invokes onFrameSelect
// with candidate.id verbatim for EVERY V4 label
// (use_as_is / light_edit / restructure / reject) — no window.confirm
// gate, no label-conditional branch, no frame swap.
// 2) Source-presence checks pin the FramePanel.tsx wiring so the runtime
// button → handler → helper chain stays intact even though we cannot
// mount React (no jsdom / RTL / happy-dom in Front devDependencies —
// verified against the IMP-56 u20 `imp90_bottom_actions.test.ts` and
// IMP-92 u5 `imp47b_human_review_toast.test.tsx` precedent that
// explicitly skip DOM mounting).
// 3) No `window.confirm` substring remains in FramePanel.tsx after u1.
//
// Out of scope (Stage 2 exit-report contract):
// - Home.tsx:523-524 `toast.error(aiReviewMsg)` (#92 operational-only).
// - FramePanel reject badge/tooltip read-only labels at L102/L147/L156
// (no popup trigger; preserved as silent operator hint).
// - Backend `zone.provisional` emission (handled by u2 template-only).
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, it, expect, vi } from "vitest";
import { applyFrameSelection } from "../src/components/FramePanel";
import type { FrameCandidate } from "../src/types/designAgent";
const __dirname = dirname(fileURLToPath(import.meta.url));
const FRAME_PANEL_SOURCE = readFileSync(
resolve(__dirname, "../src/components/FramePanel.tsx"),
"utf-8",
);
function makeCandidate(
label: FrameCandidate["label"],
id: string,
): FrameCandidate {
return {
id,
name: `Frame ${id}`,
score: 0.5,
confidence: "medium",
label,
};
}
describe("applyFrameSelection (IMP-#84 u1 — silent-automation contract)", () => {
it("forwards candidate.id to onFrameSelect for use_as_is label", () => {
const onFrameSelect = vi.fn();
applyFrameSelection(makeCandidate("use_as_is", "frame_a"), onFrameSelect);
expect(onFrameSelect).toHaveBeenCalledTimes(1);
expect(onFrameSelect).toHaveBeenCalledWith("frame_a");
});
it("forwards candidate.id to onFrameSelect for light_edit label", () => {
const onFrameSelect = vi.fn();
applyFrameSelection(makeCandidate("light_edit", "frame_b"), onFrameSelect);
expect(onFrameSelect).toHaveBeenCalledTimes(1);
expect(onFrameSelect).toHaveBeenCalledWith("frame_b");
});
it("forwards candidate.id to onFrameSelect for restructure label", () => {
const onFrameSelect = vi.fn();
applyFrameSelection(makeCandidate("restructure", "frame_c"), onFrameSelect);
expect(onFrameSelect).toHaveBeenCalledTimes(1);
expect(onFrameSelect).toHaveBeenCalledWith("frame_c");
});
it("forwards candidate.id to onFrameSelect for reject label — no popup, no frame swap", () => {
// Reject is the silent-automation pivot case: prior IMP-47B u11 gated
// this path with window.confirm; post-IMP-#84 the helper invokes
// onFrameSelect with the reject frame.id directly. Backend / AI 격리
// contract handles AI 재구성 (content-only, frame preserved).
const onFrameSelect = vi.fn();
applyFrameSelection(makeCandidate("reject", "frame_d"), onFrameSelect);
expect(onFrameSelect).toHaveBeenCalledTimes(1);
expect(onFrameSelect).toHaveBeenCalledWith("frame_d");
});
it("does not call onFrameSelect more than once per invocation", () => {
const onFrameSelect = vi.fn();
applyFrameSelection(makeCandidate("reject", "frame_e"), onFrameSelect);
applyFrameSelection(makeCandidate("use_as_is", "frame_f"), onFrameSelect);
expect(onFrameSelect).toHaveBeenCalledTimes(2);
expect(onFrameSelect).toHaveBeenNthCalledWith(1, "frame_e");
expect(onFrameSelect).toHaveBeenNthCalledWith(2, "frame_f");
});
});
describe("FramePanel.tsx source — silent-automation wiring pins (IMP-#84 u1)", () => {
it("has no window.confirm(...) call (popup removed; narrative mentions in comments are allowed)", () => {
// Match the call form `window.confirm(` rather than the bare substring
// so that explanatory comments documenting the removed popup are not
// flagged. A re-introduced call would carry an opening paren.
expect(FRAME_PANEL_SOURCE).not.toMatch(/\bwindow\.confirm\s*\(/);
});
it("does not embed the legacy reject-confirm Korean prompt body", () => {
// Prior IMP-47B u11 string fragment; absence guards against re-introduction.
expect(FRAME_PANEL_SOURCE).not.toContain("V4 reject 라벨입니다");
expect(FRAME_PANEL_SOURCE).not.toContain("계속하시겠습니까?");
});
it("wires the button onClick to handleFrameSelect(candidate)", () => {
expect(FRAME_PANEL_SOURCE).toContain(
"onClick={() => handleFrameSelect(candidate)}",
);
});
it("delegates handleFrameSelect body to applyFrameSelection", () => {
expect(FRAME_PANEL_SOURCE).toContain(
"applyFrameSelection(candidate, onFrameSelect)",
);
});
it("exports applyFrameSelection as a named export for caller-independent reuse", () => {
expect(FRAME_PANEL_SOURCE).toMatch(
/export function applyFrameSelection\(/,
);
});
});

View File

@@ -114,42 +114,10 @@
min-height: 0;
}
/* ── IMP-30 u5 : provisional zone marker (first-render invariant) ──
When V4 rank-1 candidate falls outside MVP1_ALLOWED_STATUSES (chain_exhausted)
the pipeline still renders the rank-1 frame so the first-render invariant
holds, but the zone is tagged `provisional` so the user/AI can adapt later
(IMP-31). Visual contract:
- dashed amber border + striped wash → "needs adaptation" at a glance
- inline badge top-right → text label for non-color-perceiving readers
MDX content is preserved as-is; no shrink, no rewrite. */
.zone--provisional {
outline: 2px dashed #b8860b;
outline-offset: -2px;
background-image: repeating-linear-gradient(
45deg,
rgba(184, 134, 11, 0.04) 0,
rgba(184, 134, 11, 0.04) 8px,
transparent 8px,
transparent 16px
);
}
.zone--provisional .zone__needs-adaptation-badge {
position: absolute;
top: 4px;
right: 4px;
z-index: 10;
padding: 2px 6px;
background: #b8860b;
color: #fff;
font-size: 9px;
font-weight: 700;
line-height: 1.2;
letter-spacing: 0.04em;
border-radius: 2px;
text-transform: uppercase;
pointer-events: none;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
/* IMP-84: provisional zone visual treatment removed (silent-automation
policy). `data-provisional="1"` attribute is still emitted on the
zone div as silent telemetry for downstream selectors / inspection;
no user-visible outline, wash, or badge. */
/* ── Frame-family text layout contract (shared, reusable) ──
feedback-1 (mvp1.5b_test7): visible improvement 강화.
@@ -398,8 +366,7 @@
<div class="slide-body">
<div class="layout-{{ layout_preset }}">
{% for zone in zones %}
<div class="zone{% if zone.provisional %} zone--provisional{% endif %}" data-zone-position="{{ zone.position }}" data-template-id="{{ zone.template_id }}"{% if zone.provisional %} data-provisional="1"{% endif %}{% if zone.has_popup %} data-has-popup="1"{% endif %} style="grid-area: {{ zone.position }};">
{% if zone.provisional %}<span class="zone__needs-adaptation-badge" aria-label="needs user or AI adaptation">needs adaptation</span>{% endif %}
<div class="zone" data-zone-position="{{ zone.position }}" data-template-id="{{ zone.template_id }}"{% if zone.provisional %} data-provisional="1"{% endif %}{% if zone.has_popup %} data-has-popup="1"{% endif %} style="grid-area: {{ zone.position }};">
{{ zone.partial_html | safe }}
{% if zone.has_popup %}
{% set _popup_trigger = (zone.popup_binding.detail_trigger if zone.popup_binding else None) or {} %}

View File

@@ -0,0 +1,249 @@
"""IMP-84 u2 — provisional zone silent-render contract.
Pins that `templates/phase_z2/slide_base.html` no longer surfaces the
provisional visual treatment (dashed outline, striped wash, badge span)
while keeping `data-provisional="1"` as silent telemetry on the zone div.
Stage 2 binding contract (IMP-84):
- Remove .zone--provisional class emission on the zone div.
- Remove .zone__needs-adaptation-badge <span> render.
- Remove the .zone--provisional CSS block and the
.zone__needs-adaptation-badge CSS block from the <style> section.
- Preserve data-provisional="1" attribute emission for provisional zones
(downstream telemetry / debug selectors). Out-of-scope: backend
`zone.provisional` flag emission itself.
Helpers below intentionally mirror the IMP-30 first-render test helpers
(_render_slide_base / _all_zone_div_openings / _all_badge_spans /
_zone_div_for_position) so the silent-render contract is enforced by an
independent rendering surface, not by the IMP-30 file (which u3 will
invert separately).
"""
from __future__ import annotations
import re
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape
# ─── helpers (mirrored from IMP-30 u5 to keep this test self-contained) ───
def _render_slide_base(
zones: list[dict],
*,
layout_preset: str = "single",
layout_css: dict | None = None,
) -> str:
"""Render templates/phase_z2/slide_base.html via Jinja2 with a minimal
zones list. Bypasses render_slide() so the template-only silent-render
contract is exercised without the pipeline (no mapper, no contracts,
no token CSS loader). slot_payload / partial_html are stubbed so the
assertions focus on zone div / CSS surface only."""
template_dir = (
Path(__file__).resolve().parents[2] / "templates" / "phase_z2"
)
env = Environment(
loader=FileSystemLoader(str(template_dir)),
autoescape=select_autoescape(["html"]),
)
if layout_css is None:
layout_css = {
"cols": "1fr",
"rows": "1fr",
"areas": '"single"',
}
for z in zones:
z.setdefault("partial_html", "<div class=\"_stub_partial\">stub</div>")
base = env.get_template("slide_base.html")
return base.render(
slide_title="IMP-84 u2 silent-render test",
slide_footer=None,
zones=zones,
layout_preset=layout_preset,
layout_css=layout_css,
gap_px=12,
token_css="",
embedded_mode="standalone",
)
def _zone_div_for_position(html: str, position: str) -> str:
"""Return the opening `<div class="zone..." data-zone-position="X" ...>`
tag for the zone at the given `data-zone-position`. Anchors assertions
on zone-div-level attributes / classes only."""
pattern = re.compile(
r'<div class="zone[^"]*"\s+data-zone-position="'
+ re.escape(position)
+ r'"[^>]*>',
re.DOTALL,
)
match = pattern.search(html)
return match.group(0) if match else ""
def _all_zone_div_openings(html: str) -> list[str]:
"""Every zone-div opening tag in the layout body. Scopes class /
attribute checks away from the <style> block (which may still contain
selector strings if a future change re-introduces them)."""
return re.findall(
r'<div class="zone[^"]*"[^>]*data-zone-position="[^"]*"[^>]*>',
html,
)
def _all_badge_spans(html: str) -> list[str]:
"""Every `.zone__needs-adaptation-badge` <span> element in the rendered
body. Must be empty under the IMP-84 silent-render contract regardless
of zones[i].provisional value."""
return re.findall(
r'<span class="zone__needs-adaptation-badge"[^>]*>[^<]*</span>',
html,
)
# ─── case 1 : non-provisional zone unchanged (regression boundary) ───
def test_imp84_non_provisional_zone_unchanged():
"""zones[i].provisional=False must render the zone div with no
provisional class, no data-provisional attr, no badge — byte-equivalent
to pre-IMP-84 baseline for the non-provisional path."""
zones = [
{
"position": "single",
"template_id": "MOCK_template_direct_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": False,
}
]
html = _render_slide_base(zones)
zone_open = _zone_div_for_position(html, "single")
assert zone_open != ""
assert "zone--provisional" not in zone_open
assert "data-provisional" not in zone_open
assert _all_badge_spans(html) == []
# ─── case 2 : provisional zone silent — class / badge / wash removed ───
def test_imp84_provisional_zone_emits_data_attr_only_no_visual():
"""The core IMP-84 silent-render contract.
With zones[i].provisional=True, the rendered HTML MUST:
- NOT contain the `zone--provisional` class on any zone div
- NOT render a `.zone__needs-adaptation-badge` <span>
- NOT contain the human-visible "needs adaptation" label text
- STILL emit `data-provisional="1"` on the provisional zone div
(silent telemetry preserved for downstream selectors)
"""
zones = [
{
"position": "single",
"template_id": "MOCK_template_restructure_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": True,
}
]
html = _render_slide_base(zones)
zone_open = _zone_div_for_position(html, "single")
assert zone_open != ""
assert "zone--provisional" not in zone_open
assert 'data-provisional="1"' in zone_open
assert _all_badge_spans(html) == []
assert "needs adaptation" not in html
# ─── case 3 : mixed zones — telemetry isolation preserved ───
def test_imp84_mixed_zones_data_provisional_only_on_provisional_zone():
"""In a mixed-zone slide (one provisional + one normal), the silent
telemetry attribute must appear ONLY on the provisional zone div, and
no visual artifact may surface on either zone."""
zones = [
{
"position": "top",
"template_id": "MOCK_template_direct_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": False,
},
{
"position": "bottom",
"template_id": "MOCK_template_restructure_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": True,
},
]
layout_css = {
"cols": "1fr",
"rows": "1fr 1fr",
"areas": '"top" "bottom"',
}
html = _render_slide_base(
zones, layout_preset="vertical-2", layout_css=layout_css
)
zone_divs = _all_zone_div_openings(html)
assert len(zone_divs) == 2
top_zone_open = _zone_div_for_position(html, "top")
bottom_zone_open = _zone_div_for_position(html, "bottom")
assert "data-provisional" not in top_zone_open
assert 'data-provisional="1"' in bottom_zone_open
for tag in zone_divs:
assert "zone--provisional" not in tag
assert _all_badge_spans(html) == []
assert "needs adaptation" not in html
# ─── case 4 : <style> block free of provisional visual selectors ───
def test_imp84_style_block_has_no_provisional_visual_selectors():
"""The provisional visual CSS classes are deleted at source. A future
refactor that re-introduces `.zone--provisional` or
`.zone__needs-adaptation-badge` selectors into slide_base.html breaks
this test rather than silently restoring the visual badge."""
zones = [
{
"position": "single",
"template_id": "MOCK_template_restructure_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": True,
}
]
html = _render_slide_base(zones)
assert ".zone--provisional" not in html
assert ".zone__needs-adaptation-badge" not in html
assert "zone__needs-adaptation-badge" not in html
# ─── case 5 : provisional defaults to false (template fallback) ───
def test_imp84_provisional_none_falls_back_to_silent_non_provisional():
"""When zones[i].provisional is explicitly None (falsy but not False),
the template's truthy check must NOT emit `data-provisional`. Pins the
template fallback so a refactor cannot silently invert the default."""
zones = [
{
"position": "single",
"template_id": "MOCK_template_direct_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": None,
}
]
html = _render_slide_base(zones)
zone_open = _zone_div_for_position(html, "single")
assert zone_open != ""
assert "data-provisional" not in zone_open
assert _all_badge_spans(html) == []

View File

@@ -676,12 +676,20 @@ def test_u5_zone_without_provisional_key_treated_as_non_provisional():
# ─── u5 case 2 : provisional zone renders class + badge + data attr ───
def test_u5_provisional_zone_renders_class_and_badge():
"""Opt-in path. zones[i].provisional=True must:
1. Append `zone--provisional` class to the zone div.
2. Set `data-provisional="1"` data attribute (for downstream selectors).
3. Render a `<span class="zone__needs-adaptation-badge">` element with
the literal text "needs adaptation" (aria-label included for a11y).
def test_imp84_provisional_zone_silent_no_class_no_badge():
"""IMP-84 silent-automation inversion of the prior IMP-30 u5 contract.
Under the silent contract, zones[i].provisional=True must:
1. NOT append `zone--provisional` class to the zone div (no user-visible
outline / striped wash).
2. Still set `data-provisional="1"` data attribute as silent telemetry
for downstream selectors / inspection.
3. NOT render any `<span class="zone__needs-adaptation-badge">` element
and NOT surface the literal text "needs adaptation" or its
aria-label (no user-facing badge).
Scope: assertions target the zone div body. The CSS <style> block must
likewise not carry the removed visual selectors — that surface is pinned
in `test_imp84_slide_base_css_strips_provisional_visual_selectors` below.
"""
zones = [
{
@@ -694,21 +702,26 @@ def test_u5_provisional_zone_renders_class_and_badge():
}
]
html = _render_slide_base(zones)
# zone--provisional class must appear on the zone div for position=single.
assert "zone--provisional" in html
# data-provisional="1" attribute must be present.
assert 'data-provisional="1"' in html
# Badge element with the required label text.
assert 'class="zone__needs-adaptation-badge"' in html
assert "needs adaptation" in html
assert 'aria-label="needs user or AI adaptation"' in html
zone_divs = _all_zone_div_openings(html)
assert len(zone_divs) == 1
# No zone--provisional class on the zone div (visual removed).
assert "zone--provisional" not in zone_divs[0]
# data-provisional="1" attribute still present as silent telemetry.
assert 'data-provisional="1"' in zone_divs[0]
# No badge <span> element and no badge label text anywhere in the body.
assert _all_badge_spans(html) == []
assert "needs adaptation" not in html
assert 'aria-label="needs user or AI adaptation"' not in html
def test_u5_provisional_badge_appears_inside_provisional_zone_only():
"""Mixed-zone slide: one provisional zone + one normal zone. The badge
+ class must appear ONLY in the provisional zone, not bleed into the
normal one (CSS-level isolation should already prevent this, but the
template must not emit the badge for both)."""
def test_imp84_provisional_badge_never_rendered_in_mixed_zones():
"""IMP-84 silent-automation inversion of the prior IMP-30 u5 mixed-zone
contract. Mixed-zone slide: one provisional zone + one normal zone. The
silent contract requires that NO badge span and NO `zone--provisional`
class be emitted on either zone div. The provisional zone is identifiable
only through the silent `data-provisional="1"` telemetry attribute, which
must be scoped to the provisional zone alone (no bleed onto the normal
zone)."""
zones = [
{
"position": "top",
@@ -735,21 +748,21 @@ def test_u5_provisional_badge_appears_inside_provisional_zone_only():
html = _render_slide_base(
zones, layout_preset="vertical-2", layout_css=layout_css
)
# Exactly one badge span element should be present in the rendered body
# (CSS selector in <style> excluded by the helper).
assert len(_all_badge_spans(html)) == 1
# zone--provisional must appear on exactly one zone div (CSS selector
# in <style> excluded by the helper).
# No badge <span> element should be rendered anywhere in the body
# (silent-automation policy).
assert _all_badge_spans(html) == []
# No zone div should carry the zone--provisional class (visual removed).
zone_divs = _all_zone_div_openings(html)
assert len(zone_divs) == 2
provisional_zone_divs = [d for d in zone_divs if "zone--provisional" in d]
assert len(provisional_zone_divs) == 1
# The provisional class must be associated with the bottom zone.
assert all("zone--provisional" not in d for d in zone_divs)
# data-provisional="1" telemetry must be present on the bottom (provisional)
# zone only — never on the top (non-provisional) zone.
bottom_zone_open = _zone_div_for_position(html, "bottom")
assert "zone--provisional" in bottom_zone_open
assert "zone__needs-adaptation-badge" in bottom_zone_open
# The top zone must NOT carry the provisional class.
assert 'data-provisional="1"' in bottom_zone_open
assert "zone--provisional" not in bottom_zone_open
assert "zone__needs-adaptation-badge" not in bottom_zone_open
top_zone_open = _zone_div_for_position(html, "top")
assert 'data-provisional="1"' not in top_zone_open
assert "zone--provisional" not in top_zone_open
assert "zone__needs-adaptation-badge" not in top_zone_open
@@ -779,15 +792,19 @@ def test_u5_zones_data_provisional_field_defaults_false_in_template():
assert _all_badge_spans(html) == []
def test_u5_slide_base_css_carries_provisional_marker_styles():
"""The provisional visual contract (dashed outline + striped wash + badge)
is defined in slide_base.html <style>. Pin that the relevant CSS class
selectors exist in the rendered HTML so a refactor that removes them
breaks this test rather than silently rendering an unstyled badge.
def test_imp84_slide_base_css_strips_provisional_visual_selectors():
"""IMP-84 silent-automation inversion of the prior IMP-30 u5 CSS-presence
contract. The provisional visual treatment (dashed outline + striped wash
+ badge) was deleted from `slide_base.html <style>` by IMP-84 u2. Pin
that the CSS class selectors `.zone--provisional` and
`.zone__needs-adaptation-badge` no longer appear in the rendered HTML —
a refactor that re-introduces them must break this test rather than
silently re-surfacing the removed visual signal.
This is a class-selector existence check; it does not validate the
specific color / dash pattern, which is a design decision intentionally
left malleable (e.g., palette swap for a different theme)."""
Scope: the assertion targets the entire rendered HTML (style block plus
body). Since the body badge span is also gone (covered separately above),
any occurrence of these strings in the rendered output would only come
from a regressed style block."""
zones = [
{
"position": "single",
@@ -799,9 +816,9 @@ def test_u5_slide_base_css_carries_provisional_marker_styles():
}
]
html = _render_slide_base(zones)
# Style block must define .zone--provisional and the badge selector.
assert ".zone--provisional" in html
assert ".zone__needs-adaptation-badge" in html
# Style block must NOT define .zone--provisional or the badge selector.
assert ".zone--provisional" not in html
assert ".zone__needs-adaptation-badge" not in html
# ════════════════════════════════════════════════════════════════════════