feat(A05 가이드 캡처): 장면마다 sessionStorage 비움 · clip · 번호 대상 찍기 전 스크롤 · before 단계 select · scroll
- 장면 첫 적재 때 sessionStorage 비움(window.name 표시 · 장면 안 새로고침은 지킴) · localStorage 프로젝트 · 테마 그대로 - clip · callouts 대상 scrollIntoView 뒤 자리 잼 — 화면 밖 대상 번호 어긋남 막음 - 단계 열쇠 select(value · Playwright select_option) · scroll(선택자) · 머리 설명 갱신 - 시험 test_539_guide_capture 덧(select · scroll 통과 · select value 없음 · scroll 빈 글 거절) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,8 @@
|
||||
단계 = 아래 열쇠 중 하나 + 덧붙임
|
||||
{"click": "<css>"} 누름 · {"click": …, "confirm": true} = 확인창만 여는 위험 단추(아래)
|
||||
{"fill": "<css>", "value": "글"} 칸 채움
|
||||
{"select": "<css>", "value": "값"} 고르기 칸(<select>) 값 고름(option value 또는 글)
|
||||
{"scroll": "<css>"} 그 요소가 화면에 들어오게 스크롤
|
||||
{"hover": "<css>"} 마우스 올림(툴팁)
|
||||
{"contextmenu": "<css>"} 오른쪽 누름(누름 메뉴 열기)
|
||||
{"press": "Escape"} 키 · {"press": "Enter", "sel": "<css>"} = 그 요소에서
|
||||
@@ -49,6 +51,9 @@
|
||||
- 확인창을 찍을 때만 `"confirm": true` 로 그 단추를 누름 — 그 장면은 after 에 누름 단계가 있어야 함([취소])
|
||||
- 페이지의 /api 쓰기 요청(PUT · PATCH · DELETE · allow_post 밖 POST)은 모두 끊음 · 끊은 요청은 표에 적음
|
||||
- 브라우저 확인창(window.confirm)은 늘 취소 · 새 컨텍스트라 사용자 브라우저 · 저장소와 안 섞임
|
||||
- 장면마다 새 탭 · sessionStorage 비우고 시작(앞 장면 조작이 안 남음) · localStorage 는 프로젝트 · 테마 ·
|
||||
말만 세움
|
||||
- clip · callouts 대상은 찍기 전 화면 안으로 스크롤(clip 먼저 · 번호 차례) 뒤 자리를 잼
|
||||
- `_manual.json`(관리자가 손으로 바꾼 그림 · 이름 목록 또는 이름 열쇠 사전) 이름은 건너뜀(--force 면 찍음)
|
||||
|
||||
3D(WebGL) · 지도(카카오) · CAD(iframe)
|
||||
@@ -80,7 +85,9 @@ STEP_TIMEOUT = 15000
|
||||
SETTLE_MS = 500
|
||||
CLIP_PAD = 16
|
||||
PROJECT_KEYS = ("road", "stab", "plan", "none")
|
||||
STEP_KINDS = ("click", "contextmenu", "fill", "hover", "press", "eval", "wait", "wait_ms")
|
||||
STEP_KINDS = (
|
||||
"click", "contextmenu", "fill", "select", "hover", "press", "eval", "wait", "wait_ms", "scroll",
|
||||
) # fmt: skip
|
||||
SCENE_KEYS = {
|
||||
"name", "route", "project", "viewport", "before", "settle_ms", "clip", "clip_pad",
|
||||
"callouts", "after", "allow_post",
|
||||
@@ -96,6 +103,7 @@ DANGER_RE = re.compile(
|
||||
SAFE_RE = re.compile(r"취소|닫기|\b(cancel|close)", re.I)
|
||||
THEME_KEY, LANG_KEY, PROJECT_KEY = "frd_theme", "frd_lang", "frd_current_project_id"
|
||||
CALLOUT_CLASS = "__guide_callout"
|
||||
SEEDED = "__guide_seeded" # 탭 이름(window.name)에 남겨 첫 적재를 가림
|
||||
YELLOW = "#FFD400"
|
||||
RING_PAD, DOT = 4, 28 # 테두리 바깥 여백 · 번호 원 지름(CSS px)
|
||||
|
||||
@@ -142,8 +150,8 @@ def _check_steps(steps: Any, where: str) -> tuple[list[str], bool]:
|
||||
if not isinstance(val, str) or not val.strip():
|
||||
errs.append(f"{at} {kind} 값이 빈 글")
|
||||
continue
|
||||
if kind == "fill" and not isinstance(st.get("value"), str):
|
||||
errs.append(f"{at} fill 은 value(글) 필요")
|
||||
if kind in ("fill", "select") and not isinstance(st.get("value"), str):
|
||||
errs.append(f"{at} {kind} 은 value(글) 필요")
|
||||
if st.get("confirm"):
|
||||
if kind != "click":
|
||||
errs.append(f"{at} confirm 은 click 에만")
|
||||
@@ -356,6 +364,8 @@ def run_steps(page: Any, steps: list[dict[str, Any]]) -> None:
|
||||
page.wait_for_timeout(st["wait_ms"])
|
||||
elif "wait" in st:
|
||||
_locate(page, st["wait"], frame).wait_for(state="visible", timeout=timeout)
|
||||
elif "scroll" in st:
|
||||
_locate(page, st["scroll"], frame).scroll_into_view_if_needed(timeout=timeout)
|
||||
elif "eval" in st:
|
||||
target = page.frame_locator(frame).locator(":root") if frame else page
|
||||
target.evaluate(f"() => ({st['eval']})")
|
||||
@@ -365,7 +375,7 @@ def run_steps(page: Any, steps: list[dict[str, Any]]) -> None:
|
||||
else:
|
||||
page.keyboard.press(st["press"])
|
||||
else:
|
||||
kind = next(k for k in ("click", "contextmenu", "fill", "hover") if k in st)
|
||||
kind = next(k for k in ("click", "contextmenu", "fill", "select", "hover") if k in st)
|
||||
loc = _locate(page, st[kind], frame)
|
||||
loc.wait_for(state="visible", timeout=timeout)
|
||||
if kind == "click":
|
||||
@@ -377,6 +387,8 @@ def run_steps(page: Any, steps: list[dict[str, Any]]) -> None:
|
||||
loc.click(button="right", timeout=timeout)
|
||||
elif kind == "fill":
|
||||
loc.fill(st["value"], timeout=timeout)
|
||||
elif kind == "select":
|
||||
loc.select_option(st["value"], timeout=timeout)
|
||||
else:
|
||||
loc.hover(timeout=timeout)
|
||||
|
||||
@@ -396,7 +408,7 @@ def _box(page: Any, sel: str, frame: str | None, what: str) -> dict[str, float]:
|
||||
def shoot_scene(
|
||||
context: Any, base: str, scene: dict[str, Any], pid: str | None
|
||||
) -> tuple[bytes, list[str]]:
|
||||
"""장면 하나 → (PNG 2배, 끊은 쓰기 요청). 새 탭마다 sessionStorage 새로."""
|
||||
"""장면 하나 → (PNG 2배, 끊은 쓰기 요청). 새 탭 · 첫 적재 때 sessionStorage 비움."""
|
||||
vw, vh = scene.get("viewport") or VIEWPORT
|
||||
allow = scene.get("allow_post") or []
|
||||
blocked: list[str] = []
|
||||
@@ -406,7 +418,10 @@ def shoot_scene(
|
||||
seed = json.dumps([THEME_KEY, LANG_KEY, PROJECT_KEY, pid])
|
||||
page.add_init_script(
|
||||
f"(([t, l, k, pid]) => {{ localStorage.setItem(t, 'light'); localStorage.setItem(l, 'ko');"
|
||||
f" pid ? localStorage.setItem(k, pid) : localStorage.removeItem(k); }})({seed});"
|
||||
f" pid ? localStorage.setItem(k, pid) : localStorage.removeItem(k);"
|
||||
# 첫 적재만 비움 — 장면 안 새로고침 · 이동은 그 장면 조작을 지킴
|
||||
f" if (!window.name.includes('{SEEDED}')) {{ sessionStorage.clear();"
|
||||
f" window.name += '{SEEDED}'; }} }})({seed});"
|
||||
)
|
||||
page.on("dialog", lambda d: d.dismiss())
|
||||
|
||||
@@ -431,6 +446,15 @@ def shoot_scene(
|
||||
run_steps(page, scene.get("before") or [])
|
||||
page.wait_for_timeout(scene.get("settle_ms", SETTLE_MS))
|
||||
|
||||
# 찍을 대상을 화면 안으로 — clip 먼저 · 번호 차례(어긋난 번호 · 잘린 그림 막음)
|
||||
targets = [(c["sel"], c.get("frame")) for c in scene.get("callouts") or []]
|
||||
if scene.get("clip"):
|
||||
targets.insert(0, (scene["clip"], None))
|
||||
for sel, frame in targets:
|
||||
try:
|
||||
_locate(page, sel, frame).scroll_into_view_if_needed(timeout=5000)
|
||||
except Exception:
|
||||
pass # 안 보이면 아래 _box 가 까닭을 냄
|
||||
items, boxes = [], []
|
||||
for c in scene.get("callouts") or []:
|
||||
ring, dot = callout_boxes(_box(page, c["sel"], c.get("frame"), f"번호 {c['n']}"),
|
||||
|
||||
@@ -28,7 +28,14 @@ def _folder(scenes, manual=None) -> Path:
|
||||
|
||||
def test_good_scene_passes():
|
||||
sc = _scene(
|
||||
before=[{"click": ".card"}, {"wait": ".modal"}, {"wait_ms": 300}, {"hover": ".tip"}],
|
||||
before=[
|
||||
{"click": ".card"},
|
||||
{"wait": ".modal"},
|
||||
{"wait_ms": 300},
|
||||
{"hover": ".tip"},
|
||||
{"select": "#kind", "value": "plan"},
|
||||
{"scroll": ".far"},
|
||||
],
|
||||
clip=".modal",
|
||||
callouts=[{"n": 1, "sel": ".a"}, {"n": 2, "sel": ".b", "pos": "br"}],
|
||||
after=[{"press": "Escape"}],
|
||||
@@ -57,6 +64,8 @@ def test_name_rule(name):
|
||||
{"callouts": [{"n": 1, "sel": ".a", "pos": "mid"}]},
|
||||
{"before": [{"click": ".a", "wait": ".b"}]},
|
||||
{"before": [{"fill": ".a"}]},
|
||||
{"before": [{"select": ".a"}]},
|
||||
{"before": [{"scroll": ""}]},
|
||||
{"before": [{"jump": ".a"}]},
|
||||
{"allow_post": ["projects"]},
|
||||
{"extra": 1},
|
||||
|
||||
Reference in New Issue
Block a user