"""200-2 B09 설계내역서 레벨 글 · 레벨 도구 줄 — 글 칸 왼쪽 맞춤 · 레벨 = 공종 · 명칭 칸 padding 들여쓰기 · 글꼴 = 공용 표 레벨 글꼴(228-1 · 레벨 · 묶음 따라) · 단추 넷(고른 줄 · 못 하면 disabled) · 키 셋(Tab · Shift+Tab · Alt+↑↓) · 편집 중엔 키 안 먹음. - 단추 · 키 = 우클릭 메뉴(billMenu) 같은 항목을 부름 — 줄 꼴 · 저장 꼴은 184-3 시험 그대로 - 표 · 문서 = 가짜 DOM(줄 tr · 글 칸 input) · 다시 그림 = buildBill + levelBar 다시 """ import json import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from test_184_3_b09_bill_menu import ROWS, _node, _ts # noqa: E402 ROOT = Path(__file__).resolve().parents[2] B09 = ROOT / "B09_Estimation" BAR_JS = """ class FakeEl { constructor(tag) { this.tagName = tag.toUpperCase(); this.kids = []; this.dataset = {}; this.cls = new Set(); this.on = {}; this.parent = null; this.isConnected = true; this.disabled = false; this.textContent = ""; } set className(v) { this.cls = new Set(v.split(" ").filter(Boolean)); } get classList() { const s = this.cls; return { add: (c) => s.add(c), remove: (c) => s.delete(c) }; } append(...kids) { for (const k of kids) { k.parent = this; this.kids.push(k); } } addEventListener(type, f) { (this.on[type] ??= []).push(f); } fire(type, ev) { for (const f of this.on[type] ?? []) f(ev); } contains(x) { for (let n = x; n; n = n.parent) if (n === this) return true; return false; } all() { return this.kids.flatMap((k) => [k, ...k.all()]); } closest() { for (let n = this; n; n = n.parent) if (n.tagName === "TR") return n; return null; } querySelectorAll() { return this.all().filter((n) => n.tagName === "TR" && n.cls.has("is-focus")); } querySelector(sel) { const i = sel.match(/data-i="(-?\\d+)"/)[1]; return this.all().find((n) => n.dataset.i === i) ?? null; } focus() { doc.activeElement = this; } blur() { doc.activeElement = doc.body; } } const doc = new FakeEl("#document"); doc.body = new FakeEl("body"); doc.activeElement = doc.body; doc.createElement = (t) => new FakeEl(t); globalThis.document = doc; const hovers = []; function createButton(o) { const b = new FakeEl("button"); b.label = o.label; b.variant = o.variant; b.addEventListener("click", o.onClick); return b; } function attachHoverInfo(el, text) { hovers.push([el.textContent, text]); } let user = {}; let table, root, bar; const draw = () => { table = buildBill({ sheets: { bill: { rows: __ROWS__, totals: null } }, payload: {}, get user() { return user; }, setUser: (u) => (user = u) }); if (root) root.isConnected = false; if (root?.contains(doc.activeElement)) doc.activeElement = doc.body; // 지운 요소 초점 = body root = new FakeEl("div"); table.rows.forEach((r, i) => { const tr = new FakeEl("tr"); tr.dataset.i = String(i); const td = new FakeEl("td"); const input = new FakeEl("input"); td.append(input); tr.append(td); root.append(tr); }); bar = levelBar(root, table); }; const at = (key) => table.rows.findIndex((r) => r.key === key); const tr = (key) => root.kids[at(key)]; const pick = (key) => root.fire("click", { target: tr(key).kids[0] }); const buttons = () => bar.tools; const off = () => buttons().map((b) => b.disabled); const order = () => (user.bill?.rows ?? []).map((e) => [e.key, e.parent ?? null]); const key = (k, more = {}) => { let stopped = false; doc.fire("keydown", { key: k, shiftKey: false, altKey: false, ...more, preventDefault: () => (stopped = true) }); return stopped; }; const out = {}; draw(); out.labels = buttons().map((b) => [b.label, b.dataset.b09Level, b.variant]); out.guide = bar.guide.textContent; out.hover = hovers.length; out.none = off(); pick("earth:4"); out.earth4 = off(); out.focus = [tr("earth:4").cls.has("is-focus"), pickedClass(table.rows[at("earth:4")]), pickedClass(table.rows[at("earth:5")])]; buttons()[3].fire("click"); out.down = order(); user = {}; draw(); out.kept = [off(), tr("earth:4").cls.has("is-focus"), pickedClass(table.rows[at("earth:4")])]; root.focus(); out.tabStop = key("Tab"); out.tabNoop = order(); out.shiftTab = [key("Tab", { shiftKey: true }), order()]; user = {}; draw(); pick("earth:5"); root.focus(); out.altUp = [key("ArrowUp", { altKey: true }), order()]; user = {}; draw(); pick("earth:4"); out.altDown = [key("ArrowDown", { altKey: true }), order()]; user = {}; draw(); pick("earth:4"); doc.activeElement = tr("earth:4").kids[0].kids[0]; out.editing = [key("Tab", { shiftKey: true }), key("ArrowDown", { altKey: true }), order()]; out.esc = [key("Escape"), doc.activeElement === root]; out.afterEsc = [key("Tab", { shiftKey: true }), order().length > 0]; user = {}; draw(); pick("earth:4"); doc.activeElement = new FakeEl("input"); out.outside = [key("Tab", { shiftKey: true }), order()]; root.focus(); key("Escape"); out.unpick = [off(), tr("earth:4").cls.has("is-focus"), key("Tab")]; out.keyAct = [ keyAct({ key: "Tab", shiftKey: false, altKey: false }, false), keyAct({ key: "Tab", shiftKey: true, altKey: false }, false), keyAct({ key: "ArrowUp", shiftKey: false, altKey: true }, false), keyAct({ key: "ArrowDown", shiftKey: false, altKey: true }, false), keyAct({ key: "Tab", shiftKey: false, altKey: false }, true), keyAct({ key: "ArrowUp", shiftKey: false, altKey: false }, false), keyAct({ key: "Tab", shiftKey: false, altKey: false, ctrlKey: true }, false), ]; console.log(JSON.stringify(out)); """ SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill", "UI_Tabs_Bill_Drag", "UI_Tabs_Bill_Level"] # fmt: skip IN, OUT, UP, DOWN = "들여쓰기 →", "내어쓰기 ←", "위로 ↑", "아래로 ↓" def _bar(): body = BAR_JS.replace("__ROWS__", json.dumps(ROWS, ensure_ascii=False)) return json.loads(_node(_ts(SRC) + body)) def _keys(order): return [k for k, _ in order] def test_단추_넷_고른_줄_못_하면_흐림(): out = _bar() assert out["labels"] == [["→ 들여쓰기", IN, "ghost"], ["← 내어쓰기", OUT, "ghost"], ["↑ 위로", UP, "ghost"], ["↓ 아래로", DOWN, "ghost"]] # fmt: skip assert ( "방향키" in out["guide"] and "더블클릭" in out["guide"] and "=" not in out["guide"] ) # 236-2 assert out["hover"] == 1 # 안내 한 줄 = 잘릴 때만 공용 호버(236-2) assert out["none"] == [True] * 4 # 고른 줄 없음 = 모두 흐림 # earth:4 = 1.1 첫 줄 — 들여(위 형제 없음) · 위로 = 못 함 · 내어 · 아래로 = 됨(184-3 메뉴와 같음) assert out["earth4"] == [True, False, True, False] assert out["focus"] == [True, "is-focus", None] # [↓ 아래로] = 메뉴 「아래로 ↓」 와 같은 결과 keys = _keys(out["down"]) assert keys.index("earth:5") + 1 == keys.index("earth:4") # 다시 그려도 고른 줄 · 단추 상태 남음 assert out["kept"] == [[True, False, True, False], True, "is-focus"] def test_키_셋_편집_중엔_안_먹음(): out = _bar() assert out["tabStop"] is True and out["tabNoop"] == [] # 못 하는 들여쓰기 = 바뀜 없음 stopped, order = out["shiftTab"] assert stopped and ("earth:4", "sec:1") in [tuple(x) for x in order] # 내어쓰기 for name in ("altUp", "altDown"): stopped, order = out[name] keys = _keys(order) assert stopped and keys.index("earth:5") + 1 == keys.index("earth:4"), name # 글 칸 편집 중 = 키 안 먹음(기본 동작 그대로) · Esc = 편집 끝(표에 초점) → 키 먹음 assert out["editing"] == [False, False, []] assert out["esc"] == [False, True] assert out["afterEsc"] == [True, True] # 초점이 표 · 도구 줄 밖 = 안 먹음 · Esc(편집 아님) = 고름 풂 → 흐림 · Tab 은 기본 동작 assert out["outside"] == [False, []] assert out["unpick"] == [[True] * 4, False, False] # 그냥 ↑ = 옮기기(204 방향키 · test_204_1) assert out["keyAct"] == [IN, OUT, UP, DOWN, None, UP, None] def test_왼쪽_맞춤_레벨_들여쓰기_글꼴_공용_표(): css = (B09 / "B09_Estimation_UI_Page.css").read_text("utf-8") bill = '.b09-estimation [data-b09-table="bill"]' # 레벨 들여쓰기 · 글꼴 = 공용 표 level 옵션(test_201_1 · test_228_1) · 페이지 CSS 에 따로 두지 않음 assert "is-lv" not in css and "font-size: 13px" not in css common = (ROOT / "ui_template" / "ui_template_data_table.css").read_text("utf-8") assert "td.ui-data-table__lv.is-lv1.is-node" in common src = (B09 / "B09_Estimation_UI_Tabs_Bill.ts").read_text("utf-8") assert "of: (i) => rows[i]?.level as number | undefined," in src assert 'group: (i) => rows[i]?.kind !== "item",' in src # 글 칸 왼쪽 맞춤(수량만 오른쪽) assert re.search( re.escape(f'{bill} .ui-data-table__input:not([data-k="qty"]) {{') + r"\s*text-align: left;", css, ) assert ".b09-estimation__level" not in css # 229-1 표 위 도구 줄 없앰 def test_셸_배선(): src = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8") assert ( "bill ? (i) => pickedClass(bill.rows[i]) ?? pickClass(jumpRange(), i)" in src ) # 239-1 · 242-1 이동 줄 빨강 곁(고름과 한때 하나) assert "if (level) bars.setTools(level.tools);" in src # 229-1 단추 = 보조 탭 줄 assert ( "panel.replaceChildren(table.root, ...(foot ? [foot] : []))" in src ) # 203 레벨 안내 + 탭 안내 = 한 영역 lvl = (B09 / "B09_Estimation_UI_Tabs_Bill_Level.ts").read_text("utf-8") assert "table.menu(index)" in lvl # 단추 · 키 = 우클릭 메뉴 같은 함수 assert "b09-estimation__level" not in lvl