Files
Aislo/resources/tester/test_547_2_home.py
T
eomsangdonandClaude Opus 5.5 729f4c75b8 feat(B01): 대시보드 홈 — 진행 중 수에 따라 꼴이 저절로(1 큰 카드 · 2~3 중간 · 4~8 한 줄 · 9 넘음 요약) (PLAN 547-2)
- 등록부 home 줄 = mountHome · 썸네일 선 · 단계 점 · 「다음: 지금 단계」 · [이어서 하기] = 그 단계 화면 · [자세히] = 상세
- 4~8 = 팀원별 · 단계 분포 · 14 일 넘게 그대로 표시 · 9 넘음 = 요약 숫자 · 회사별 막대 · 종류 원(인라인 SVG)
- 오른쪽 = 처리할 일 · 임시 보관함 · 회사(회사 관리자) · 예정노선(산림청) · 처음 = 세 걸음 · 꼴 고정(자동 · 작게 · 크게) 기억
- 홈 역할 차례 = 셸 roleOf 와 같게(회사 관리자가 산림청 앞)
- test_546 등록부 시험 — import 한 mount 이름을 빈 함수로 저절로(페이지 줄 갈아 끼워도 그대로)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 18:20:21 +09:00

153 lines
5.7 KiB
Python

"""547-2 대시보드 홈 — 개수별 꼴 고르기(TS logic) · 역할 가르기 · 역할별 홈 API 칸."""
import asyncio
import json
import re
import subprocess
from pathlib import Path
import pytest
import B01_Dashboard.B01_Dashboard_Router_Home as home
ROOT = Path(__file__).resolve().parents[2]
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
LOGIC = ROOT / "B01_Dashboard" / "logic" / "B01_Dashboard_Home_Layout.ts"
_RUNNER = """
import { writeFileSync } from "node:fs";
import * as m from "./B01_Dashboard_Home_Layout.js";
const counts = [0, 1, 2, 3, 4, 8, 9, 40];
const out = {
auto: counts.map((n) => m.homeLayout(n)),
small: counts.map((n) => m.homeLayout(n, "small")),
large: counts.map((n) => m.homeLayout(n, "large")),
buckets: m.stageBuckets([
{ stage_index: 0, stage_total: 7 }, { stage_index: 3, stage_total: 7 },
{ stage_index: 6, stage_total: 7 }, { stage_index: 2, stage_total: 3 },
]),
stale: [m.isStale({ stage_index: 1, stage_total: 7, stale_days: 13 }),
m.isStale({ stage_index: 1, stage_total: 7, stale_days: 14 })],
count: m.countBy(["나", "가", "나"], (x) => x),
rest: m.topWithRest([5, 4, 3, 2, 1, 1].map((c, i) => ({ label: "c" + i, count: c })), 5),
donut: m.donutArcs([1, 1, 2]),
};
writeFileSync(process.argv[2], JSON.stringify(out));
"""
@pytest.fixture(scope="module")
def ts(tmp_path_factory):
if not TSC.is_file():
pytest.skip("프론트엔드 의존성 미설치")
tmp = tmp_path_factory.mktemp("home")
out = tmp / "js"
args = ["--outDir", str(out), "--module", "esnext", "--target", "es2022", "--ignoreConfig"]
subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(TSC), str(LOGIC), *args], cwd=str(ROOT), check=True, capture_output=True
)
for emitted in out.glob("*.js"):
text = emitted.read_text(encoding="utf-8")
emitted.write_text(re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', text), encoding="utf-8")
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
dst = tmp / "out.json"
subprocess.run(["node", str(out / "runner.mjs"), str(dst)], check=True, capture_output=True) # noqa: S603, S607
return json.loads(dst.read_text(encoding="utf-8"))
def test_layout_by_count(ts):
# 개수 0 1 2 3 4 8 9 40
assert ts["auto"] == ["empty", "one", "few", "few", "list", "list", "many", "many"]
assert ts["small"] == ["empty", "list", "list", "list", "list", "list", "many", "many"]
assert ts["large"] == ["empty", "one", "few", "few", "few", "few", "few", "few"]
def test_counts_and_charts(ts):
assert ts["buckets"] == {"start": 1, "working": 1, "last": 2}
assert ts["stale"] == [False, True]
assert ts["count"] == [{"label": "나", "count": 2}, {"label": "가", "count": 1}]
assert ts["rest"][-1] == {"label": "", "count": 2, "rest": 2} and len(ts["rest"]) == 5
assert [a["dash"] for a in ts["donut"]] == ["25 75", "25 75", "50 50"]
assert [a["offset"] for a in ts["donut"]] == [25, 0, -25]
def test_home_role_order_matches_shell():
s = {"email": "a@forest.go.kr"}
assert home.home_role({**s, "role": "SYSTEM_ADMIN"}) == "SYSTEM_ADMIN"
assert home.home_role({**s, "role": "ADMIN"}) == "ADMIN"
assert home.home_role({**s, "role": "USER"}) == "FOREST"
assert home.home_role({"email": "a@b.com", "role": "USER"}) == "USER"
class _Cursor:
async def __aenter__(self):
return self
async def __aexit__(self, *_):
return False
async def execute(self, sql, args=()):
self.sql = sql
async def fetchone(self):
return {"n": 2, "name": "회사", "member_count": 3, "companies": 4, "users": 9}
async def fetchall(self):
if "temp_upload_batches" in self.sql or "plan_route_years" in self.sql:
return []
return [{"id": 1, "name": "회사"}]
class _Pool:
def acquire(self):
class _Conn:
async def __aenter__(self):
return self
async def __aexit__(self, *_):
return False
def cursor(self, *_):
return _Cursor()
return _Conn()
@pytest.mark.parametrize(
("session", "keys", "join"),
[
({"role": "SYSTEM_ADMIN", "user_id": 1, "company_id": 1}, {"system"}, True),
({"role": "ADMIN", "user_id": 1, "company_id": 1}, {"company"}, True),
({"role": "USER", "user_id": 1, "company_id": 4, "email": "x@a.go.kr"}, {"forest"}, False),
({"role": "USER", "user_id": 1, "company_id": 1, "email": "x@a.com"}, set(), False),
],
)
def test_home_blocks_by_role(monkeypatch, session, keys, join):
async def fake_ids(_session, _role):
return ["a", "b"]
async def fake_summaries(ids):
return [
{"id": i, "done": i == "b", "thumb": [[0, 0], [1, 1]]} for i in ids
] # b = 완료 → 빠짐
async def fake_plans(_session):
return {"routes": []}
async def fake_resources(days):
return {"current": {"cpu_usage_percent": 1, "memory_usage_percent": 2}}
import D01_PlanRegister.D01_PlanRegister_Router as d01
monkeypatch.setattr(home, "home_project_ids", fake_ids)
monkeypatch.setattr(home, "project_summaries", fake_summaries)
monkeypatch.setattr(home, "get_db_pool", lambda: _Pool())
monkeypatch.setattr(home, "get_system_resources", fake_resources)
monkeypatch.setattr(d01, "assigned_plan_routes", fake_plans)
body = asyncio.run(home.dashboard_home(session=session))
assert {"system", "company", "forest"} & body.keys() == keys
assert [p["id"] for p in body["projects"]] == ["a"] and body["project_total"] == 2
assert ("join_requests" in body["todos"]) is join
if "system" in body:
assert body["system"]["server"] == {"cpu": 1, "memory": 2, "disk": None}