Files
Aislo/resources/tester/test_225_drawer_shortcuts.py
T
eomsangdonandClaude Opus 5.5 26fcebeb46 feat(설정): 334-2 ☰ 시스템 설정 4 페이지 바로가기 — 🏠 대시보드 + 카드 · 지금 칸 강조 · M02 → M02 이동은 떠나기 물음 먼저
- 새 A00_Common/m_settings_nav: settingLinks = 대시보드 카드 역할 규칙 한 벌 · mountSettingsDrawer = ☰ 진행 단계 자리
- B01 시스템 설정 카드가 settingLinks 로만 그림(화면 그대로)
- M01 · M02 page 한 줄씩(M02 = 진입 open · company 이어 실음)
- test_b01_settings_cards 가 옮긴 함수 소스를 같이 실음(기대값 그대로) · test_225 뒷부분

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
2026-10-15 15:53:07 +09:00

273 lines
12 KiB
Python

"""PLAN 334 — ☰ 바로가기(요구 225-1 · 225-2).
① Node — 진행 단계 「시스템 관리자 전용」 = 전처리 길 기준(칸 번호 아님) · 대시보드 둘째 칸은 안 꺼짐
settingLinks 역할 셋 · 설정 ☰ 지금 칸(진입 인자)
② 실제 브라우저(sub2 화면 서버 5177 · 꺼지면 건너뜀) — 대시보드 ☰ 이름 = 구역 제목 차례(역할 셋) ·
「보안」 누름 → 서랍 닫힘 · 보안 위 끝 ≈ ☰ 자리(52) · M01 ☰ 다섯 · 강조 · 「도면 양식」 → open=도면 ·
M02 → M02 이동은 떠나기 물음 먼저(가드 자리에 물음 창을 심어 확인 · 취소 = 머묾)
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import urllib.request
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
BASE = os.environ.get("AISLO_UI_BASE", "http://localhost:5177") # sub2 화면 서버(OWNERS 자기 줄)
LAYOUT = ROOT / "ui_template" / "ui_template_workflow_layout.ts"
NAV = ROOT / "A00_Common" / "m_settings_nav.ts"
STEPS = ["파일입력", "전처리", "종단설계"]
WF_ROUTES = ["b03-file-input", "b04-preprocess", "b05-profile"]
def _fn(src: str, name: str) -> str:
found = re.search(rf"^(?:export )?function {name}\([\s\S]*?^}}", src, re.M)
assert found, f"{name} 를 못 찾음"
return found.group(0).removeprefix("export ")
def _node(script: str, *args: str):
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script, *args],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
FAKE_DOM = (
"const mk = (): any => { const cls = new Set<string>(); return {"
" children: [] as any[], textContent: '', title: '', disabled: false, on: null as any,"
" set className(v: string) { cls.clear(); v.split(' ').filter(Boolean).forEach((c) => cls.add(c)); },"
" get className() { return [...cls].join(' '); },"
" classList: { add: (...c: string[]) => c.forEach((x) => cls.add(x)),"
" remove: (c: string) => cls.delete(c), contains: (c: string) => cls.has(c),"
" toggle: (c: string, on: boolean) => (on ? cls.add(c) : cls.delete(c)) },"
" setAttribute() {}, append(...c: any[]) { this.children.push(...c); },"
" addEventListener(_: string, f: any) { this.on = f; } }; };\n"
"const document = { createElement: () => mk() };\n"
)
def _step_bar(role: str, routes: list[str]) -> list[bool]:
"""역할 · 길 목록으로 그린 단계 단추의 꺼짐 — 역할 물음이 끝난 뒤"""
src = _fn(LAYOUT.read_text(encoding="utf-8"), "createStepBar")
script = (
FAKE_DOM
+ "const ROUTES = { B01_ACCOUNT: 'b01-account', B04_PREPROCESS: 'b04-preprocess' };\n"
"const t = (k: string) => k;\nconst navigateTo = () => {};\n"
"const getDashboardRole = async () => process.argv[1];\n"
+ src.replace("import.meta.env.DEV", "false")
+ "\nconst bar = createStepBar(JSON.parse(process.argv[2]), -1,"
" { routes: JSON.parse(process.argv[3]) });"
"\nawait new Promise((r) => setTimeout(r, 0));"
"\nconsole.log(JSON.stringify(bar.children.map((b: any) => b.disabled)));"
)
return _node(script, role, json.dumps(STEPS), json.dumps(routes))
def test_전처리_잠금은_길_기준():
# 프로젝트 진행 단계 — 그대로(전처리만 시스템 관리자 전용)
assert _step_bar("ADMIN", WF_ROUTES) == [False, True, False]
assert _step_bar("USER", WF_ROUTES) == [False, True, False]
assert _step_bar("SYSTEM_ADMIN", WF_ROUTES) == [False, False, False]
# 대시보드 구역 바로가기 — 둘째 칸이 회사 관리자 · 일반에서 안 꺼짐
titles = ["시스템 설정", "프로젝트", "임시 보관함"]
assert _step_bar("ADMIN", titles) == [False, False, False]
assert _step_bar("USER", titles) == [False, False, False]
def _settings(user: dict, active: str, company) -> dict:
"""settingLinks 목록 + ☰ 에 맡긴 단계 막대(이름 · 지금 칸 · 아이콘 · 🏠)"""
src = NAV.read_text(encoding="utf-8")
icons = re.search(r"^const ICONS.*$", src, re.M).group(0)
script = (
"const ROUTES = { M01_MASTER_DATA: 'm01-master-data',"
" M02_MASTER_TEMPLATE: 'm02-master-template' };\n"
"const L = (k: string) => k;\nlet bar: any = null;\n"
"const createStepBar = (titles: string[], active: number, o: any) =>"
" ({ titles, active, icons: o.icons, home: o.homeButton, routes: o.routes });\n"
"const setDrawerSteps = (b: any) => { bar = b; };\nconst go = () => {};\n"
+ icons
+ "\n"
+ _fn(src, "settingLinks")
+ "\n"
+ _fn(src, "mountSettingsDrawer")
+ "\nconst [user, active, company] = JSON.parse(process.argv[1]);"
"\nmountSettingsDrawer(user, active, company);"
"\nconsole.log(JSON.stringify({ links: settingLinks(user, company).map((l) =>"
" [l.key, l.route, l.query ?? null]), bar }));"
)
return _node(script, json.dumps([user, active, company]))
M02 = "m02-master-template"
def test_설정_바로가기_역할_셋_지금_칸():
admin = _settings({"role": "SYSTEM_ADMIN", "company_id": 1}, "옵션", "3")
assert admin["links"] == [
["m01", "m01-master-data", None],
["구조물조합", M02, {"open": "구조물조합", "root": "personal", "company": "3"}],
["옵션", M02, {"open": "옵션", "root": "master", "company": "3"}],
["도면", M02, {"open": "도면", "root": "master", "company": "3"}],
]
bar = admin["bar"]
assert bar["active"] == 2 and bar["home"] is True
assert bar["icons"] == ["🗄️", "🧱", "🎛️", "📑"]
assert _settings({"role": "SYSTEM_ADMIN"}, "m01", None)["bar"]["active"] == 0
# 일위대가조합 = 강조 없음
assert _settings({"role": "SYSTEM_ADMIN"}, "일위대가조합", None)["bar"]["active"] == -1
# 그 밖 역할 = 🏠 + 구조물 조합 하나(회사 인자 안 실음)
company = _settings({"role": "ADMIN", "company_id": 1}, "구조물조합", "3")
assert company["links"] == [["구조물조합", M02, {"open": "구조물조합", "root": "personal"}]]
assert company["bar"]["active"] == 0
assert _settings({"role": "USER", "is_master": True}, "구조물조합", None)["links"][0][2] == {
"open": "구조물조합",
"root": "company",
}
assert _settings({"role": "USER"}, "구조물조합", None)["links"][0][2]["root"] == "personal"
# ── 실제 브라우저 ─────────────────────────────────────────────
def _up() -> bool:
try:
urllib.request.urlopen(BASE, timeout=3)
return True
except Exception:
return False
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")}
def _page(browser, who: str, route: str, size=(1280, 700)):
ctx = browser.new_context(viewport={"width": size[0], "height": size[1]})
r = ctx.request.post(f"{BASE}/api/auth/login/request", data=_cred(who), timeout=60000)
assert r.ok, f"로그인 실패 {r.status}" # 비밀번호는 안 찍음
page = ctx.new_page()
page.goto(f"{BASE}/#/{route}", wait_until="domcontentloaded")
return page
DRAWER_LABELS = (
"[...document.querySelectorAll('.app-drawer__steps .ui-workflow-layout__step-label')]"
".map((e) => e.textContent.trim())"
)
DRAWER_DISABLED = (
"[...document.querySelectorAll('.app-drawer__steps .ui-workflow-layout__step')]"
".map((e) => e.disabled)"
)
SECTION_TITLES = (
"[...document.querySelector('.b01-dashboard__grid').children]"
".map((s) => s.querySelector('.ui-card__title').textContent.trim())"
)
DRAWER_OPEN = "!!document.querySelector('.is-drawer-open')"
def _open_drawer(page):
page.click(".app-burger")
page.wait_for_selector(".app-drawer__steps .ui-workflow-layout__step")
@pytest.mark.skipif(not _up(), reason="sub2 화면 서버(5177) 꺼짐")
@pytest.mark.parametrize("who, count", [("시스템 관리자", 10), ("회사관리자", 8), ("일반", 6)])
def test_대시보드_구역_바로가기(who, count):
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(channel="msedge", headless=True)
page = _page(browser, who, "b01-account")
page.wait_for_selector(".b01-dashboard__grid", timeout=60000)
page.wait_for_load_state("networkidle")
titles = page.evaluate(SECTION_TITLES)
assert len(titles) == count, titles
_open_drawer(page)
assert page.evaluate(DRAWER_LABELS) == titles
assert page.evaluate(DRAWER_DISABLED) == [False] * count # 둘째 칸 안 꺼짐
if who == "시스템 관리자":
page.click(".app-drawer__steps .ui-workflow-layout__step:has-text('보안')")
assert not page.evaluate(DRAWER_OPEN)
top = (
"[...document.querySelector('.b01-dashboard__grid').children]"
".find((s) => s.querySelector('.ui-card__title').textContent.trim() === '보안')"
".getBoundingClientRect().top"
)
try: # 부드러운 굴림이 멎을 때까지
page.wait_for_function(f"() => Math.abs({top} - 52) <= 2", timeout=5000)
except Exception:
pass
assert abs(page.evaluate(top) - 52) <= 2, page.evaluate(top)
browser.close()
ACTIVE = "document.querySelector('.app-drawer__steps .is-active .ui-workflow-layout__step-label')?.textContent"
HASH = "decodeURIComponent(location.hash)"
def _drawer_click(page, label: str):
_open_drawer(page)
page.click(f".app-drawer__steps .ui-workflow-layout__step:has-text('{label}')")
@pytest.mark.skipif(not _up(), reason="sub2 화면 서버(5177) 꺼짐")
def test_설정_4_페이지_바로가기():
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(channel="msedge", headless=True)
page = _page(browser, "시스템 관리자", "m01-master-data")
page.wait_for_selector(".m01-master", timeout=60000)
_open_drawer(page)
assert page.evaluate(DRAWER_LABELS) == [
"대시보드",
"마스터 데이터",
"구조물 조합",
"구조물 옵션",
"도면 양식",
]
assert page.evaluate(ACTIVE) == "마스터 데이터"
page.click(".app-drawer__steps .ui-workflow-layout__step:has-text('도면 양식')")
page.wait_for_function("decodeURIComponent(location.hash).includes('open=도면')")
assert M02 in page.evaluate(HASH)
page.wait_for_selector(".m02-master", timeout=60000)
page.wait_for_load_state("networkidle")
_open_drawer(page)
assert page.evaluate(ACTIVE) == "도면 양식"
page.click(".app-burger") # 닫음
# M02 → M02 — 저장 안 한 고침 대신 가드 자리에 앱의 물음 창을 심음
page.evaluate(
"""async () => {
const url = (p) => performance.getEntriesByType('resource')
.map((e) => e.name).find((n) => n.includes(p));
const router = await import(url('/router.ts'));
const ui = await import(url('/ui_template_elements.ts'));
router.setLeaveGuard(() => ui.showConfirmDialog('저장 안 한 고침'));
}"""
)
before = page.evaluate(HASH)
_drawer_click(page, "구조물 옵션")
page.wait_for_selector(".ui-confirm")
page.click(".ui-confirm__actions button >> nth=0") # 취소 = 머묾
page.wait_for_timeout(300)
assert page.evaluate(HASH) == before
_drawer_click(page, "구조물 옵션")
page.wait_for_selector(".ui-confirm")
page.click(".ui-confirm__actions button >> nth=1") # 버림 = 이동
page.wait_for_function("decodeURIComponent(location.hash).includes('open=옵션')")
browser.close()