- A05_EduDetail_Guide_Capture.py — 헤드리스 Edge(없으면 chrome) · 1600×900 · 2배 · 밝은 테마 · 장면마다 새 탭 · 노란 둥근 테두리 + 검은 번호 원 · 통째 / clip 요소 · WebP 품질 85 · _manual.json 건너뜀 - 장면 목록 _scenes.json 꼴 = 머리 설명(before · after 단계 click · contextmenu · fill · hover · press · eval · wait · wait_ms · frame = CAD iframe 안 · viewport · settle_ms · clip_pad · callouts pos · allow_post) - 지킴 — 위험 단추(저장 · 확정 · 초기화 · 적용 · 삭제 · 메일) 선택자 · 실제 글자 둘 다 거절 · confirm 은 after [취소] 필요 · 페이지 /api 쓰기 요청 끊음 - 지킴 시험 test_539_guide_capture.py 45 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
183 lines
6.1 KiB
Python
183 lines
6.1 KiB
Python
"""539-1 가이드 캡처 — 화면 없이 도는 부분(장면 목록 읽기 · 검사 · 이름 규칙 · _manual 건너뜀 · WebP)."""
|
|
|
|
import io
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "A05_EduDetail"))
|
|
import A05_EduDetail_Guide_Capture as G # noqa: E402
|
|
|
|
|
|
def _scene(**kw):
|
|
base = {"name": "B05_01_plan", "route": "#/b05-profile", "project": "road"}
|
|
return {**base, **kw}
|
|
|
|
|
|
def _folder(scenes, manual=None) -> Path:
|
|
d = Path(tempfile.mkdtemp())
|
|
(d / "_scenes.json").write_text(json.dumps(scenes, ensure_ascii=False), encoding="utf-8")
|
|
if manual is not None:
|
|
(d / "_manual.json").write_text(json.dumps(manual), encoding="utf-8")
|
|
return d
|
|
|
|
|
|
def test_good_scene_passes():
|
|
sc = _scene(
|
|
before=[{"click": ".card"}, {"wait": ".modal"}, {"wait_ms": 300}, {"hover": ".tip"}],
|
|
clip=".modal",
|
|
callouts=[{"n": 1, "sel": ".a"}, {"n": 2, "sel": ".b", "pos": "br"}],
|
|
after=[{"press": "Escape"}],
|
|
)
|
|
assert G.check_scene(sc, "B05") == []
|
|
assert G.load_scenes(_folder([sc]), "B05")[0]["name"] == "B05_01_plan"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"name",
|
|
["B05_1_plan", "b05_01_plan", "B05_01_Plan", "B05_01_평면", "B05-01-plan", "B06_01_plan", None],
|
|
)
|
|
def test_name_rule(name):
|
|
assert G.check_scene(_scene(name=name), "B05")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"bad",
|
|
[
|
|
{"route": "b05-profile"},
|
|
{"project": "mine"},
|
|
{"viewport": [100, 900]},
|
|
{"clip": ""},
|
|
{"callouts": [{"n": 1, "sel": ".a"}, {"n": 1, "sel": ".b"}]},
|
|
{"callouts": [{"n": 0, "sel": ".a"}]},
|
|
{"callouts": [{"n": 1, "sel": ".a", "pos": "mid"}]},
|
|
{"before": [{"click": ".a", "wait": ".b"}]},
|
|
{"before": [{"fill": ".a"}]},
|
|
{"before": [{"jump": ".a"}]},
|
|
{"allow_post": ["projects"]},
|
|
{"extra": 1},
|
|
],
|
|
)
|
|
def test_bad_fields(bad):
|
|
assert G.check_scene(_scene(**bad), "B05")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"sel",
|
|
[
|
|
"button:has-text('저장')",
|
|
"text=확정",
|
|
".reset-btn",
|
|
"button:has-text('초기화')",
|
|
"button:has-text('적용')",
|
|
"button:has-text('메일 보내기')",
|
|
"#save",
|
|
"button:has-text('삭제')",
|
|
],
|
|
)
|
|
def test_danger_click_rejected(sel):
|
|
errs = G.check_scene(_scene(before=[{"click": sel}]), "B05")
|
|
assert any("위험" in e for e in errs)
|
|
|
|
|
|
def test_danger_eval_rejected():
|
|
errs = G.check_scene(
|
|
_scene(before=[{"eval": "document.querySelector('.save').click()"}]), "B05"
|
|
)
|
|
assert any("위험" in e for e in errs)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"sel", ["button:has-text('취소')", "text=닫기", ".cancel", "button:has-text('내보내기')"]
|
|
)
|
|
def test_safe_click_passes(sel):
|
|
assert G.check_scene(_scene(before=[{"click": sel}]), "B05") == []
|
|
|
|
|
|
def test_confirm_needs_cancel_after():
|
|
sc = _scene(before=[{"click": "button:has-text('저장')", "confirm": True}])
|
|
assert any("after" in e for e in G.check_scene(sc, "B05"))
|
|
sc["after"] = [{"click": "button:has-text('취소')"}]
|
|
assert G.check_scene(sc, "B05") == []
|
|
sc["after"] = [{"click": "button:has-text('확인')", "confirm": True}]
|
|
assert G.check_scene(sc, "B05")
|
|
|
|
|
|
def test_is_danger_runtime_text():
|
|
assert G.is_danger("저장")
|
|
assert G.is_danger("추천 노선 적용")
|
|
assert not G.is_danger("취소")
|
|
assert not G.is_danger("엑셀 내보내기")
|
|
assert not G.is_danger("")
|
|
|
|
|
|
def test_load_reports_all_errors_and_duplicates():
|
|
d = _folder([_scene(), _scene(), _scene(name="B05_02_x", route="x")])
|
|
with pytest.raises(G.SceneError) as e:
|
|
G.load_scenes(d, "B05")
|
|
assert "이름 겹침: B05_01_plan" in str(e.value) and "route" in str(e.value)
|
|
|
|
|
|
def test_load_missing_and_broken():
|
|
with pytest.raises(G.SceneError):
|
|
G.load_scenes(Path(tempfile.mkdtemp()), "B05")
|
|
d = Path(tempfile.mkdtemp())
|
|
(d / "_scenes.json").write_text("[{", encoding="utf-8")
|
|
with pytest.raises(G.SceneError):
|
|
G.load_scenes(d, "B05")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"manual", [["B05_01_plan.webp"], ["B05_01_plan"], {"B05_01_plan.webp": {"by": "admin"}}]
|
|
)
|
|
def test_manual_names(manual):
|
|
assert G.manual_names(_folder([_scene()], manual)) == {"B05_01_plan"}
|
|
|
|
|
|
def test_manual_skipped_in_list(capsys):
|
|
d = _folder([_scene(), _scene(name="B05_02_cut")], ["B05_01_plan.webp"])
|
|
assert G.main(["--page", "B05", "--out", str(d), "--list"]) == 0
|
|
out = capsys.readouterr().out.splitlines()
|
|
assert "건너뜀" in out[0] and "건너뜀" not in out[1]
|
|
|
|
|
|
def test_pick_scenes():
|
|
scenes = [_scene(), _scene(name="B05_02_cut")]
|
|
assert [s["name"] for s in G.pick_scenes(scenes, ["B05_02"])] == ["B05_02_cut"]
|
|
assert len(G.pick_scenes(scenes, None)) == 2
|
|
with pytest.raises(G.SceneError):
|
|
G.pick_scenes(scenes, ["B05_09"])
|
|
|
|
|
|
def test_resolve_project():
|
|
assert G.resolve_project("none", "x", {}) is None
|
|
assert G.resolve_project("road", None, {"road": "r1"}) == "r1"
|
|
assert G.resolve_project("road", "cli", {"road": "r1"}) == "cli"
|
|
with pytest.raises(G.SceneError):
|
|
G.resolve_project("stab", None, {"road": "r1"})
|
|
|
|
|
|
def test_save_webp():
|
|
buf = io.BytesIO()
|
|
Image.new("RGBA", (64, 32), (255, 212, 0, 255)).save(buf, "PNG")
|
|
path = Path(tempfile.mkdtemp()) / "B05_01_plan.webp"
|
|
w, h, kb = G.save_webp(buf.getvalue(), path)
|
|
assert (w, h) == (64, 32) and kb > 0
|
|
with Image.open(path) as img:
|
|
assert img.format == "WEBP" and img.mode == "RGB"
|
|
|
|
|
|
def test_callout_and_clip_boxes():
|
|
ring, dot = G.callout_boxes({"x": 100, "y": 100, "width": 50, "height": 20}, "tl", 1600, 900)
|
|
assert ring == {"x": 96, "y": 96, "w": 58, "h": 28}
|
|
assert dot["x"] < ring["x"] and dot["y"] < ring["y"] # 바깥 왼쪽 위
|
|
_, edge = G.callout_boxes({"x": 0, "y": 0, "width": 50, "height": 20}, "tl", 1600, 900)
|
|
assert edge["x"] >= 0 and edge["y"] >= 0 # 화면 밖이면 안으로
|
|
clip = G.union_clip([ring, dot], 16, 1600, 900)
|
|
assert clip["x"] <= dot["x"] - 16 + 0.01 and clip["width"] > ring["w"]
|
|
assert G.union_clip([{"x": -50, "y": 0, "w": 2000, "h": 10}], 16, 1600, 900)["width"] == 1600
|