# -*- 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()