diff --git a/A00_Common/design/design_structure_form.ts b/A00_Common/design/design_structure_form.ts index 1448cc72e..fac7319a8 100644 --- a/A00_Common/design/design_structure_form.ts +++ b/A00_Common/design/design_structure_form.ts @@ -124,6 +124,10 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = { return slot; }; + /** 관경 빈칸 = 빈 보기 그대로 — 기본값 1000 을 몰래 채우지 않음(505-4 · 295). */ + const blankKept = (c: SpecContainer, item: SpecItem): boolean => + spec?.typeId === "pipe" && c.id === "pipe" && item.id === "diameter"; + const control = ( c: SpecContainer, item: SpecItem, @@ -140,7 +144,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = { const blank = !tag ? "" : name ? `${name} (${tag})` : tag; // 필수 상자 = 빈 보기 없음(54-43) · 선택 상자 빈 보기 = 빈 글 select.replaceChildren( - ...(c.required && !tag ? [] : [new Option(blank, "")]), + ...(c.required && !tag && !blankKept(c, item) ? [] : [new Option(blank, "")]), ...choices.map((ch) => new Option(choiceLabel(c, item, ch, names), ch.value)), ); // 선택지 밖 옛 값은 그 값만 남겨 보임(지운 값을 몰래 바꾸지 않음) @@ -223,6 +227,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = { for (const item of c.items) { if (item.kind !== "select" || values[item.id] || (item.by && !branchOf(item, values))) continue; + if (blankKept(c, item)) continue; const choices = choicesOf(item, values); const key = branchOf(item, values)?.key; const wanted = String((key ? defaults[key] : undefined) ?? item.default ?? ""); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index 520474528..f3a802301 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -191,7 +191,8 @@ export async function mountRouteEditor( station_interval_m: stationIntervalM, station_snap: stationSnap, }), - onShown: () => draw(), // 횡단이 뜨면 그 측점 반폭 선을 그린다. + // 횡단이 뜨면 그 측점 반폭 선을 그린다 — 새로 고침 뒤 고른 측점은 만드는 도중 불려 아래 선언이 아직이라 한 박자 뒤에(505-4). + onShown: () => queueMicrotask(draw), }); /** 구간 재기 — 계획·예상 노선 위 두 점. 셈·서버 묻기는 `_Measure` 몫(계획서 0-9 ⑤). */ const measure = createMeasureTool({ diff --git a/resources/tester/test_505_4_refresh_blank_pipe.py b/resources/tester/test_505_4_refresh_blank_pipe.py new file mode 100644 index 000000000..7921a19da --- /dev/null +++ b/resources/tester/test_505_4_refresh_blank_pipe.py @@ -0,0 +1,102 @@ +"""PLAN 505-4 — 구조물 고른 채 새로 고침 = 평면 편집기 뜸 · 빈 관경 = 빈 선택지(값 안 채움). + +글 시험 = 소스 순서 · 화면 시험 = 자기 포트(5179) Playwright 머리 없는 브라우저 · 서버가 떠 있을 때만 · 검증_sub4. + + ./venv/Scripts/python.exe -m pytest resources/tester/test_505_4_refresh_blank_pipe.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) +LIST_URL = f"{BASE}/api/projects/{PROJECT}/route/structures" + + +def _read(*parts: str) -> str: + return ROOT.joinpath(*parts).read_text(encoding="utf-8") + + +def test_횡단_뜸_콜백은_만드는_도중_안_그림() -> None: + edit = _read("B05_Profile", "B05_Profile_UI_RouteEdit.ts") + # createCrossPreview 가 고른 측점을 만들며 곧바로 부름 — draw 는 아래 선언(closed · view)이 선 뒤에 + assert "onShown: () => queueMicrotask(draw)," in edit + assert "onShown: () => draw()" not in edit + assert edit.index("const crossPreview = createCrossPreview(") < edit.index( + "let closed = false;" + ) + + +def test_관경_빈칸은_빈_보기_채움_없음() -> None: + form = _read("A00_Common", "design", "design_structure_form.ts") + assert 'spec?.typeId === "pipe" && c.id === "pipe" && item.id === "diameter"' in form + assert "c.required && !tag && !blankKept(c, item) ? [] : [new Option(blank," in form + fill = form.index("필수 상자 빈 고르기 칸 = 정의 기본값") + assert form.index("if (blankKept(c, item)) continue;", fill) < form.index( + "values[item.id] =", fill + ) + + +def _up() -> bool: + try: + urllib.request.urlopen(BASE, timeout=3) + return True + except Exception: + return False + + +JS_DIAMETER = """() => [...document.querySelectorAll('select')] + .filter((s) => s.offsetParent && [...s.options].some((o) => o.value === '800')) + .map((s) => ({ value: s.value, first: s.options[0].value, count: s.options.length }))""" +JS_CANVAS = """() => { + const c = document.querySelector('.b05-routeedit__canvas'); + const d = c.getContext('2d').getImageData(0, 0, c.width, c.height).data; + let n = 0; + for (let i = 3; i < d.length; i += 4) if (d[i] > 0) n += 1; + return { visible: !!c.offsetParent, painted: n }; +}""" + + +@pytest.mark.skipif(not _up(), reason="5179 서버 없음") +def test_화면_새로_고침_편집기_뜸_빈_관경_빈_선택지() -> None: + 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) + try: + 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 + tab = ctx.new_page() + errors: list[str] = [] + tab.on("pageerror", lambda e: errors.append(str(e))) + 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) + before = ctx.request.get(LIST_URL).text() + # 관경 빈칸 관(19+8) — 빈 선택지 · 값 안 채움 + tab.locator(".b05-route__irregular-item", has_text="19+8").first.click() + tab.wait_for_timeout(3000) + if not tab.evaluate(JS_DIAMETER): + tab.get_by_text("구조물 배치", exact=True).click() + tab.wait_for_timeout(1500) + assert tab.evaluate(JS_DIAMETER) == [{"value": "", "first": "", "count": 5}] + assert ctx.request.get(LIST_URL).text() == before # 열기만 — 저장 값 그대로 + # 고른 채 새로 고침 — 편집기가 뜸(전엔 `closed` 선언 전 씀으로 멈춤) + errors.clear() + tab.reload(wait_until="domcontentloaded") + tab.wait_for_function("() => !!window.__aisloPipeline", timeout=90000) + tab.wait_for_timeout(9000) + assert not [e for e in errors if "before initialization" in e], errors + canvas = tab.evaluate(JS_CANVAS) + assert canvas["visible"] and canvas["painted"] > 0 + assert ctx.request.get(LIST_URL).text() == before + finally: + chrome.close()