"""PLAN 511-B B-4 (요구 320-4) — 계산 자리 cross = server 화면 길(검증_sub2 · 이 폴더 서버 · Playwright). 못박는 것 - 판 both 와 server(판 응답만 바꿈 · 판 파일 안 씀)에서 페이지 묶음 cross 출력 · 저장 상세 설계(구조물 폐회로 면적 층까지)가 0 차 — 저장 노선(`cross.refresh` · 다시 셈 창구) · 노선 초안 둔 채 다시 열기(`stage.cross`). - server 일 때만 그 id 를 서버에 묻고 · both 면 /calc 0. - 왕복 시간(`__aisloCalcServer.calls`)을 남김(PLAN 시간 줄). 저장 안 함 · 세션만(노선 초안은 창과 함께 버림). ./venv/Scripts/python.exe -m pytest resources/tester/test_511_b4_cross_server_browser.py -q """ from __future__ import annotations import json import sys from pathlib import Path import pytest HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] for path in (ROOT, HERE): if str(path) not in sys.path: sys.path.insert(0, str(path)) from test_511_b1_calc_node_stages import BASE, JS_DRAFT, PROJECT, _diff, screen # noqa: E402 pytestmark = pytest.mark.skipif( not (ROOT / "config" / "node_modules" / "vite").is_dir(), reason="프론트엔드 의존성 미설치" ) OUT = ROOT / "tmp" / "511b4_capture.json" # 묶음 cross 출력 · 저장 상세 설계(면적 · 구조물 층) · 서버 부름 기록 JS_STATE = r"""async () => { const urls = performance.getEntriesByType('resource').map((e) => e.name); const mod = async (re) => { const u = urls.find((n) => re.test(n)); if (!u) throw new Error('모듈 없음 ' + re); return import(u); }; const page = await mod(/design_pipeline_page\.ts/); const pid = localStorage.getItem('frd_current_project_id'); const pipe = page.pagePipeline(pid); await page.reconcilePagePipeline(pid); await pipe.settled(); const data = await mod(/design_page_data\.ts/); const routeId = data.pageRouteId(pid); const detail = (await mod(/B06_Section_Section_Canon\.ts/)).savedSectionDetail(pid, routeId); const shown = page.pipelineDetail(pid) ?? detail; const calls = (window.__aisloCalcServer?.calls ?? []).map((c) => ({ id: c.id, roundMs: c.roundMs, nodeMs: c.nodeMs, wallMs: c.wallMs, bytes: c.bytes, })); return JSON.parse(JSON.stringify({ cross: pipe.output('cross'), designs: (shown?.cross_sections ?? []).map((s) => ({ chainage_m: s.chainage_m, design: s.design, samples: (s.samples ?? []).length, })), calls, runs: (window.__aisloPipeline?.runs ?? []).slice(-1).map((r) => r.outcome), })); }""" def _open(play, cred: dict, cross: str) -> tuple: chrome = play.chromium.launch(channel="msedge", headless=True) context = chrome.new_context(viewport={"width": 1600, "height": 1000}) login = {k: cred[k] for k in ("email", "password")} assert context.request.post(f"{BASE}/api/auth/login/request", data=login, timeout=60000).ok def board(route): # 판 응답만 바꿈(판 파일 안 씀) response = route.fetch() body = response.json() body["sites"]["cross"] = cross route.fulfill(response=response, json=body) context.route("**/api/dashboard/calc-sites", board) page = context.new_page() page.add_init_script( f"localStorage.setItem('frd_current_project_id', '{PROJECT}');" " performance.setResourceTimingBufferSize(20000);" ) return chrome, page def _shot(page) -> dict: page.goto(f"{BASE}/#/b05-profile", wait_until="domcontentloaded") page.wait_for_function("() => !!window.__aisloPipeline", timeout=90000) page.wait_for_timeout(9000) return page.evaluate(JS_STATE) @pytest.fixture(scope="module") def shots(): sync = pytest.importorskip("playwright.sync_api") cred = json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8")) out = {} with sync.sync_playwright() as play: for cross in ("both", "server"): chrome, page = _open(play, cred, cross) try: out[f"saved_{cross}"] = _shot(page) page.evaluate(JS_DRAFT) # 노선 초안(가운데 교각점 옮김 · 세션만) page.wait_for_timeout(3000) page.reload(wait_until="domcontentloaded") page.wait_for_function("() => !!window.__aisloPipeline", timeout=90000) page.wait_for_timeout(9000) out[f"draft_{cross}"] = page.evaluate(JS_STATE) finally: chrome.close() # 세션 초안은 창과 함께 버림 OUT.write_text(json.dumps(out, ensure_ascii=False), encoding="utf-8") return out @screen @pytest.mark.parametrize("case", ["saved", "draft"]) def test_cross_server_화면_값_브라우저와_0차(shots, case): both, server = shots[f"{case}_both"], shots[f"{case}_server"] assert both["runs"] == ["done"] and server["runs"] == ["done"], (both["runs"], server["runs"]) assert both["cross"]["byKey"], "cross 출력 없음" hit = _diff(server["cross"], both["cross"], "cross") assert hit is None, hit hit = _diff(server["designs"], both["designs"], "designs") assert hit is None, hit # 부름 — both = /calc 없음 · server = 그 길 id assert not [c for c in both["calls"] if "cross" in c["id"]] want = "cross.refresh" if case == "saved" else "stage.cross" assert any(c["id"] == want for c in server["calls"]), server["calls"]