From 38e75b85ca584ee087e23401414f066ce6e93106 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 21 Oct 2026 12:57:22 +0900 Subject: [PATCH] =?UTF-8?q?test(506-3):=20B07=20=C2=B7=20B09=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EA=B2=80=EC=A6=9D=20=EC=8B=9C=ED=97=98=20=E2=80=94?= =?UTF-8?q?=205177=20Playwright=20=C2=B7=20AISLO=5FLIVE=5FDB=3D1=20?= =?UTF-8?q?=EC=9D=BC=20=EB=95=8C=EB=A7=8C=20=C2=B7=20=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=ED=96=89=20=C2=B7=20storage=20=EB=96=A0=20=EB=91=90=EA=B3=A0?= =?UTF-8?q?=20=EB=90=98=EB=8F=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- resources/tester/test_506_3_b07_browser.py | 266 +++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 resources/tester/test_506_3_b07_browser.py diff --git a/resources/tester/test_506_3_b07_browser.py b/resources/tester/test_506_3_b07_browser.py new file mode 100644 index 000000000..667c53788 --- /dev/null +++ b/resources/tester/test_506_3_b07_browser.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +"""506-3 화면 — 자기 포트(5177) Playwright 머리 없는 브라우저 · 검증_sub2 · `AISLO_LIVE_DB=1` 일 때만. + +- B07 표지에 선 하나 → CAD 자동백업 열쇠 = `OPEN_WEB_CAD__RECOVERY__b07:{프로젝트}:cover` +- 특성 판 색 칸 다섯 번 끌기 → 되돌리기 한 번 = 처음 색 +- 미리받기가 도는 중 [저장] → 「저장했습니다」 · 머묾 글 없음 · 저장 동안 새 미리받기 GET 없음 +- [초기화] 물음 글 = 프로젝트 도각 안내(밝음 · 어두움 한 장씩 · tmp/shots_506_3) → [취소] +- B09 [인쇄 미리보기] = PUT /estimation 뒤 #/b09-print · complete 안 부름 +- 단계 행 · B07 · B09 storage 를 먼저 떠 두고 끝에 되돌림 + + AISLO_LIVE_DB=1 ./venv/Scripts/python.exe -m pytest resources/tester/test_506_3_b07_browser.py -q -s +""" + +from __future__ import annotations + +import asyncio +import json +import os +import shutil +import sys +import time +import urllib.request +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) +BASE = "http://localhost:5177" +PROJECT = "cb15892a-5788-4a26-9316-20a15421f68b" # 검증_sub2(OWNERS.md) +SHOTS = ROOT / "tmp" / "shots_506_3" +COLS = ( + "state", + "progress_percent", + "params", + "message", + "started_at", + "completed_at", + "updated_at", +) + + +def _up() -> bool: + try: + urllib.request.urlopen(BASE, timeout=3) + return True + except Exception: + return False + + +pytestmark = pytest.mark.skipif( + os.environ.get("AISLO_LIVE_DB") != "1" or not _up(), + reason="AISLO_LIVE_DB=1 · 5177 서버 있을 때만(검증_sub2 storage · 단계 행을 씀)", +) + + +async def _keep(mode: str, held: dict) -> None: + """단계 행 · projects 상태 · B07 · B09 storage 떠 두기(save) · 되돌리기(restore).""" + import aiomysql + + from common_util.common_util_storage import resolve_stored_project_path + from config.config_db import DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USER + + conn = await aiomysql.connect( + host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASSWORD, db=DB_NAME, + charset="utf8mb4", autocommit=False, + ) # fmt: skip + try: + async with conn.cursor(aiomysql.DictCursor) as cur: + if mode == "save": + await cur.execute("SELECT storage_path FROM projects WHERE id=%s", (PROJECT,)) + root = Path(resolve_stored_project_path((await cur.fetchone())["storage_path"])) + await cur.execute( + "SELECT * FROM project_workflow_stages WHERE project_id=%s", (PROJECT,) + ) + held["stages"] = await cur.fetchall() + await cur.execute("SELECT status, updated_at FROM projects WHERE id=%s", (PROJECT,)) + held["project"] = await cur.fetchone() + held["copy"] = ROOT / "tmp" / f"pt_506_3_keep_{int(time.time())}" + held["dirs"] = [ + d for d in root.iterdir() if d.is_dir() and d.name.startswith(("B07", "B09")) + ] + for d in held["dirs"]: + shutil.copytree(d, held["copy"] / d.name) + return + for row in held["stages"]: + await cur.execute( + f"UPDATE project_workflow_stages SET {', '.join(f'{k}=%s' for k in COLS)} " + "WHERE id=%s", + (*[row[k] for k in COLS], row["id"]), + ) + project = held["project"] + await cur.execute( + "UPDATE projects SET status=%s, updated_at=%s WHERE id=%s", + (project["status"], project["updated_at"], PROJECT), + ) + await conn.commit() + for d in held["dirs"]: + shutil.rmtree(d) + shutil.copytree(held["copy"] / d.name, d) + finally: + conn.close() + + +@pytest.fixture(scope="module") +def ctx(): + sync = pytest.importorskip("playwright.sync_api") + cred = json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8")) + held: dict = {} + asyncio.run(_keep("save", held)) + try: + with sync.sync_playwright() as play: + chrome = play.chromium.launch(channel="msedge", headless=True) + context = chrome.new_context(viewport={"width": 1600, "height": 950}) + 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}" # 비밀번호는 안 찍음 + context.add_init_script(f"localStorage.setItem('frd_current_project_id', '{PROJECT}')") + yield context + chrome.close() + finally: + asyncio.run(_keep("restore", held)) + + +def _cad(page): + return next(f for f in page.frames if "/b07-cad/" in f.url) + + +def _type(page, *lines: str) -> None: + for line in lines: + page.keyboard.type(line) + page.keyboard.press("Enter") + + +def test_b07_저장_길_화면(ctx): + page = ctx.new_page() + gets: list[tuple[float, str]] = [] + puts: list[tuple[float, float]] = [] + page.on( + "request", + lambda q: ( + q.method == "GET" + and "/design-drawings/" in q.url + and gets.append((time.time(), q.url.rsplit("/", 1)[-1])) + ), + ) + starts: dict = {} + page.on( + "request", + lambda q: q.method == "PUT" and "/confirm" in q.url and starts.update(t=time.time()), + ) + page.on( + "requestfinished", + lambda q: ( + q.method == "PUT" and "/confirm" in q.url and puts.append((starts["t"], time.time())) + ), + ) + page.goto(f"{BASE}/#/b07-design-detail", wait_until="domcontentloaded") + page.wait_for_selector( + '.b07-drawing-button[data-drawing-id="cover"][data-active="true"]', timeout=90000 + ) + page.wait_for_selector(".cad-host[data-loading='false']", timeout=90000) + cad = _cad(page) + + # 선 하나 — 표지 미저장 · 자동백업 + cad.locator("canvas").first.click(position={"x": 900, "y": 600}) + page.keyboard.press("Escape") + _type(page, "l", "100,100", "400,100") + page.keyboard.press("Escape") + page.keyboard.press("Control+a") + cad.wait_for_timeout(300) + + # 특성 판 색 다섯 번 끌기 → 되돌리기 한 번 + if cad.locator(".cad-inspector[data-collapsed='true']").count(): + cad.locator(".cad-inspector__collapse").click() + cad.locator(".cad-inspector-tabs button", has_text="특성").click() + color = cad.locator(".cad-properties-editor input[type=color]") + color.wait_for(timeout=10000) + first = color.input_value() + for value in ("#110000", "#220000", "#330000", "#440000", "#550000"): + color.evaluate( + "(el, v) => { Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')" + ".set.call(el, v); el.dispatchEvent(new Event('input', { bubbles: true })); }", + value, + ) + cad.wait_for_timeout(60) + painted = color.input_value() + cad.wait_for_timeout(600) # 멎음 → 한 칸 + cad.locator("canvas").first.click(position={"x": 600, "y": 60}) # 초점 = 캔버스(고름은 다시) + page.keyboard.press("Control+z") + page.keyboard.press("Control+a") + cad.wait_for_timeout(300) + undone = color.input_value() + print(f"색 처음 {first} · 끈 뒤 {painted} · 되돌리기 한 번 {undone}") + assert painted == "#550000" and undone == first + + # 자동백업 열쇠(적재 뒤 5 s 쉼 + 5 s 미룸) + cad.wait_for_timeout(11000) + keys = cad.evaluate( + "Object.keys(localStorage).filter(k => k.startsWith('OPEN_WEB_CAD__RECOVERY'))" + ) + print("자동백업 열쇠", keys) + assert f"OPEN_WEB_CAD__RECOVERY__b07:{PROJECT}:cover" in keys + assert "OPEN_WEB_CAD__RECOVERY__cover" not in keys + + # 미리받기 도는 중 [저장] + before = len(gets) + page.get_by_role("button", name="저장", exact=True).click() + toast = page.wait_for_selector( + ".ui-toast:has-text('저장했습니다'), .ui-toast:has-text('머묾')", timeout=90000 + ).inner_text() + page.wait_for_function( + "() => document.querySelector('.b07-drawing-button[data-drawing-id=cover]')?.dataset.confirmed === 'true'", + timeout=90000, + ) + put_start, put_end = puts[-1] + during = [name for t, name in gets if put_start < t < put_end] + print( + f"[저장] 앞 미리받기 GET {before} · PUT {put_end - put_start:.2f} s · 알림 「{toast}」 · 저장 중 새 GET {during}" + ) + assert before > 1 # 미리받기 도는 중 + assert "머묾" not in toast and "저장했습니다" in toast + assert during == [] + + # [초기화] 물음 글 → [취소] + page.get_by_role("button", name="초기화", exact=True).click() + message = page.wait_for_selector(".ui-confirm__message", timeout=10000) + text = message.inner_text() + print("초기화 물음", text) + assert "프로젝트 도각은 그대로" in text and "[기본 도각 복원]" in text + SHOTS.mkdir(parents=True, exist_ok=True) + panel = page.locator(".ui-confirm__panel") + panel.screenshot(path=str(SHOTS / "reset_light.png")) + page.evaluate("document.documentElement.setAttribute('data-theme', 'dark')") + panel.screenshot(path=str(SHOTS / "reset_dark.png")) + page.evaluate("document.documentElement.setAttribute('data-theme', 'light')") + page.locator(".ui-confirm__actions button", has_text="취소").click() + page.close() + + +def test_b09_인쇄_미리보기_저장_뒤_이동(ctx): + page = ctx.new_page() + calls: list[str] = [] + page.on( + "request", + lambda q: ( + "/estimation" in q.url + and q.method != "GET" + and calls.append(f"{q.method} {q.url.split('/estimation')[-1] or '/'}") + ), + ) + page.goto(f"{BASE}/#/b09-estimation", wait_until="domcontentloaded") + button = page.wait_for_selector("[data-b09-print]", timeout=90000) + page.wait_for_timeout(6000) # 원가 자료 받기 + user = ctx.request.get(f"{BASE}/api/projects/{PROJECT}/estimation").json().get("user") or {} + # 고친 값 있음 표 — 지금 서버 값 그대로(PUT 이 같은 값을 씀) + page.evaluate( + "([k, v]) => sessionStorage.setItem(k, v)", + [f"aislo:draft:b09-estimation:{PROJECT}", json.dumps(user, ensure_ascii=False)], + ) + button.click() + page.wait_for_url("**/#/b09-print", timeout=60000) + print("B09 [인쇄 미리보기] 요청", calls) + assert calls[:1] == ["PUT /"] + assert not any("complete" in call for call in calls) + page.close()