From f16c25bb2ed0ae17dc6ff2eb1831ff5db757df60 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 22 Oct 2026 15:55:10 +0900 Subject: [PATCH] =?UTF-8?q?test(511-C=20=ED=99=94=EB=A9=B4=20=C2=B7=20?= =?UTF-8?q?=EB=91=98=EB=A0=88):=20=EA=B2=80=EC=A6=9D=5Fsub4=20B05=20?= =?UTF-8?q?=EC=9C=A0=ED=86=A0=20=EB=B0=B0=EB=B6=84=20=ED=8C=90=20server=20?= =?UTF-8?q?=EB=8C=80=20both=20=E2=80=94=20server=20=3D=20/haul-plan=201=20?= =?UTF-8?q?=C2=B7=20=EB=B0=B0=EB=B6=84=20=EB=AA=A8=EB=93=88=20=EC=95=88=20?= =?UTF-8?q?=EB=B0=9B=EC=9D=8C=20=C2=B7=20both=20=3D=20/haul-plan=200=20?= =?UTF-8?q?=C2=B7=20=EB=AA=A8=EB=93=88=20=EB=B0=9B=EC=9D=8C=20=C2=B7=20?= =?UTF-8?q?=EC=9C=A0=ED=86=A0=EA=B3=A1=EC=84=A0=20=EA=B7=B8=EB=A6=BC=20?= =?UTF-8?q?=EA=B0=99=EC=9D=8C=20=C2=B7=20=EA=B3=84=EC=82=B0=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=EC=B0=BD=20E2=20=EC=97=B4=EB=A6=BC(=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EA=BA=BC=EC=A7=90)=20=C2=B7=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EB=A7=8C=20G3=20D1=20D2=20N1=20=EA=B9=8C=EB=8B=AD=20=EB=84=B7?= =?UTF-8?q?=20=EB=8B=A4=EB=A6=84(test=5F511=5Fc=5Fhaul=5Fbrowser)=20=C2=B7?= =?UTF-8?q?=20511-B1=20=ED=8C=90=20=EC=97=B4=EB=A6=B0=20=EC=A4=84=EC=97=90?= =?UTF-8?q?=20haul=20=E2=80=94=20=EC=9A=94=EA=B5=AC=20320-4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../tester/test_511_b1_calc_node_stages.py | 1 + resources/tester/test_511_c_haul_browser.py | 157 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 resources/tester/test_511_c_haul_browser.py diff --git a/resources/tester/test_511_b1_calc_node_stages.py b/resources/tester/test_511_b1_calc_node_stages.py index ebe967fd2..142acd14f 100644 --- a/resources/tester/test_511_b1_calc_node_stages.py +++ b/resources/tester/test_511_b1_calc_node_stages.py @@ -94,6 +94,7 @@ def test_판_다섯_줄_잠금_기본값_그대로(): "route", # B-5 = 엶(511-B B-5) "stations", # B-2 끝 = 엶(511-B B-2) "profile", # B-3 엶(511-B · test_511_b3_profile_node) + "haul", # 511-C E2 엶(기본 server · 판 파일 바뀜 0) "fixed_browser", } committed = subprocess.run( # noqa: S603 — 고정 실행 파일 diff --git a/resources/tester/test_511_c_haul_browser.py b/resources/tester/test_511_c_haul_browser.py new file mode 100644 index 000000000..0c295546a --- /dev/null +++ b/resources/tester/test_511_c_haul_browser.py @@ -0,0 +1,157 @@ +"""PLAN 511-C 화면 — 자기 포트 Playwright 머리 없는 브라우저 · 서버가 떠 있을 때만 · 검증_sub4. + +- 판 haul 기본(server) = B05 계획고 ▲ 뒤 /haul-plan 요청 · 배분 모듈 안 받음(첫 묶음 밖) +- 판 haul both = /haul-plan 0 · 배분 모듈을 그때 받아 셈 · 유토곡선(배분 balloon 포함) 그림 + = server 판과 같음 +- 계산 자리 창 = E2 열린 줄(기본 꺼짐) · 서버만 G3 D1 D2 N1 줄마다 다른 까닭 +- 저장 안 함(세션 초안만) · 판 = 파일 바이트 되돌림 · 시험 브라우저 표식 시스템 로그 행만 지움 + + ./venv/Scripts/python.exe -m pytest resources/tester/test_511_c_haul_browser.py -q -s +""" + +from __future__ import annotations + +import asyncio +import json +import sys +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from helper_verify_project import own_ports # noqa: E402 +from test_511_a_calc_sites_browser import ( # noqa: E402, F401 + _UP, + JS_GRADE, # noqa: E402 + _started, +) + +API, BASE = own_ports() +BOARD = ROOT / "resources" / "master_template" / "calc_sites.json" +PROJECT = "c725d747-50ae-46b0-87ce-4d4e56a53cf3" # 검증_sub4(OWNERS.md) +MARK = "AisloTest-511-C" +CARD = "계산 자리" + +pytestmark = pytest.mark.skipif( + _UP is None or _UP != _started(API), reason="이 폴더 서버 없음 · vite 가 이 폴더 백엔드로 안 감" +) + +# 그림 글 — 그릴 때마다 새로 짓는 clipPath 이름만 한 글자로(값 아님) +JS_CHART = r"""() => { + const chart = document.querySelector('.b05-profile__masshaul-chart'); + return chart ? chart.innerHTML.replace(/mass-clip-[a-z0-9]+/g, 'mass-clip') : null; +}""" +JS_LOADED = r"""() => performance.getEntriesByType('resource').map((e) => e.name) + .some((n) => /common_util_mass_haul_balance\.ts/.test(n))""" + + +@pytest.fixture(scope="module") +def ctx(): + sync = pytest.importorskip("playwright.sync_api") + committed = BOARD.read_bytes() + cred = json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8")) + try: + with sync.sync_playwright() as play: + chrome = play.chromium.launch(channel="msedge", headless=True) + context = chrome.new_context(viewport={"width": 1600, "height": 1000}, user_agent=MARK) + login = {k: cred[k] for k in ("email", "password")} + r = context.request.post(f"{BASE}/api/auth/login/request", data=login, timeout=60000) + assert r.ok, f"로그인 실패 {r.status}" # 비밀번호는 안 찍음 + try: + yield context + finally: + chrome.close() + finally: + BOARD.write_bytes(committed) + _drop_test_logs() + + +def _drop_test_logs() -> None: + from config.config_db import close_db_pool, init_db_pool + + async def run(): + pool = await init_db_pool() + try: + async with pool.acquire() as connection, connection.cursor() as cursor: + await cursor.execute("DELETE FROM system_audit_logs WHERE user_agent = %s", (MARK,)) + await connection.commit() + finally: + await close_db_pool() + + asyncio.run(run()) + + +def _board(haul: str | None) -> None: + board = json.loads(BOARD.read_text("utf-8")) + board["sites"].pop("haul", None) + if haul: + board["sites"]["haul"] = haul + BOARD.write_text(json.dumps(board, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def _run(context) -> dict: + page = context.new_page() + page.add_init_script( + f"localStorage.setItem('frd_current_project_id', '{PROJECT}');" + " performance.setResourceTimingBufferSize(20000);" + ) + posts: list[dict] = [] + + def on_response(res): + if res.url.endswith("/haul-plan") and res.request.method == "POST": + posts.append(res.json()) + + page.on("response", on_response) + page.goto(f"{BASE}/#/b05-profile", wait_until="domcontentloaded") + page.wait_for_function("() => !!window.__aisloPipeline", timeout=90000) + page.wait_for_timeout(9000) + page.click(".ui-workflow-overlay__handle--bottom") # 종단 펼침 + page.wait_for_timeout(2000) + if page.evaluate(JS_CHART) is None: + page.click(".b05-profile__masshaul-handle") # 유토곡선 펼침 + page.evaluate(JS_GRADE) # 계획고 ▲ 한 번(가운데 측점 +0.3 m) — 새 누가토량 → 배분 다시 + page.wait_for_timeout(5000) + out = {"posts": posts, "chart": page.evaluate(JS_CHART), "loaded": page.evaluate(JS_LOADED)} + page.close() + return out + + +def test_haul_server_both_그림_같음(ctx): + _board(None) # 판 파일에 haul 없음 = 기본 server + server = _run(ctx) + _board("both") + both = _run(ctx) + _board(None) + assert server["posts"] and all(p["haul_plan"] for p in server["posts"]) + assert any(p["haul_plan"]["transfers"] for p in server["posts"]) + assert not server["loaded"] # 기본값 = 배분 모듈 안 받음 + assert both["posts"] == [] and both["loaded"] + assert server["chart"] and "balloon" in server["chart"] + assert both["chart"] == server["chart"] + print("posts", len(server["posts"]), "chart bytes", len(server["chart"])) + + +def test_계산_자리_창_haul_엶_서버만_까닭(ctx): + page = ctx.new_page() + page.goto(f"{BASE}/#/b01-account", wait_until="domcontentloaded") + page.wait_for_selector(".b01-dashboard__setting-card", timeout=60000) + page.get_by_text(CARD, exact=True).click() + page.wait_for_selector(".b01-dashboard__calc-sites", timeout=30000) + rows = page.eval_on_selector_all( + ".b01-dashboard__calc-sites tbody tr", + "(trs) => trs.map((tr) => ({ name: tr.cells[0].textContent.trim()," + " why: tr.cells[3].textContent.trim()," + " boxes: [...tr.querySelectorAll('input')].map((i) => [i.checked, i.disabled]) }))", + ) + page.close() + calcs = {row["name"].split(" · ")[0]: row for row in rows if " · " in row["name"]} + assert calcs["E2"]["boxes"] == [[False, False], [True, True]] # 열림 · 기본 server(꺼짐) + whys = {no: calcs[no]["why"] for no in ("G3", "D1", "D2", "N1")} + for no, word in {"G3": "DB", "D1": "래스터", "D2": "D1", "N1": "지형"}.items(): + assert word in whys[no] and calcs[no]["boxes"][0] == [False, True], (no, whys[no]) + assert len(set(whys.values())) == 4 + print(whys)