"""203 B09 탭 상세 안내 글 영역 = 줄 수 변수(`--b09-hint-lines` 기본 3)만큼 · 넘치면 영역 안 스크롤 · 표 칸 = 창 끝까지. - CSS: 줄 수 변수 하나 · 안내 글 최대 높이 = 변수 × 줄 높이(1lh) · 세로 스크롤 · 한 줄 줄임표 걷음 - Shell: 탭 표 = 공용 fillViewport(팝업 · 인쇄 표는 그대로) · 설계내역서 레벨 안내 + 탭 안내 = 한 영역 - 공용 spaceBelow: 아래 형제 바깥 여백(B09 안내 글 margin-top)도 셈 — 빼면 페이지 스크롤이 생김 - 실제 브라우저(정적 쪽 · 서버 불필요): 변수 3 · 2 × 줄 많음 · 적음 — 안내 글 높이 · 합계 줄 · 안내 글 자리 · 페이지 스크롤 0 """ import re import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] B09 = ROOT / "B09_Estimation" UI = ROOT / "ui_template" def _read(path: Path) -> str: return path.read_text("utf-8") def _block(css: str, selector: str) -> str: m = re.search(re.escape(selector) + r"\s*\{([^}]*)\}", css) assert m, selector return m.group(1) def test_CSS_줄_수_변수_하나(): css = _read(B09 / "B09_Estimation_UI_Page.css") assert re.search(r"--b09-hint-lines:\s*3;", _block(css, ".b09-estimation")) assert css.count("--b09-hint-lines:") == 1 # 고칠 자리 하나 hint = _block(css, ".b09-estimation__line-hint") assert "max-height: calc(var(--b09-hint-lines) * 1lh)" in hint assert "overflow-y: auto" in hint for gone in ("white-space: nowrap", "text-overflow: ellipsis"): assert gone not in hint, gone def test_Shell_표_창_끝_안내_한_영역(): shell = _read(B09 / "B09_Estimation_UI_Shell.ts") assert "fillViewport: true" in shell assert "panel.replaceChildren(table.root, ...(foot ? [foot] : []))" in shell assert "hint.textContent = `${level.guide.textContent} · ${hint.textContent}`" in shell # 팝업 · 인쇄 표는 창 끝 셈 안 씀(팝업 = 창 안 max-height · 인쇄 = 펼침) assert "fillViewport" not in _read(B09 / "B09_Estimation_UI_Builders.ts") assert "fillViewport" not in _read(B09 / "B09_Estimation_UI_Popup.ts") # ── 실제 브라우저 ───────────────────────────────────────────────────────────── TOKENS = ( ":root { --spacing-4: 4px; --spacing-8: 8px; --spacing-16: 16px; --spacing-24: 24px;" " --text-caption: 12px; --radius-lg: 8px; }" " html, body { margin: 0; line-height: 1.5; font-family: sans-serif; }" ) LONG = " · ".join(["긴 안내 글 시험 문장"] * 40) SHORT = "줄을 누르면 단가를 낸 탭 · 줄로 이동" def _module_js() -> str: """공용 표 모듈(TS) → 브라우저 ES 모듈(CSS import 뺌).""" src = UI / "ui_template_data_table.ts" code = ( "const ts = require(process.argv[1]);" "const fs = require('fs');" "const out = ts.transpileModule(fs.readFileSync(process.argv[2], 'utf8')," " { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2020 } });" "process.stdout.write(out.outputText);" ) ts_path = ROOT / "config" / "node_modules" / "typescript" proc = subprocess.run( ["node", "-e", code, str(ts_path), str(src)], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return re.sub(r'^import\s+"[^"]+\.css";\s*$', "", proc.stdout, flags=re.M) MEASURE = """async ([rows, lines, text]) => { const { createDataTable } = window.dt; const body = document.querySelector('.b09-estimation'); body.style.setProperty('--b09-hint-lines', String(lines)); const panel = body.querySelector('.b09-estimation__panel'); const table = createDataTable({ columns: [{ key: 'name', label: '이름' }, { key: 'sum', label: '금액', kind: 'number' }], rows: Array.from({ length: rows }, (_, i) => ({ name: '줄 ' + i, sum: i })), total: { name: '계', sum: 1 }, fillViewport: true, }); const hint = document.createElement('p'); hint.className = 'ui-general-block__empty b09-estimation__line-hint'; hint.textContent = text; panel.replaceChildren(table.root, hint); await new Promise((r) => setTimeout(r, 300)); const t = table.root.getBoundingClientRect(); const h = hint.getBoundingClientRect(); const foot = table.root.querySelector('tfoot').getBoundingClientRect(); return { vh: innerHeight, page: document.scrollingElement.scrollHeight, lh: parseFloat(getComputedStyle(hint).lineHeight), hintH: h.height, hintScrolls: hint.scrollHeight > hint.clientHeight, tableTop: t.top, tableBottom: t.bottom, tableScrolls: table.root.scrollHeight > table.root.clientHeight, maxH: parseFloat(table.root.style.maxHeight), hintTop: h.top, hintBottom: h.bottom, padB: parseFloat(getComputedStyle(body).paddingBottom), footBottom: foot.bottom, }; }""" @pytest.fixture(scope="module") def page(): from playwright.sync_api import sync_playwright css = _read(UI / "ui_template_data_table.css") + _read(B09 / "B09_Estimation_UI_Page.css") html = ( f"" "
" "
" ) js = _module_js() with sync_playwright() as p: browser = p.chromium.launch(channel="msedge", headless=True) pg = browser.new_page(viewport={"width": 1280, "height": 900}) pg.set_content(html) pg.add_script_tag( type="module", content=f"{js}\nwindow.dt = {{ createDataTable }};", ) pg.wait_for_function("!!window.dt") yield pg browser.close() @pytest.mark.parametrize("lines", [3, 2]) def test_줄_많음_표_창_끝_합계_줄_고정_안내_글_바닥(page, lines): out = page.evaluate(MEASURE, [300, lines, LONG]) assert out["hintH"] == pytest.approx(lines * out["lh"], abs=0.5), out # 높이 = 줄 수 × 줄 높이 assert out["hintScrolls"], out # 넘친 글 = 영역 안 스크롤 assert out["tableScrolls"], out assert out["page"] == out["vh"], out # 페이지 스크롤 0 assert out["hintBottom"] + out["padB"] == pytest.approx(out["vh"], abs=1.5), out # 창 끝까지 씀 assert out["hintTop"] == pytest.approx(out["tableBottom"] + 8, abs=0.5), out # 표 바로 아래 assert out["tableBottom"] - 30 < out["footBottom"] <= out["tableBottom"], ( out ) # 합계 줄 = 표 칸 바닥 def test_줄_수_2_이면_표가_한_줄만큼_더_김(page): three = page.evaluate(MEASURE, [300, 3, LONG]) two = page.evaluate(MEASURE, [300, 2, LONG]) assert two["maxH"] - three["maxH"] == pytest.approx(three["lh"], abs=1), (two, three) @pytest.mark.parametrize("lines", [3, 2]) def test_줄_적음_합계_줄_안내_글_끌려_올라옴(page, lines): out = page.evaluate(MEASURE, [3, lines, LONG]) assert not out["tableScrolls"], out assert out["tableBottom"] - out["tableTop"] < out["maxH"] - 100, out # 내용만큼(창 끝 아님) assert out["hintTop"] == pytest.approx(out["tableBottom"] + 8, abs=0.5), out # 표 끝에 붙음 assert out["hintBottom"] < out["vh"] - 300, out # 화면 바닥에 떨어져 있지 않음 assert out["hintH"] == pytest.approx(lines * out["lh"], abs=0.5), out assert out["footBottom"] <= out["tableBottom"], out def test_짧은_글은_한_줄만(page): out = page.evaluate(MEASURE, [300, 3, SHORT]) assert out["hintH"] == pytest.approx(out["lh"], abs=0.5), out assert not out["hintScrolls"], out assert out["page"] == out["vh"], out def test_spaceBelow_형제_바깥_여백_셈(): src = _read(UI / "ui_template_data_table.ts") assert "px(m.marginTop) + px(m.marginBottom)" in src # 브라우저 배치는 위 시험이 잼