197 lines
9.0 KiB
Python
197 lines
9.0 KiB
Python
"""200-2 B09 설계내역서 레벨 글 · 레벨 도구 줄 — 글 칸 왼쪽 맞춤 · 레벨 = 공종 · 명칭 칸 padding 들여쓰기 ·
|
|
글자 크기 한 벌 · 단추 넷(고른 줄 · 못 하면 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.kids.filter((k) => k.tagName === "BUTTON");
|
|
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.kids.at(-1).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 "Tab" in out["guide"] and "Shift+Tab" in out["guide"] and "Alt+↑↓" in out["guide"]
|
|
assert out["hover"] == 1 # 안내 한 줄 = 공용 호버로 자세히
|
|
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"]'
|
|
# 201-1 레벨 들여쓰기 = 공용 표 level 옵션(test_201_1) · 레벨별 글자 크기 규칙 없음(한 벌)
|
|
assert "is-lv" not in css and "font-size: 13px" not in css
|
|
src = (B09 / "B09_Estimation_UI_Tabs_Bill.ts").read_text("utf-8")
|
|
assert 'level: { key: "name", of: (i) => rows[i]?.level' in src
|
|
# 글 칸 왼쪽 맞춤(수량만 오른쪽)
|
|
assert re.search(
|
|
re.escape(f'{bill} .ui-data-table__input:not([data-k="qty"]) {{') + r"\s*text-align: left;",
|
|
css,
|
|
)
|
|
assert re.search(r"\.b09-estimation__level \{\s*display: flex;", css)
|
|
|
|
|
|
def test_셸_배선():
|
|
src = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
|
assert "makeTable(bill ? (i) => pickedClass(bill.rows[i]) : undefined)" in src
|
|
assert "...(level ? [level] : []), table.root" in src
|
|
lvl = (B09 / "B09_Estimation_UI_Tabs_Bill_Level.ts").read_text("utf-8")
|
|
assert "table.menu(index)" in lvl # 단추 · 키 = 우클릭 메뉴 같은 함수
|