- 해시 ?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
201 lines
8.8 KiB
Python
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
|