test(506-3): B07 · B09 화면 검증 시험 — 5177 Playwright · AISLO_LIVE_DB=1 일 때만 · 단계 행 · storage 떠 두고 되돌림
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user