Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
186 lines
7.9 KiB
Python
186 lines
7.9 KiB
Python
"""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"<!doctype html><html><head><style>{TOKENS}{css}</style></head><body>"
|
||
"<div class='b09-estimation'><div class='b09-estimation__tabs'><button>탭</button></div>"
|
||
"<div class='b09-estimation__panel'></div></div></body></html>"
|
||
)
|
||
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 # 브라우저 배치는 위 시험이 잼
|