- 표 부품 — 열 0 이면 [열 더하기] 가 켜져 첫 열을 만들고 머리 글 고치기를 바로 엶(종류/분류 는 / 로 이음) - 표 부품 `readOnly` — 단추 줄 · 안내 글 숨김 · 칸 · 머리 · 층 이름 고치기 · 열 너비 끌기 · Delete 막음 · M02 가 읽기만일 때 넘김 - 시험 test_m02_layer_screen.py 7(첫 열 Node 실행 · 읽기만 자리) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Kyw9qnkJ94pC1A4PfpDtA
152 lines
7.0 KiB
Python
152 lines
7.0 KiB
Python
"""M02 층 화면(`?layer=personal|company`) 붙는 자리 시험 — 화면 조작은 ORCA.
|
|
|
|
· 권한 규칙(`accessFor`)을 Node 로 바로 돌림 — 시스템 SYSTEM_ADMIN ·
|
|
회사 ADMIN/is_master · 개인 본인 · 회사 없음
|
|
· 주소 인자를 라우트에서 떼는지 · 대시보드 카드 역할별 단추 · 층 길 접두어
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
M02 = ROOT / "M02_MasterTemplete"
|
|
LAYER = M02 / "M02_MasterTemplete_Layer.ts"
|
|
|
|
|
|
def _text(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
|
def test_층별_고칠_권한():
|
|
found = re.search(r"^export function accessFor[\s\S]*?^}", _text(LAYER), re.M)
|
|
assert found, "accessFor 를 못 찾음"
|
|
script = (
|
|
'type ManageLayer = "system" | "company" | "personal";\n'
|
|
"interface SessionUser { role?: string; company_id?: number | null; is_master?: boolean }\n"
|
|
"interface LayerAccess { canWrite: boolean; needCompany: boolean }\n"
|
|
+ found.group(0).replace("export ", "")
|
|
+ "\nconst cases = JSON.parse(process.argv[1]);"
|
|
+ "\nconsole.log(JSON.stringify(cases.map((c) => accessFor(c[0], c[1]))));"
|
|
)
|
|
sysadmin = {"role": "SYSTEM_ADMIN", "company_id": None}
|
|
admin = {"role": "ADMIN", "company_id": 3}
|
|
master = {"role": "USER", "company_id": 3, "is_master": True}
|
|
user = {"role": "USER", "company_id": 3}
|
|
orphan = {"role": "USER", "company_id": None}
|
|
cases = [
|
|
["system", sysadmin], ["system", admin], ["system", None],
|
|
["company", admin], ["company", master], ["company", user], ["company", orphan],
|
|
["personal", user], ["personal", admin], ["personal", orphan], ["personal", sysadmin],
|
|
] # fmt: skip
|
|
result = subprocess.run(
|
|
["node", "--experimental-strip-types", "--no-warnings",
|
|
"--input-type=module-typescript", "-e", script, json.dumps(cases)],
|
|
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
|
) # fmt: skip
|
|
assert result.returncode == 0, result.stderr
|
|
ok = {"canWrite": True, "needCompany": False}
|
|
ro = {"canWrite": False, "needCompany": False}
|
|
no_company = {"canWrite": False, "needCompany": True}
|
|
assert json.loads(result.stdout) == [
|
|
ok, ro, ro, # 시스템 = SYSTEM_ADMIN 만
|
|
ok, ok, ro, no_company, # 회사 = ADMIN · is_master · 일반은 읽기만 · 회사 없음 안내
|
|
ok, ok, no_company, no_company, # 개인 = 본인 · 회사 없으면 안내
|
|
] # fmt: skip
|
|
|
|
|
|
def test_주소_인자를_라우트에서_뗌():
|
|
router = _text(ROOT / "A00_Common" / "router.ts")
|
|
assert '.split("?")[0]' in router
|
|
assert "export function currentQuery()" in router
|
|
assert "query?: Record<string, string>" in router
|
|
layer = _text(LAYER)
|
|
assert 'value === "company" || value === "personal" ? value : "system"' in layer
|
|
|
|
|
|
def test_대시보드_카드_역할별_단추():
|
|
page = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_UI_Page.ts")
|
|
assert (
|
|
page.count('section(L("B01_Dashboard_StructureManage"), buildStructurePanel(state))') == 3
|
|
)
|
|
body = page.split("function buildStructurePanel")[1].split("function buildPage")[0]
|
|
# 개인용은 모두 · 회사용은 ADMIN · SYSTEM_ADMIN 만
|
|
assert 'state.user.role === "ADMIN" || state.user.role === "SYSTEM_ADMIN"' in body
|
|
assert body.index('go("company"') < body.index('go("personal"')
|
|
assert "ROUTES.M02_MASTER_TEMPLATE, { layer }" in body
|
|
config = _text(ROOT / "config" / "config_frontend.ts")
|
|
assert re.search(r"PROTECTED_ROUTES[\s\S]*?ROUTES\.M02_MASTER_TEMPLATE,[\s\S]*?\];", config)
|
|
|
|
|
|
def test_층_길_접두어와_고정_풀림():
|
|
api = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts")
|
|
assert 'activeLayer === "system" ? "" : `/layers/${activeLayer}`' in api
|
|
for path in ("/structures/numbers", "/structure-combos", "/material-combos"):
|
|
assert re.search(rf'scoped\(\s*[`"]{re.escape(path)}', api), path
|
|
assert "/layers/personal/items/copy" in api
|
|
for name in ("Structure", "Combo_Api"):
|
|
assert '"system"' not in _text(M02 / f"M02_MasterTemplete_{name}.ts")
|
|
side = _text(M02 / "M02_MasterTemplete_UI_Side.ts")
|
|
assert "const layer = opt.layer;" in side
|
|
assert 'layer === "system"' in side # 도면 양식은 시스템 층에만
|
|
main = _text(M02 / "M02_MasterTemplete_UI_Main.ts")
|
|
assert 'sel.layer !== "system"' not in main
|
|
assert 'return layer === "project" || isAdmin;' in main
|
|
|
|
|
|
def test_읽기만_회사용_개인용으로_복사_막대():
|
|
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
|
|
assert 'layer === "company" && !isAdmin ? buildReadOnlyBar(user)' in page
|
|
assert "onColumn: (column) => readOnlyBar?.setColumn(column)" in page
|
|
assert "access.needCompany" in page and "buildNeedCompany()" in page
|
|
assert "titleOf(layer)" in page
|
|
layer = _text(LAYER)
|
|
assert 'fromLayer: "company"' in layer and "M02_CopyToPersonal" in layer
|
|
|
|
|
|
SHEET = ROOT / "ui_template" / "sheet"
|
|
|
|
|
|
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
|
def test_열_0_첫_열_만들기():
|
|
"""빈 개인 · 회사 층 — 열이 하나도 없어도 첫 열(머리 글 + 빈 아래 층)을 만듦."""
|
|
ops = _text(SHEET / "ui_template_sheet_ops.ts")
|
|
fresh = re.search(r"^function freshId[\s\S]*?^}", ops, re.M)
|
|
first = re.search(r"^export function addFirstColumn[\s\S]*?^}", ops, re.M)
|
|
assert fresh and first, "freshId · addFirstColumn 을 못 찾음"
|
|
script = (
|
|
"interface SheetColumn { id: string; 머리: (string | null)[]; 단위?: string | null;"
|
|
" 꼴?: string; 일위대가?: string | null; 자재조합?: string | null }\n"
|
|
"interface SheetDoc { 열: SheetColumn[] }\n"
|
|
+ fresh.group(0)
|
|
+ "\n"
|
|
+ first.group(0).replace("export ", "")
|
|
+ "\nconst doc: SheetDoc = { 열: [] };"
|
|
+ "\nconst a = addFirstColumn(doc, 4, '새 열');"
|
|
+ "\nconsole.log(JSON.stringify([a, doc.열[0].머리, doc.열[0].꼴, doc.열.length]));"
|
|
)
|
|
result = subprocess.run(
|
|
["node", "--experimental-strip-types", "--no-warnings",
|
|
"--input-type=module-typescript", "-e", script],
|
|
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
|
) # fmt: skip
|
|
assert result.returncode == 0, result.stderr
|
|
assert json.loads(result.stdout) == ["c1", ["새 열", "", "", ""], "수", 1]
|
|
|
|
|
|
def test_읽기만_표는_단추_고치기_모두_막음():
|
|
sheet = _text(SHEET / "ui_template_sheet.ts")
|
|
assert "const ro = !!opts.readOnly;" in sheet
|
|
assert sheet.count("if (ro") + sheet.count("!ro") + sheet.count("ro ||") >= 7
|
|
# 열 0 = 「열 더하기」 하나만 켜짐 · 첫 열
|
|
assert "b.disabled = !hasCol && !empty" in sheet and "addFirstColumn(" in sheet
|
|
assert re.search(r"readOnly,\s*onChange", _text(M02 / "M02_MasterTemplete_UI_Main.ts"))
|
|
assert ".ui-sheet__toolbar[hidden]" in _text(SHEET / "ui_template_sheet.css")
|