- 새 UI_Side_Sangeun.ts — 서고 칸(716-4) kind sangeun · 목록표 고름 sangeunList - 새 Sangeun_View.ts — 이름 · 단위 · 바탕 · 덮개 · 더한 줄 · 뺀 줄 · 열린 입력 요약 + 상세 자리표(717-2b 가 갈음) - UI_Main 종류 sangeun 갈래 · 목록표 조건에 sangeun · Library_List 열 표(단가산출 = 바탕 · 고친 줄 · 쓰는 곳) · sangeunBlank(단위 식 · 바탕 없음) - UI_Page 단가산출 층 상태 sk · UI_Side 맨 위 칸 · reveal 「단가산출」 · Layer open 「단가산출」 · Api Kind sangeun - m_settings_nav 대시보드 마스터 템플릿 묶음 「단가산출」 줄(?open=단가산출) · locale_m2 키 - 낡은 글 대조 시험 넷 고침(197_1 · b01_settings_cards · m02_layer_screen · m02_precombo_scope) — 대시보드 줄 하나 늘고 OPENS · reveal · 목록 열 표 · 칸 차례가 바뀜 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
183 lines
9.1 KiB
Python
183 lines
9.1 KiB
Python
"""197-1 · 716-1 대시보드 「구조물 옵션 정의」 줄 → M01 「구조물 옵션 정의」 화면이 바로 열림 시험 — 조작은 ORCA.
|
|
|
|
716-1 — 화면 입구가 M02 에서 M01 로 옮겨감(화면 코드 · 서버 길은 M02 폴더 그대로) · 옛 M02 `open=옵션` 주소는
|
|
시스템 관리자면 M01 `?open=옵션` 으로 돌려보냄.
|
|
|
|
원인 — 진입 해시 `open=옵션` 이 옵션 줄을 눌러 정의 화면을 띄우는 사이 첫 화면(집계표)의 `mount` 가
|
|
모듈 · 번호를 기다리다 뒤늦게 끝나 정의 화면 위에 집계표를 덮음(`mount` 에 낡음 검사 없었음).
|
|
|
|
· 줄 → 해시(M01 open=로직 · 옵션 / M02 open=일위대가조합 · 구조물조합 · 도면) · 해시 → 진입(`entryFromHash`) Node
|
|
· M02 `reveal` 가 옵션만 줄을 누르고 도면은 펼치기만 · 구조물조합은 아무것도 안 함 · M01 `reveal` 도 같은 꼴
|
|
· 옛 `open=옵션` 돌려보냄(`redirectOldOptions`)
|
|
· `mount` 집계표 갈래(진짜 소스 조각)를 Node 로 돌림 — 기다리는 사이 다른 화면이 열리면 그리지 않음
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
M02 = ROOT / "M02_MasterTemplete"
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
NODE = ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript"]
|
|
needs_node = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
|
|
|
|
|
def _text(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def _run(script: str, *args: str) -> object:
|
|
result = subprocess.run(
|
|
[*NODE, "-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)
|
|
|
|
|
|
@needs_node
|
|
def test_줄_해시_옵션은_M01():
|
|
from test_b01_settings_cards import _cards
|
|
|
|
cards = _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3)
|
|
heads = [c[2].split("?")[0] + "?" + c[2].split("?")[1].split("&")[0] for c in cards[1:8]]
|
|
assert heads == [
|
|
"m01-master-data?open=로직",
|
|
"m01-master-data?open=옵션",
|
|
"m01-master-data?open=별칭", # 718-3
|
|
"m02-master-template?open=단가산출", # 717-2a
|
|
"m02-master-template?open=일위대가조합",
|
|
"m02-master-template?open=구조물조합",
|
|
"m02-master-template?open=도면",
|
|
]
|
|
assert all("&root=master" in c[2] for c in cards[4:8])
|
|
|
|
|
|
@needs_node
|
|
def test_해시_진입_셋():
|
|
layer = _text(M02 / "M02_MasterTemplete_Layer.ts")
|
|
fn = re.search(r"^export function entryFromHash\([\s\S]*?^}", layer, re.M)
|
|
assert fn, "entryFromHash 를 못 찾음"
|
|
opens = re.search(r"^const OPENS[^\n]*", layer, re.M)
|
|
roots = re.search(r"^const ROOTS[^\n]*", layer, re.M)
|
|
assert opens and roots
|
|
script = (
|
|
"const currentQuery = () => new URLSearchParams(process.argv[1]);\n"
|
|
"const defaultRoot = () => 'master';\n"
|
|
"type Root = string; type OpenTarget = string; type Entry = any; type SessionUser = any;\n"
|
|
f"{opens.group(0)}\n{roots.group(0)}\n"
|
|
+ fn.group(0).replace("export function", "function")
|
|
+ "\nconsole.log(JSON.stringify(entryFromHash(null)));"
|
|
)
|
|
for want in ("구조물조합", "옵션", "도면", "일위대가조합", "단가산출"):
|
|
got = _run(script, f"open={want}&root=master&company=3")
|
|
assert got["open"] == want and got["root"] == "master" and got["company"] == "3"
|
|
|
|
|
|
def test_reveal_옵션은_M01_로():
|
|
side = _text(M02 / "M02_MasterTemplete_UI_Side.ts")
|
|
body = side.split("const reveal = ")[1].split("\n };")[0]
|
|
# 716-1 · 716-3 · 717-2a — 옵션 정의는 M01 로 옮김 · M02 reveal 은 단가산출 · 일위대가 · 도면만 펼침
|
|
flat = " ".join(body.split())
|
|
assert (
|
|
'target === "단가산출" ? sangeunSec : target === "일위대가조합" ? precomboSec'
|
|
' : target === "도면" ? drawing : null'
|
|
) in flat
|
|
assert 'sec?.classList.remove("is-collapsed");' in body
|
|
assert "options" not in body and "옵션: section(" not in side
|
|
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
|
|
assert 'if (entry.open !== "구조물조합") side.reveal(entry.open);' in page
|
|
|
|
|
|
@needs_node
|
|
def test_집계표_갈래_기다리는_사이_다른_화면이_열리면_그리지_않음():
|
|
main = _text(M02 / "M02_MasterTemplete_UI_Main.ts")
|
|
start = main.index(" async function mount(doc: unknown, at: Selection)")
|
|
stop = main.index(' if (at.kind === "labor" || isPrecombo(at.kind)) {', start)
|
|
table = main[start:stop] + "\n}\n"
|
|
assert table.count("if (mine !== seq)") == 2 # 모듈 받은 뒤 · 번호 받은 뒤
|
|
script = (
|
|
"type Selection = any; type EditorHandle = any; type SheetEditorHandle = any;\n"
|
|
"type StructureColumn = any;\n"
|
|
"let seq = 0; let panel: any = null; let editor: any = null; let sheet: any = null;\n"
|
|
"let dirty = false; const log: string[] = [];\n"
|
|
"const STRUCTURE_TABLE = 'T'; const SHEET_MODS = {}; const L = (k: string) => k;\n"
|
|
"const loaded: any = null; const extras: any = {};\n"
|
|
"const host = { replaceChildren() {} }; const writable = () => true;\n"
|
|
"const empty = (m: string) => log.push('empty:' + m);\n"
|
|
"const comboView = (d: any) => d; const comboMerge = (_: any, d: any) => d;\n"
|
|
"const laborSumText = () => ''; const laborSumState = () => '';\n"
|
|
"const numberLabels = (_: any) => ({ laborLabel: () => '', drawingLabel: () => '',"
|
|
" areaLabel: () => '', laborSumLabel: () => '', laborSumState: () => undefined });\n"
|
|
"const gate = (): [Promise<void>, () => void] => {\n"
|
|
" let go = () => {}; return [new Promise<void>((r) => (go = r)), () => go()];\n"
|
|
"};\n"
|
|
"let modGate = gate(); let numGate = gate();\n"
|
|
"const findExport = async () => {\n"
|
|
" await modGate[0]; return (_h: any, _d: any, _o: any) => (log.push('create'),"
|
|
" { destroy() {}, getDoc: () => ({}) });\n"
|
|
"};\n"
|
|
"const buildPanel = () => ({ root: {}, numbers: () => ({}), async reload() {\n"
|
|
" await numGate[0]; }, dirty: () => false, destroy: () => log.push('panel.destroy') });\n"
|
|
+ table
|
|
+ "\nasync function run(bumpAt: string): Promise<string[]> {\n"
|
|
" seq = 1; log.length = 0; panel = null; modGate = gate(); numGate = gate();\n"
|
|
" const going = mount({}, { layer: 'system', kind: 'table', name: STRUCTURE_TABLE,"
|
|
" projectId: null });\n"
|
|
" if (bumpAt === 'mod') seq++;\n"
|
|
" modGate[1](); await new Promise((r) => setTimeout(r, 5));\n"
|
|
" if (bumpAt === 'num') seq++;\n"
|
|
" numGate[1](); await going; return [...log, 'panel:' + (panel ? 'set' : 'null')];\n"
|
|
"}\n"
|
|
"console.log(JSON.stringify({ none: await run('none'), mod: await run('mod'),"
|
|
" num: await run('num') }));"
|
|
)
|
|
got = _run(script)
|
|
assert got["none"] == ["create", "panel:set"] # 안 끼어들면 그대로 그림
|
|
assert got["mod"] == ["panel:null"] # 모듈 기다리는 사이 다른 화면 — 그리지 않음
|
|
assert got["num"] == ["panel.destroy", "panel:null"] # 번호 기다리는 사이 — 패널도 버림
|
|
|
|
|
|
@needs_node
|
|
def test_옛_옵션_주소_M01로_돌려보냄():
|
|
layer = _text(M02 / "M02_MasterTemplete_Layer.ts")
|
|
fn = re.search(r"^export function redirectOldOptions\([\s\S]*?^}", layer, re.M)
|
|
assert fn, "redirectOldOptions 를 못 찾음"
|
|
script = (
|
|
"const currentQuery = () => new URLSearchParams(process.argv[1]);\n"
|
|
"const ROUTES = { M01_MASTER_DATA: 'm01' }; const sent: string[] = [];\n"
|
|
"const navigateTo = (r: string, q: any) => sent.push(r + JSON.stringify(q));\n"
|
|
"type SessionUser = any;\n"
|
|
+ fn.group(0).replace("export function", "function")
|
|
+ "\nconst a = redirectOldOptions({ role: 'SYSTEM_ADMIN' });"
|
|
+ "\nconst b = redirectOldOptions({ role: 'USER' });"
|
|
+ "\nconst c = redirectOldOptions(null);"
|
|
+ "\nconsole.log(JSON.stringify([a, b, c, sent]));"
|
|
)
|
|
assert _run(script, "open=옵션&root=master") == [True, False, False, ['m01{"open":"옵션"}']]
|
|
# 옵션이 아니면 안 돌려보냄
|
|
assert _run(script, "open=도면&root=master") == [False, False, False, []]
|
|
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
|
|
assert "if (redirectOldOptions(user))" in page
|
|
assert 'mountSettingsDrawer(user ?? {}, "m02", entry.company);' in page
|
|
|
|
|
|
def test_M01_open_인자_로직_옵션():
|
|
m01 = ROOT / "M01_MasterData"
|
|
page = _text(m01 / "M01_MasterData_UI_Page.ts")
|
|
assert 'if (want === "로직" || want === "옵션" || want === "별칭") side.reveal(want);' in page
|
|
assert "setLeaveGuard(leaveOptions)" in page # 저장 안 한 정의 고침 — 떠날 때 물음
|
|
side = _text(m01 / "M01_MasterData_UI_Side.ts")
|
|
assert "buildOptionsSection({ ...optionsOpt, isAdmin: true })" in side
|
|
# 로직 칸 아래 구분선 부품 + 옵션 칸
|
|
assert "logic, createDivider(), optionsSec, aliasSec" in side # 718-3 별칭 장부 칸
|