Files
Aislo/resources/tester/test_507_1_exposed_mark_screen.py
T
eomsangdonandClaude Opus 5.5 254b5267ff fix(507-1 노출형 횡단수로 · 개거 = 횡단면 표시만): 부품 상자 형상 「표시만」(mark · 읽는 칸 없음 · 그림 없음) · 두 종류 처음값 = 측점 세움(anchor) + 표시만 · B05 「횡단」 판 · 임시 횡단 창 = 머리 걷기 전 구조물 이름 칩 남김 — 요구 311-2
- 사용자 마스터(구조물정의.json 반영) 안 고침 · M02 표 형상 칸에서 「표시만」 고름
- 시험 약속 바뀜: 322-1 처음값 · 272-1 · 322-3 측점 120 더함 · 322-2 218 규칙 보기 = 반사경 · 505-3 칩에 이름
- 새 시험 test_507_1_exposed_mark(.py · _screen)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:49:05 +09:00

190 lines
8.3 KiB
Python

"""PLAN 507-1 화면 — 자기 포트(5179) Playwright 머리 없는 브라우저 · 서버가 떠 있을 때만 · 검증_sub4.
- B05 「구조물 배치」 노출형 횡단수로 7+7(147 m) 추가(세션만 · [저장] 안 함) → 측점 목록에 섬
- 「횡단」 판 · 임시 횡단 창 = B06 카드 + 이름 칩 「노출형 횡단수로」 · 구조물 형상 0
- B06 횡단 카드 머리 = 이름 · 형상 0 → 지움(세션) · 측점 목록에서 빠짐
- 계정 화면 취향만 시험 앞 값으로 되돌림 · DB · storage 안 씀
./venv/Scripts/python.exe -m pytest resources/tester/test_507_1_exposed_mark_screen.py -q
"""
from __future__ import annotations
import json
import urllib.request
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
BASE = "http://localhost:5179"
PROJECT = "c725d747-50ae-46b0-87ce-4d4e56a53cf3" # 검증_sub4(OWNERS.md)
SHOTS = ROOT / "tmp" / "shots_507_1"
AT_M = 147 # 7+7 — 격자 · 관(19+8) 측점과 안 겹침
NAME = "노출형 횡단수로"
PREFS = "/api/dashboard/me/ui-prefs"
TOUCHED = ("profile-collapsed", "profile-height", "b05-routeedit-cross-v2")
#: 구조물 형상 그림(관 · 박스 · 세월교 · 벽 · 맹암거) — 표시만이면 0
SHAPES = (
".b06-chart__culvert-pipe, [class*='b06-chart__box'], [class*='b06-chart__ford'],"
" [class*='b06-chart__wall'], [class*='underdrain']"
)
def _up() -> bool:
try:
urllib.request.urlopen(BASE, timeout=3)
return True
except Exception:
return False
pytestmark = pytest.mark.skipif(not _up(), reason="5179 서버 없음")
JS_ADD = r"""async ([no, dist]) => {
const w = (ms) => new Promise((r) => setTimeout(r, ms));
const sec = document.querySelector('.b05-structure-section');
const fire = (el, v) => { el.value = v; el.dispatchEvent(new Event('change', { bubbles: true })); };
const [group, type] = sec.querySelectorAll('.b05-structure__grid select');
fire(group, 'A'); await w(300);
fire(type, 'cross_drain_exposed'); await w(500);
const [a, b] = sec.querySelectorAll('.b05-structure__position-row > label')[2].querySelectorAll('input');
fire(a, String(no)); fire(b, String(dist)); await w(200);
sec.querySelector('.is-primary').click(); await w(1500);
}"""
JS_ITEMS = (
"() => [...document.querySelectorAll('.b05-route__irregular-list li')]"
".map((l) => l.textContent.trim())"
)
JS_SCROLL = r"""(m) => {
const marker = [...document.querySelectorAll('.b06-chart__station')]
.find((e) => Math.abs(+e.getAttribute('data-chainage') - m) < 1e-6);
const body = marker.closest('.b05-route-profile__body');
body.scrollLeft = Math.max(0, +marker.querySelector('line').getAttribute('x1') - body.clientWidth / 2);
}"""
JS_POINT = r"""(m) => {
const marker = [...document.querySelectorAll('.b06-chart__station')]
.find((e) => Math.abs(+e.getAttribute('data-chainage') - m) < 1e-6);
const r = marker.querySelector('.b06-chart__station-line').getBoundingClientRect();
return { x: r.left + r.width / 2, y: r.top + r.height * 0.3 };
}"""
JS_CARD = r"""(scope) => {
const root = document.querySelector(scope);
const card = root?.querySelector('.b06-cross-card');
return {
card: !!card,
chips: [...(root?.querySelectorAll('.b05-cross-chips > *') ?? [])].map((e) => e.textContent),
shapes: card ? card.querySelectorAll(SHAPES).length : -1,
};
}""".replace("SHAPES", json.dumps(SHAPES))
JS_B06 = r"""(c) => ({
head: c.querySelector(':scope > header .b06-cross-card__structure')?.textContent ?? null,
label: c.querySelector('.b06-cross-card__title strong')?.textContent ?? null,
shapes: c.querySelectorAll(SHAPES).length,
})""".replace("SHAPES", json.dumps(SHAPES))
@pytest.fixture(scope="module")
def page():
sync = pytest.importorskip("playwright.sync_api")
cred = json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8"))
with sync.sync_playwright() as play:
chrome = play.chromium.launch(channel="msedge", headless=True)
ctx = chrome.new_context(viewport={"width": 1600, "height": 1000})
login = {k: cred[k] for k in ("email", "password")}
assert ctx.request.post(f"{BASE}/api/auth/login/request", data=login, timeout=60000).ok
prefs = ctx.request.get(f"{BASE}{PREFS}").json().get("prefs", {})
tab = ctx.new_page()
tab.add_init_script(f"localStorage.setItem('frd_current_project_id', '{PROJECT}')")
tab.goto(f"{BASE}/#/b05-profile", wait_until="domcontentloaded")
tab.wait_for_function("() => !!window.__aisloPipeline", timeout=90000)
tab.wait_for_timeout(8000)
SHOTS.mkdir(parents=True, exist_ok=True)
try:
yield tab
finally:
tab.wait_for_timeout(2000)
now = ctx.request.get(f"{BASE}{PREFS}").json().get("prefs", {})
for key in (f"aislo:pref:{name}" for name in TOUCHED):
if key in prefs:
now[key] = prefs[key]
else:
now.pop(key, None)
assert ctx.request.put(f"{BASE}{PREFS}", data={"prefs": now}).ok
chrome.close()
def _profile(page, open_: bool) -> None:
height = page.evaluate(
"() => document.querySelector('.b05-route-profile')?.getBoundingClientRect().height ?? 0"
)
if (height > 100) != open_:
page.get_by_text("종단", exact=True).last.click()
page.wait_for_timeout(3000)
def test_1_추가_측점_섬(page):
before = page.evaluate(JS_ITEMS)
assert not any(NAME in t for t in before)
page.evaluate(JS_ADD, [AT_M // 20, AT_M % 20])
page.wait_for_timeout(4000)
items = page.evaluate(JS_ITEMS)
assert f"7+7{NAME}" in items, items
assert len(items) == len(before) + 1
def test_2_횡단_판_이름_칩_형상_0(page):
_profile(page, False)
page.locator(".b05-route__irregular-item", has_text=NAME).first.click()
page.wait_for_timeout(4000)
got = page.evaluate(JS_CARD, ".b05-routeedit__cross")
page.locator(".b05-routeedit__cross").first.screenshot(path=str(SHOTS / "panel.png"))
assert got["card"] and NAME in got["chips"] and got["shapes"] == 0, got
def test_3_임시_횡단_이름_칩_형상_0(page):
_profile(page, True)
page.evaluate(JS_SCROLL, AT_M)
page.wait_for_timeout(800)
at = page.evaluate(JS_POINT, AT_M)
page.mouse.move(at["x"], at["y"])
page.wait_for_timeout(300)
page.mouse.click(at["x"], at["y"])
page.wait_for_function(
"() => document.querySelector('.b05-crossmodal .b06-cross-card')", timeout=30000
)
page.wait_for_timeout(1500)
station = page.evaluate("() => document.querySelector('.b05-crossmodal__station').textContent")
got = page.evaluate(JS_CARD, ".b05-crossmodal")
page.locator(".b05-crossmodal").screenshot(path=str(SHOTS / "modal.png"))
page.locator(".b05-crossmodal").get_by_text("닫기").click()
assert station == "7+7" and NAME in got["chips"] and got["shapes"] == 0, (station, got)
def test_4_B06_카드_머리_이름_형상_0_지움(page):
page.evaluate("() => { location.hash = '#/b06-section'; }")
page.wait_for_timeout(9000)
page.locator(".b05-route__irregular-item:visible", has_text=NAME).first.click()
page.wait_for_timeout(3000)
card = page.locator(".b06-cross-card:visible", has_text=NAME).first
card.scroll_into_view_if_needed()
page.wait_for_timeout(1500)
got = card.evaluate(JS_B06)
card.screenshot(path=str(SHOTS / "b06_card.png"))
assert got == {"head": NAME, "label": "7+7", "shapes": 0}, got
# 되돌림 — 고른 채로 세션에서 지움([저장] 안 함)
section = page.locator(".b05-structure-section:visible").first
if not section.locator("button.is-danger", has_text="삭제").first.is_visible():
page.get_by_text("구조물 배치", exact=True).first.click() # B06 = 칸이 접혀 있음
page.wait_for_timeout(1500)
# 「구조물 전체 삭제」 와 헷갈리지 않게 글이 꼭 「삭제」 인 것만
remove = section.get_by_role("button", name="삭제", exact=True)
if remove.is_disabled(): # 칸을 펴면 고름이 풀림 — 다시 고름
page.locator(".b05-route__irregular-item:visible", has_text=NAME).first.click()
page.wait_for_timeout(1500)
remove.click()
page.wait_for_timeout(3000)
left = page.evaluate(JS_ITEMS)
page.screenshot(path=str(SHOTS / "b06_after_delete.png"))
assert not any(NAME in t for t in left), left