From f5fb507a331c28e93a15a6982ce648aa36dd764f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 12:30:59 +0900 Subject: [PATCH] =?UTF-8?q?fix(504-2=20=EA=B3=84=EC=82=B0=20=EC=9E=90?= =?UTF-8?q?=EB=A6=AC=20=EC=B0=BD):=20=EB=AC=B6=EC=9D=8C=20=C2=B7=20?= =?UTF-8?q?=EA=B9=8C=EB=8B=AD=20=EA=B8=80=20=EC=A4=84=EB=B0=94=EA=BF=88=20?= =?UTF-8?q?=C2=B7=20=EC=9C=84=20=EB=91=90=20=EC=A4=84=20=ED=8B=88=20?= =?UTF-8?q?=EC=A4=84=EC=9E=84=20=C2=B7=20=ED=99=94=EB=A9=B4=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98(5177)=20=EC=83=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면 시험 test_309_2_calc_sites_browser — 시스템 관리자만 카드 · ADMIN · USER 403 · G1 끔 뒤 B05 지반 서버 요청 0 → 4 · 판 기본값으로 되돌림 · 밝음 · 어두움 창 사진 Co-Authored-By: Claude Opus 5.5 --- B01_Dashboard/B01_Dashboard_UI_Style.css | 13 ++ .../tester/test_309_2_calc_sites_browser.py | 151 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 resources/tester/test_309_2_calc_sites_browser.py diff --git a/B01_Dashboard/B01_Dashboard_UI_Style.css b/B01_Dashboard/B01_Dashboard_UI_Style.css index 114a72f0e..2e658221f 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Style.css +++ b/B01_Dashboard/B01_Dashboard_UI_Style.css @@ -787,3 +787,16 @@ outline: 2px solid var(--color-royal-amethyst, #6843a7); outline-offset: 2px; } + +/* 계산 자리 창(504-2) — 묶음 · 까닭 글은 줄바꿈 · 체크 칸은 가운데 */ +.b01-dashboard__calc-sites .b01-dashboard__table td { + white-space: normal; +} +.b01-dashboard__calc-sites .b01-dashboard__table td:nth-child(2), +.b01-dashboard__calc-sites .b01-dashboard__table td:nth-child(3) { + text-align: center; +} +.b01-dashboard__calc-count, +.b01-dashboard__calc-note { + margin: 0 0 0.5rem; +} diff --git a/resources/tester/test_309_2_calc_sites_browser.py b/resources/tester/test_309_2_calc_sites_browser.py new file mode 100644 index 000000000..e82b392e6 --- /dev/null +++ b/resources/tester/test_309_2_calc_sites_browser.py @@ -0,0 +1,151 @@ +"""PLAN 504-2 화면 — 자기 포트(5177) Playwright 머리 없는 브라우저 · 서버가 떠 있을 때만. + +- 시스템 관리자 = 「계산 자리」 카드 · 창(밝음 · 어두움 한 장씩 · tmp/shots_504_2) +- 회사 관리자 · 일반 = 카드 없음 · PUT 403 +- G1 체크 끔 → [저장] → B05 열기(검증_sub2) → 지반 표본이 서버 길(`/route/elevations`) · 판 기본값으로 되돌림 + + ./venv/Scripts/python.exe -m pytest resources/tester/test_309_2_calc_sites_browser.py -q +""" + +from __future__ import annotations + +import json +import subprocess +import urllib.request +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +BASE = "http://localhost:5177" +BOARD = ROOT / "resources" / "master_template" / "calc_sites.json" +PROJECT = "cb15892a-5788-4a26-9316-20a15421f68b" # 검증_sub2(OWNERS.md) +SHOTS = ROOT / "tmp" / "shots_504_2" +CARD = "계산 자리" + + +def _up() -> bool: + try: + urllib.request.urlopen(BASE, timeout=3) + return True + except Exception: + return False + + +pytestmark = pytest.mark.skipif(not _up(), reason="5177 서버 없음") + + +def _cred(who: str) -> dict: + if who == "시스템 관리자": + return json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8")) + accounts = json.loads((ROOT / "tmp" / "verify_accounts.json").read_text(encoding="utf-8")) + return {k: accounts[who][k] for k in ("email", "password")} + + +@pytest.fixture(scope="module") +def browser(): + sync = pytest.importorskip("playwright.sync_api") + committed = subprocess.run( + ["git", "show", "HEAD:resources/master_template/calc_sites.json"], + cwd=ROOT, capture_output=True, check=True, + ).stdout # fmt: skip + with sync.sync_playwright() as play: + chrome = play.chromium.launch(channel="msedge", headless=True) + yield chrome + chrome.close() + BOARD.write_bytes(committed) # 판 = 커밋한 기본값 그대로 + + +def _context(browser, who: str): + ctx = browser.new_context(viewport={"width": 1400, "height": 900}) + r = ctx.request.post(f"{BASE}/api/auth/login/request", data=_cred(who), timeout=60000) + assert r.ok, f"로그인 실패 {r.status}" # 비밀번호는 안 찍음 + return ctx + + +def _dashboard(ctx): + page = ctx.new_page() + page.goto(f"{BASE}/#/b01-account", wait_until="domcontentloaded") + page.wait_for_selector(".b01-dashboard__setting-card", timeout=60000) + return page + + +def _cards(page) -> list[str]: + return page.eval_on_selector_all( + ".b01-dashboard__setting-title", "(els) => els.map((e) => e.textContent.trim())" + ) + + +@pytest.mark.parametrize("who", ["회사관리자", "일반"]) +def test_다른_역할_카드_없음_403(browser, who): + ctx = _context(browser, who) + try: + assert CARD not in _cards(_dashboard(ctx)) + put = ctx.request.put( + f"{BASE}/api/dashboard/admin/calc-sites", + data={"sites": {"ground_sample": "server"}}, + ) + assert put.status == 403 + assert ctx.request.get(f"{BASE}/api/dashboard/calc-sites").ok # 읽기 = 로그인 + finally: + ctx.close() + + +def _elevation_calls(ctx) -> int: + page = ctx.new_page() + # 대시보드가 지금 프로젝트 칸을 비움 — 페이지 스크립트보다 먼저 넣음 + page.add_init_script(f"localStorage.setItem('frd_current_project_id', '{PROJECT}')") + calls: list[str] = [] + page.on("request", lambda r: "/route/elevations" in r.url and calls.append(r.url)) + page.goto(f"{BASE}/#/b05-profile", wait_until="domcontentloaded") + page.wait_for_function("() => !!window.__aisloPipeline", timeout=90000) + page.wait_for_timeout(8000) + page.close() + return len(calls) + + +def test_시스템_관리자_카드_창_G1_서버_길(browser): + ctx = _context(browser, "시스템 관리자") + try: + before = _elevation_calls(ctx) # 기본값 = 격자(브라우저) + page = _dashboard(ctx) + assert CARD in _cards(page) + page.get_by_text(CARD, exact=True).click() + dialog = 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) => [...tr.querySelectorAll('input')]" + ".map((i) => [i.checked, i.disabled]))", + ) + assert len(rows) == 8 + assert rows[0] == [[True, False], [True, True]] # G1 바꿀 수 있음 · 서버 정본 잠금 + assert all(row[0][1] and row[1][1] for row in rows[2:]) # 나머지 잠금 + assert "지금 서버 0 줄" in dialog.inner_text() + SHOTS.mkdir(parents=True, exist_ok=True) + dialog.screenshot(path=str(SHOTS / "light.png")) + page.evaluate("document.documentElement.setAttribute('data-theme', 'dark')") + dialog.screenshot(path=str(SHOTS / "dark.png")) + page.evaluate("document.documentElement.setAttribute('data-theme', 'light')") + + page.click("input[data-calc-site='ground_sample']") + assert "지금 서버 1 줄" in dialog.inner_text() + page.locator(".b01-dashboard__calc-sites .ui-modal__actions button").click() + page.wait_for_selector(".b01-dashboard__calc-sites", state="detached", timeout=30000) + assert json.loads(BOARD.read_text("utf-8"))["sites"]["ground_sample"] == "server" + logs = ctx.request.get(f"{BASE}/api/dashboard/admin/audit-logs").json()["items"] + assert any( + log.get("action") == "CALC_SITE_CHANGE" + and log.get("resource_ref") == "ground_sample=server" + for log in logs + ) + + after = _elevation_calls(ctx) + print(f"elevations 요청: 기본값 {before} · 서버 {after}") + assert after > before + + back = ctx.request.put( + f"{BASE}/api/dashboard/admin/calc-sites", data={"sites": {"ground_sample": "both"}} + ) + assert back.ok and back.json()["sites"]["ground_sample"] == "both" + finally: + ctx.close()