Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_2

This commit is contained in:
2026-10-26 09:54:00 +09:00
4 changed files with 191 additions and 0 deletions
@@ -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),
@@ -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 }));
@@ -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