"""저장 안 한 고침 3단추 물음(PLAN 86-1 · `ui_template_leave_dialog.ts`) — Node 로 TS 를 돌려 판정. 못박는 것 - 단추 순서 [취소] [버리고 넘어감] [저장 후 넘어감] · 기본 초점 = 취소 · Esc = 취소. - 저장 후 넘어감: 저장 성공 + 고침 없음 = saved / 저장 던짐 · 고침 남음 = stay(머묾). - 버리고 넘어감: discard 갈고리 호출 · 취소: 아무것도 안 부름. - M02 자리(UI_Main · Structure · ItemCards)가 이 물음을 씀. """ import json import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] M02 = ROOT / "M02_MasterTemplete" SCRIPT = r""" const fs = require("fs"); const ts = require(process.argv[1] + "/config/node_modules/typescript"); const src = fs.readFileSync(process.argv[1] + "/ui_template/ui_template_leave_dialog.ts", "utf8"); const js = ts.transpileModule(src, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; let focused = null; let overlay = null; let buttons = []; const node = (tag, o = {}) => ({ tag, ...o, kids: [], on: {}, append(...x) { this.kids.push(...x); }, addEventListener(k, f) { this.on[k] = f; }, remove() { overlay = null; }, }); const stubs = { "./ui_template_elements": { el: node, createButton: (o) => { const b = node("button", { label: o.label, variant: o.variant }); b.focus = () => { focused = b.label; }; b.click = o.onClick; buttons.push(b); return b; }, }, "./ui_template_locale": { t: (k) => k }, }; globalThis.document = { activeElement: null, body: { append(x) { overlay = x; } } }; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, (n) => stubs[n] ?? require(n)); const { leaveOrStay } = box.exports; const run = async (pick, hooks) => { buttons = []; focused = null; const calls = []; const wrap = { save: async () => { calls.push("save"); if (hooks.throws) throw new Error("x"); }, stillDirty: () => !!hooks.left, discard: () => calls.push("discard"), }; const p = leaveOrStay("msg", wrap); const labels = buttons.map((b) => b.label); const first = focused; if (pick === "esc") overlay.on.keydown({ key: "Escape", stopPropagation() {} }); else buttons[pick].click(); return { got: await p, calls, labels, first }; }; (async () => { const out = { cancel: await run(0, {}), esc: await run("esc", {}), discard: await run(1, {}), save: await run(2, {}), saveThrows: await run(2, { throws: true }), saveLeft: await run(2, { left: true }), }; process.stdout.write(JSON.stringify(out)); })(); """ @pytest.fixture(scope="module") def got() -> dict: done = subprocess.run( ["node", "-e", SCRIPT, str(ROOT)], capture_output=True, text=True, encoding="utf-8", check=True, ) return json.loads(done.stdout) def test_단추_순서_기본_초점_취소(got): row = got["cancel"] assert row["labels"] == ["M02_TreeCancel", "M02_TreeDiscardGo", "M02_TreeSaveGo"] assert row["first"] == "M02_TreeCancel" def test_취소_Esc는_머묾(got): assert got["cancel"]["got"] == "stay" and got["cancel"]["calls"] == [] assert got["esc"]["got"] == "stay" and got["esc"]["calls"] == [] def test_버리고_넘어감(got): assert got["discard"]["got"] == "discarded" assert got["discard"]["calls"] == ["discard"] def test_저장_후_넘어감과_실패_머묾(got): assert got["save"] == {**got["save"], "got": "saved", "calls": ["save"]} assert got["saveThrows"]["got"] == "stay" # 저장 던짐 assert got["saveLeft"]["got"] == "stay" # 저장 뒤에도 고침이 남음(카드 저장 실패 등) def test_M02_자리가_3단추_물음을_씀(): main = (M02 / "M02_MasterTemplete_UI_Main.ts").read_text(encoding="utf-8") assert "leaveOrStay(message, { save: doSave, stillDirty: busy" in main assert "askSettle" not in main assert "!(await showConfirmDialog(L(" not in main cards = (M02 / "M02_MasterTemplete_ItemCards.ts").read_text(encoding="utf-8") assert "leaveOrStay(TEXT.discard" in cards struct = (M02 / "M02_MasterTemplete_Structure.ts").read_text(encoding="utf-8") assert struct.count("leaveOrStay(TEXT.discardCards") == 2