"""200-1 B09 박스보기 안내 글 줄임표 + 호버 · 카드 같은 가로 줄 높이 같게. - 안내 글 = 203 줄 수 변수만큼(넘치면 스크롤) · 가로로 잘렸을 때만 공용 호버(`attachHoverInfo`)로 전문 · 자리 = 표 · 카드 판 아래(202-1 로 옮김) - 카드 격자 = 행 늘림(`align-items: stretch`) · 카드 = 세로 flex · 몸통이 늘어 합계 줄이 아래 - 카드 제목 줄 · 합계 줄 바탕색(200-3) """ import json import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402 B09 = "B09_Estimation/B09_Estimation_UI_Tabs_" DG = {"id": "DG1", "no": 1, "name": "찰쌓기", "unit": "m2", "lines": [], "sum": 0} LONG = "손료 = 기준가(천원) × 손료계수 ÷ 10,000 · 운전사 = 인 × 시간 노임 · 연료 = ℓ/hr × 적용 단가" HINT = ( "(() => { const h = document.createElement('p');" f" h.className = 'ui-general-block__empty'; h.textContent = '{LONG}'; return h; }})()" ) def _block(css: str, selector: str) -> str: m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) assert m, selector return m.group(1) def _panel(view: str, hint: str): """고른 보기 · 안내 글로 그린 뒤 안내 글 자리 · 호버 기록.""" return run( [COMMON, TABS, f"{B09}Labor.ts", GROUPS], f"\nconst spec = buildDaegaBlocks({{ sheets: {{ daega_blocks: [{json.dumps(DG, ensure_ascii=False)}] }} }});" f"\nchooseGroupView('{view}');" f"\nconst hint = {hint};" "\nconst o = { tab: 'dg', spec, hint, goto: () => {}, redraw: () => {}, setTools: () => {}," " makeTable: () => ({ root: document.createElement('table'), focusRow: () => {} }) };" "\nconst out = renderGroupPanel(o);" "\nconst last = out[out.length - 1];" "\nconst inBar = hint ? out.some((e) => e.all('.ui-group-cards__bar').some((b) => b.contains(hint))) : false;" "\nconst call = hoverCalls[0];" "\nconst wide = call ? call.text({ scrollWidth: 900, clientWidth: 300, textContent: 'T' }) : null;" "\nconst fit = call ? call.text({ scrollWidth: 300, clientWidth: 300, textContent: 'T' }) : null;" "\nconsole.log(JSON.stringify({ n: out.length, hintLast: !!hint && last === hint, inBar," " hintCls: hint ? hint.className : '', hover: hoverCalls.length," " hoverIsHint: call ? call.root === hint : false, wide, fit, first: out[0].tagName }));", RAW, ) def test_카드_보기_안내_글은_카드_판_아래(): out = _panel("cards", HINT) assert out["n"] == 2 and out["hintLast"] and not out["inBar"] # 카드 판 + 안내 글(맨 아래) assert "b09-estimation__line-hint" in out["hintCls"] def test_표_보기_안내_글은_표_아래(): out = _panel("table", HINT) assert out["n"] == 2 and out["first"] == "TABLE" and out["hintLast"] # 표 + 안내 글 def test_안내_글_잘렸을_때만_공용_호버로_전문(): for view in ("cards", "table"): out = _panel(view, HINT) assert out["hover"] == 1 and out["hoverIsHint"] assert out["wide"] == "T" # 잘림(scrollWidth > clientWidth) = 전문 assert out["fit"] is None # 다 보이면 안 띄움 def test_안내_글_없으면_호버도_없음(): for view in ("cards", "table"): out = _panel(view, "null") assert out["n"] == 1 and out["hintLast"] is False and out["hover"] == 0 def test_CSS_안내_글_줄_수_변수(): page = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Page.css").read_text("utf-8") hint = _block(page, ".b09-estimation__line-hint") for need in ( # 203 한 줄 줄임표 → 줄 수 변수만큼 · 넘치면 스크롤 "max-height: calc(var(--b09-hint-lines) * 1lh)", "overflow-y: auto", "min-width: 0", ): assert need in hint, need def test_CSS_카드_같은_줄_높이_같게(): css = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8") grid = _block(css, ".ui-group-cards__grid") assert "align-items: stretch" in grid and "align-items: start" not in grid card = _block(css, ".ui-group-card") assert "display: flex" in card and "flex-direction: column" in card assert "flex: 1 1 auto" in _block(css, ".ui-group-card__body") # 합계 줄 = 카드 아래 def test_잇기(): groups = (ROOT / GROUPS).read_text("utf-8") assert "attachHoverInfo(hint," in groups and "placeTools: o.setTools" in groups cards = (ROOT / COMMON).read_text("utf-8") assert "opts.lead" not in cards and "if (opts.placeTools) {" in cards # ── 200-3 카드 제목 줄 · 합계 줄 바탕색(밝음 · 어두움) ───────────────────────── def _theme_vars(dark: bool) -> dict: """테마 CSS 의 `:root`(밝음) · 어두움 덮어쓰기 토큰 → 한 벌.""" css = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8") light = dict(re.findall(r"(--[\w-]+):\s*([^;]+);", _block(css, ":root"))) if dark: light.update(re.findall(r"(--[\w-]+):\s*([^;]+);", _block(css, ':root[data-theme="dark"]'))) return light def _rgb(token: str, theme: dict) -> tuple: """토큰 → (r, g, b) · var() 별칭 따라감 · 16진수만.""" val = theme[token].split("/*")[0].strip() while m := re.fullmatch(r"var\((--[\w-]+)\)", val): val = theme[m.group(1)].split("/*")[0].strip() assert re.fullmatch(r"#[0-9a-fA-F]{6}", val), (token, val) return tuple(int(val[i : i + 2], 16) for i in (1, 3, 5)) def _mix(spec: str, theme: dict) -> tuple: """`color-mix(in srgb, var(A) N%, var(B))` → 섞은 (r, g, b).""" m = re.fullmatch( r"color-mix\(in srgb,\s*var\((--[\w-]+)\)\s*(\d+)%,\s*var\((--[\w-]+)\)\)", spec ) assert m, spec a, n, b = _rgb(m.group(1), theme), int(m.group(2)) / 100, _rgb(m.group(3), theme) return tuple(a[i] * n + b[i] * (1 - n) for i in range(3)) def _lum(c: tuple) -> float: ch = [ (v / 255) / 12.92 if v / 255 <= 0.03928 else (((v / 255) + 0.055) / 1.055) ** 2.4 for v in c ] return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2] def _contrast(a: tuple, b: tuple) -> float: hi, lo = sorted((_lum(a), _lum(b)), reverse=True) return (hi + 0.05) / (lo + 0.05) def _tokens() -> tuple: css = (ROOT / "ui_template" / "ui_template_group_cards.css").read_text("utf-8") root = _block(css, ".ui-group-cards") head = re.search(r"--group-head-bg:\s*([^;]+);", root) foot = re.search(r"--group-foot-bg:\s*([^;]+);", root) assert head and foot return css, head.group(1), foot.group(1) def test_카드_제목_줄과_합계_줄_바탕색_토큰(): css, head, foot = _tokens() assert head != foot assert "background-color: var(--group-head-bg)" in _block(css, ".ui-group-card__head") assert "background-color: var(--group-foot-bg)" in _block(css, ".ui-group-card__foot") for spec in (head, foot): # 고른 값 없이 테마 변수만(색 고정값 없음) assert "#" not in spec and "rgb(" not in spec def test_밝음_어두움_두_색이_다르고_글_대비_유지(): _, head, foot = _tokens() for dark in (False, True): theme = _theme_vars(dark) h, f = _mix(head, theme), _mix(foot, theme) assert sum(abs(h[i] - f[i]) for i in range(3)) >= 40, (dark, h, f) # 서로 다른 색 card = _rgb("--color-surface-raised", theme) assert sum(abs(f[i] - card[i]) for i in range(3)) >= 40 # 카드 바탕과도 구분 for color in ("--color-text", "--color-text-secondary", "--color-accent"): assert _contrast(_rgb(color, theme), h) >= 4.5, (dark, "head", color) for color in ("--color-text", "--color-text-body", "--color-text-secondary"): assert _contrast(_rgb(color, theme), f) >= 4.5, (dark, "foot", color) def test_어두움_테마_값(): # 어두움 테마에서 쓰는 토큰이 실제로 어두움 값으로 덮임(밝음 값 그대로가 아님) light, dark = _theme_vars(False), _theme_vars(True) for token in ("--color-surface", "--color-surface-raised", "--color-accent"): assert _rgb(token, light) != _rgb(token, dark), token