Files
Aislo/resources/tester/test_b01_dashboard_tabs.py
T
eomsangdonandClaude Sonnet 5.5 2a2c4d0f62 feat(B01): 대시보드 항목별 탭 — 해시 탭 줄 · 개요 · 프로젝트 · 회사 · 내 계정 · 시스템 (92-1·92-2)
- 해시 ?tab= 탭 줄(B08 단추 꼴) · 고른 탭 자료만 받음 · 설정 카드는 탭 줄 위 · 회사 고르기 알약은 탭 줄 오른쪽 하나
- 회사 없음 진입 두 곳(router · M02 Layer)은 ?tab=company · 회사 관리자 프로젝트 목록 한 번만 받음
- 개요 숫자 카드(단계별 · 실패 · 재확인 · 가입 대기 · 보관함 · 리소스) · 프로젝트 표 칸 보강 · 찾기 · 거르기 · 쪽 넘김 · 진행 현황
- 회사 정보 칸(사업자번호 · 대표 · 상태) · 팀원 읽기 · 자산 표 · 내 계정 마지막 로그인 · 시스템 리소스 7·30·90일 · 활성 추이 · 전체 회사 · 사용자 · 로그 쪽 · 고객 문의
- 시험 test_b01_dashboard_tabs 신규 · test_b01_settings_cards 고침

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
2026-10-08 13:58:34 +09:00

201 lines
8.8 KiB
Python

"""B01 대시보드 항목별 탭 시험(92장) — 화면 조작은 ORCA.
· 탭 줄: 역할별 탭 수 · 해시 값 해석(없거나 모르는 값 = 개요) · 시스템 탭은 시스템 관리자만
· 프로젝트 계산: 단계별 수 · 실패 · 재확인 · 거르기 · 찾기 · 진행 현황 단계 고르기
· 진입 두 곳(회사 없음)이 회사 탭으로 · 같은 프로젝트 목록을 두 번 받지 않음
· 새 문구 키가 사전에 있음 · 소스 700줄 이하
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
B01 = ROOT / "B01_Dashboard"
PAGE = B01 / "B01_Dashboard_UI_Page.ts"
STATS = B01 / "B01_Dashboard_UI_ProjectStats.ts"
TABS = B01 / "B01_Dashboard_UI_Tabs.ts"
NODE = ["node", "--experimental-strip-types", "--no-warnings"]
def _text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _node(script: str, *args: str) -> object:
result = subprocess.run(
[*NODE, "--input-type=module", "-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)
def _stage(no: int, state: str, **extra: object) -> dict:
return {"stage_no": no, "stage_key": f"S{no}", "state": state, "progress_percent": 0, **extra}
def _project(name: str, current: int, states: tuple[str, ...] = (), **extra: object) -> dict:
stages = [_stage(i, states[i] if i < len(states) else "NOT_STARTED") for i in range(7)]
return {
"id": name, "name": name, "workflow_stage": 0,
"workflow_state": {"project_id": name, "current_stage": current, "stages": stages},
**extra,
} # fmt: skip
PROJECTS = [
_project(
"가-노선", 2, ("COMPLETE", "COMPLETE", "IN_PROGRESS"), region="강원", client_org="산림청"
),
_project("나-횡단", 3, ("COMPLETE", "COMPLETE", "COMPLETE", "FAILED"), owner_name="홍길동"),
_project("다-상세", 4, ("COMPLETE", "STALE", "COMPLETE", "COMPLETE", "IN_PROGRESS")),
_project("라-입력", 0, ()),
{
"id": "마",
"name": "마-옛",
"workflow_stage": 9,
}, # 단계 정보 없는 옛 줄 · 단계 9 → 끝으로 눌림
]
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_프로젝트_계산():
script = (
f"import * as m from '{STATS.as_uri()}';\n"
"const ps = JSON.parse(process.argv[1]);\n"
"const names = (f, q = '') => ps.filter((p) => m.matchesFilter(p, f) && m.matchesQuery(p, q))"
".map((p) => p.name);\n"
"console.log(JSON.stringify({\n"
" sum: m.summarizeProjects(ps),\n"
" failed: names('failed'), stale: names('stale'), stage2: names('stage-2'),\n"
" stage6: names('stage-6'), all: names('all').length,\n"
" q1: names('all', '산림청'), q2: names('all', '강원 노선'), q3: names('all', '없는말'),\n"
" filters: ['failed', 'stale', 'stage-3', 'stage-7', 'stage-x', null, 'all']"
".map((v) => m.parseFilter(v)),\n"
" prog: ps.map((p) => m.progressStage(p)?.stage_no ?? null),\n"
"}));\n"
)
out = _node(script, json.dumps(PROJECTS))
assert out["sum"] == {
"total": 5,
"byStep": [1, 0, 1, 1, 1, 0, 1], # 마-옛 는 9 → 6 으로 눌림
"failed": 1,
"stale": 1,
}
# 개요 숫자 = 프로젝트 탭 거르기 수
assert out["failed"] == ["나-횡단"] and len(out["failed"]) == out["sum"]["failed"]
assert out["stale"] == ["다-상세"] and len(out["stale"]) == out["sum"]["stale"]
assert out["stage2"] == ["가-노선"] and out["stage6"] == ["마-옛"] and out["all"] == 5
assert out["q1"] == ["가-노선"] and out["q2"] == ["가-노선"] and out["q3"] == []
assert out["filters"] == ["failed", "stale", "stage-3", "all", "all", "all", "all"]
# 진행 현황 단계 — 실패 → 재확인 → 지금 단계
assert out["prog"] == [2, 3, 1, 0, None]
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_탭_수와_해시_해석():
src = _text(TABS)
funcs = "\n".join(
re.search(rf"^export function {name}[\s\S]*?^}}", src, re.M).group(0).replace("export ", "")
for name in ("tabsFor", "resolveTab")
)
script = (
"type Tab = 'overview' | 'projects' | 'company' | 'account' | 'system';\n"
"type Role = 'SYSTEM_ADMIN' | 'ADMIN' | 'USER';\n"
f"interface DashboardUser {{ role: Role }}\n{funcs}\n"
"const roles: Role[] = ['SYSTEM_ADMIN', 'ADMIN', 'USER'];\n"
"console.log(JSON.stringify({\n"
" tabs: roles.map((r) => tabsFor(r)),\n"
" pick: roles.map((r) => ['company', 'system', 'zzz', null].map((v) => resolveTab(r, v))),\n"
"}));\n"
)
result = subprocess.run(
[*NODE, "--input-type=module-typescript", "-e", script],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
out = json.loads(result.stdout)
base = ["overview", "projects", "company", "account"]
assert out["tabs"] == [[*base, "system"], base, base] # 시스템 관리자 5 · 나머지 4
assert out["pick"] == [
["company", "system", "overview", "overview"],
["company", "overview", "overview", "overview"], # 시스템 탭은 시스템 관리자만
["company", "overview", "overview", "overview"],
]
assert "navigateTo(ROUTES.B01_ACCOUNT, { tab, ...extra })" in src # 라우터 안 바꿈 · 해시만
def test_회사_없음_두_곳은_회사_탭으로():
router = _text(ROOT / "A00_Common" / "router.ts")
assert 'navigateTo(ROUTES.B01_ACCOUNT, { tab: "company" })' in router
layer = _text(ROOT / "M02_MasterTemplete" / "M02_MasterTemplete_Layer.ts")
assert 'navigateTo(ROUTES.B01_ACCOUNT, { tab: "company" })' in layer
def test_탭마다_그_탭_자료만_받음():
src = _text(PAGE)
load = src.split("async function loadTabData")[1].split("\n}\n")[0]
# 같은 프로젝트 목록을 두 번 받지 않음 — 회사 관리자는 회사 목록 하나
assert load.count("fetchUserProjects()") == 1 and load.count("fetchCompanyProjects(") == 2
assert 'if (tab === "overview" || tab === "projects")' in load
for key in ("overview", "company", "system"):
assert f'tab === "{key}"' in load
# 고른 회사 인자 한 곳
assert "const scoped = system ? state.adminCompanyId : undefined;" in load
for call in ("fetchCompanyProjects(scoped)", "fetchAllUsers(scoped)",
"fetchCompanyJoinRequests(scoped)"): # fmt: skip
assert call in load, call
# 시스템 탭 자료는 시스템 탭에서만
sys_part = load.split('if (tab === "system")')[1]
for call in ("fetchAuditLogPage(", "fetchSupportRequests()", "fetchAllUsers(null)"):
assert call in sys_part, call
assert "fetchSupportRequests" not in load.split('if (tab === "system")')[0]
def test_설정_카드는_탭_줄_위_알약은_탭_줄_오른쪽_하나():
src = _text(PAGE)
build = src.split("function buildPage(")[1].split("\n}\n")[0]
assert build.count("buildSystemSettingsSection(state)") == 1
assert build.index("buildSystemSettingsSection(state)") < build.index("buildTabBar(")
assert build.count("buildCompanyPill(state)") == 1
assert 'const ADMIN_COMPANY_KEY = "aislo.adminCompany";' in src # M02 와 같은 키
assert "withCompanyPill" not in src
tab_bar = _text(TABS)
assert "if (trailing) bar.append(trailing);" in tab_bar
# 설정 카드 안 알약은 없어짐
assert (
"company-pill" not in src.split("function buildSystemSettingsSection")[1].split("\n}\n")[0]
)
def test_새_문구_키가_사전에_있음():
locales = "".join(_text(p) for p in (ROOT / "ui_template").glob("ui_template_locale*.ts"))
names = ("Tabs", "Overview", "ProjectsTab", "CompanyTab", "System", "Paging", "Profile",
"Projects", "Company", "Admin", "Page") # fmt: skip
for name in names:
src = _text(B01 / f"B01_Dashboard_UI_{name}.ts")
for key in set(re.findall(r'L\(\s*"(B01_[A-Za-z_]+)"', src)):
assert f"{key}:" in locales, f"{name}: {key}"
# 템플릿 글자로 만드는 키(상태 · 문의 상태)도 모두 있음
for key in (
*(
f"B01_Pj_State_{s}"
for s in ("NOT_STARTED", "IN_PROGRESS", "COMPLETE", "FAILED", "STALE")
),
*(f"B01_Sy_Inquiry_{s}" for s in ("NEW", "REVIEWED", "RESOLVED", "DELETED")),
):
assert f"{key}:" in locales, key
def test_소스_700줄_이하():
for path in [*B01.glob("B01_Dashboard_UI_*.ts"), *B01.glob("B01_Dashboard_UI_Style*.css")]:
assert len(_text(path).splitlines()) <= 700, path.name