diff --git a/resources/tester/helper_280_2_step_nav.cjs b/resources/tester/helper_280_2_step_nav.cjs index 92ed1250a..01d9efb6d 100644 --- a/resources/tester/helper_280_2_step_nav.cjs +++ b/resources/tester/helper_280_2_step_nav.cjs @@ -123,6 +123,7 @@ const STUBS = { }, "./ui_template_locale": { t: (key) => key }, "./ui_template_modal": { openModal: () => undefined }, + "./ui_template_help_edit": { attachHelpEditing: () => ({ flush: () => undefined }) }, "./ui_template_elements": { showToast: (message) => toasts.push(message), el: (tag) => new FakeNode(tag), diff --git a/resources/tester/helper_359_2_late_actions.cjs b/resources/tester/helper_359_2_late_actions.cjs new file mode 100644 index 000000000..2d9f78f8a --- /dev/null +++ b/resources/tester/helper_359_2_late_actions.cjs @@ -0,0 +1,141 @@ +/* 요구 359-2 · 360-2 — 좌측 패널 [?] 자리. `ui_template_overlay.ts` 를 트랜스파일해 가짜 DOM 위에서 + * 액션 줄을 ① 패널 세우기 전(E05 꼴) ② 세운 뒤(E03 · E04 · E06 · E07 꼴)에 붙여 쪼갬 · [?] 를 JSON 한 벌로 냄. + */ +const path = require("path"); +const { makeLoader } = require("./helper_ts_stub_loader.cjs"); + +const ROOT = path.join(__dirname, "..", ".."); + +class FakeNode { + constructor(tag) { + this.tagName = tag; + this.children = []; + this.parent = null; + this.style = {}; + this._classes = new Set(); + const self = this; + this.classList = { + add: (...n) => n.forEach((x) => self._classes.add(x)), + remove: (...n) => n.forEach((x) => self._classes.delete(x)), + toggle: (n, f) => + (f === undefined ? !self._classes.has(n) : !!f) + ? self._classes.add(n) + : self._classes.delete(n), + contains: (n) => self._classes.has(n), + }; + } + set className(v) { + this._classes = new Set(String(v).split(/\s+/).filter(Boolean)); + } + get className() { + return [...this._classes].join(" "); + } + get parentElement() { + return this.parent; + } + get firstChild() { + return this.children[0] ?? null; + } + get childElementCount() { + return this.children.length; + } + _adopt(kid) { + if (kid.parent) kid.parent.children = kid.parent.children.filter((c) => c !== kid); + kid.parent = this; + return kid; + } + append(...kids) { + kids.forEach((k) => this.children.push(this._adopt(k))); + mutated(); + } + prepend(...kids) { + this.children.unshift(...kids.map((k) => this._adopt(k))); + mutated(); + } + setAttribute() {} + addEventListener() {} + querySelector(selector) { + const cls = selector.slice(1); + let hit = null; + const walk = (n) => + n.children.forEach((k) => { + if (!hit && k._classes.has(cls)) hit = k; + if (!hit) walk(k); + }); + walk(this); + return hit; + } +} +// 진짜 MutationObserver 처럼 바뀐 뒤 한꺼번에 — `flush()` 가 부름(지켜보는 것만 · 끊으면 안 부름) +const observers = new Set(); +let dirty = false; +function mutated() { + dirty = true; +} +function flush() { + while (dirty) { + dirty = false; + [...observers].forEach((o) => o.cb()); + } +} +globalThis.MutationObserver = class { + constructor(cb) { + this.cb = cb; + } + observe() { + observers.add(this); + } + disconnect() { + observers.delete(this); + } +}; +globalThis.document = { createElement: (tag) => new FakeNode(tag) }; +globalThis.window = { innerWidth: 1600 }; + +const { loadTs } = makeLoader({ + root: ROOT, + stubs: { + "./ui_template_locale": { t: (key) => key }, + "./ui_template_help": { + placeHelpButton: (actions) => { + const button = new FakeNode("button"); + button.className = "ui-help-btn"; + actions.prepend(button); + }, + }, + "./ui_template_overlay_drag": { makePanelDraggable: () => ({ refresh() {} }) }, + "/b_workflow_nav": { fetchWorkflowState: async () => ({}) }, + "/b_page_state": { readByKey: () => null, writeByKey: () => undefined }, + "/device_context": { SIZE_BREAKS: { expanded: 840 } }, + }, +}); +const overlay = loadTs(path.join(ROOT, "ui_template", "ui_template_overlay.ts")); + +function mount(late) { + const panel = new FakeNode("div"); + const actions = new FakeNode("div"); + actions.className = "ui-sidebar-actions"; + panel.append(new FakeNode("p")); + if (!late) panel.append(actions); + const { root } = overlay.createWorkflowOverlays({ + title: "t", + optionsContent: panel, + showProjectName: false, + }); + const before = !!actions.querySelector(".ui-help-btn"); + if (late) panel.append(actions); + flush(); + const body = root.querySelector(".ui-workflow-overlay__body"); + return { + before, + help: actions.querySelector(".ui-help-btn") ? 1 : 0, + split: body.classList.contains("ui-sidebar-split"), + scroll: !!panel.querySelector(".ui-sidebar-scroll"), + lastIsActions: panel.children[panel.children.length - 1] === actions, + watching: observers.size, + }; +} + +const early = mount(false); +const late = mount(true); +process.stdout.write(JSON.stringify({ early, late })); diff --git a/resources/tester/test_359_2_help_late_actions.py b/resources/tester/test_359_2_help_late_actions.py new file mode 100644 index 000000000..8ba004403 --- /dev/null +++ b/resources/tester/test_359_2_help_late_actions.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +"""요구 359-2 · 360-2 — 좌측 패널 아래 [?] 는 액션 줄을 언제 붙여도 한 벌. + +① 패널 세우기 전에 붙인 줄(E05 꼴) = 바로 쪼갬 · [?] +② 세운 뒤에 붙인 줄(E03 · E04 · E06 · E07 꼴) = 붙는 때 쪼갬 · [?] · 지켜보기 끊음 +""" + +import json +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture(scope="module") +def results(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_359_2_late_actions.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_먼저_붙인_줄은_바로_물음(results): + early = results["early"] + assert early["before"] is True + assert early["help"] == 1 and early["split"] and early["scroll"] and early["lastIsActions"] + assert early["watching"] == 0 + + +def test_늦게_붙인_줄도_물음_한_벌(results): + late = results["late"] + assert late["before"] is False + assert late["help"] == 1 and late["split"] and late["scroll"] and late["lastIsActions"] + assert late["watching"] == 0 diff --git a/ui_template/ui_template_overlay.ts b/ui_template/ui_template_overlay.ts index bc3f46928..f102253af 100644 --- a/ui_template/ui_template_overlay.ts +++ b/ui_template/ui_template_overlay.ts @@ -201,6 +201,14 @@ function createPanel( if (body.childElementCount > 0) { body.classList.add("ui-workflow-overlay__body"); if (isSidebar) splitSidebarActions(body); + // 액션 줄을 패널을 세운 뒤에 붙이는 화면(E03 · E04 · E06 · E07) — 붙는 때 쪼갬 · [?] 자리 + if (isSidebar && !body.classList.contains("ui-sidebar-split")) { + const watch = new MutationObserver(() => { + splitSidebarActions(body); + if (body.classList.contains("ui-sidebar-split")) watch.disconnect(); + }); + watch.observe(body, { childList: true, subtree: true }); + } root.append(body); }