feat(480 드롭다운): 마우스로 고른 뒤 한 박자 뒤 초점 뺌 — 키보드 고름 그대로 · 공용 진입 · CAD iframe · 종단 새 창 세 자리

- ui_template_native_menu.ts releaseSelectAfterPick — 창 캡처 pointerdown · keydown · change · 아직 그 select 에 초점일 때만 blur
- 시험 test_280_6_select_release 새로 · 옛 가짜 import 두 헬퍼(282-1 · 224-1c)에 새 함수 흉내

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 22:07:40 +09:00
co-authored by Claude Opus 5.5
parent afe6e4a799
commit 9a8ff3ee86
8 changed files with 163 additions and 7 deletions
@@ -117,7 +117,7 @@ const js = ts.transpileModule(read("A00_Common", "design", "design_profile_popou
const mod = {};
new Function("exports", "require", "window", "document", js)(
mod,
() => ({ showToast() {} }),
() => ({ showToast() {}, releaseSelectAfterPick() {} }),
main,
mainDoc,
);
@@ -0,0 +1,76 @@
/* 280-6 드롭다운 초점 빼기(`ui_template/ui_template_native_menu.ts` `releaseSelectAfterPick`) 단위검증 헬퍼 —
* 그 자리에서 트랜스파일해 Node 로 돌리고 값을 JSON 으로 냄. 가짜 창 · 가짜 select · 가짜 타이머로 판정. */
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const js = ts.transpileModule(
fs.readFileSync(path.join(ROOT, "ui_template", "ui_template_native_menu.ts"), "utf8"),
{ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } },
).outputText;
const box = { exports: {} };
new Function("exports", "module", "require", js)(box.exports, box, require);
const m = box.exports;
// 가짜 창 — 캡처 듣기 · 타이머(손으로 돌림) · 문서 하나
function setup() {
const listeners = [];
const timers = [];
const doc = { activeElement: null };
const win = {
addEventListener: (type, fn, capture) => listeners.push({ type, fn, capture }),
setTimeout: (fn) => timers.push(fn),
};
m.releaseSelectAfterPick(win);
const el = (tagName) => {
const node = { tagName, ownerDocument: doc, blurs: 0 };
node.blur = () => {
node.blurs += 1;
doc.activeElement = null;
};
return node;
};
const fire = (type, target) =>
listeners.filter((l) => l.type === type).forEach((l) => l.fn({ type, target }));
const tick = () => timers.splice(0).forEach((fn) => fn());
return { listeners, doc, el, fire, tick };
}
// 차례 하나 — 초점을 select 에 두고 입력들을 쏜 뒤 change · (처리) · 한 틱
function run(inputs, tag = "SELECT", moveFocus = false) {
const t = setup();
const target = t.el(tag);
t.doc.activeElement = target;
inputs.forEach((type) => t.fire(type, target));
t.fire("change", target);
const before = target.blurs; // 한 틱 전엔 안 뺌
if (moveFocus) t.doc.activeElement = t.el("INPUT");
t.tick();
return { before, blurs: target.blurs };
}
const src = (rel) => fs.readFileSync(path.join(ROOT, rel), "utf8");
const setupOnce = setup();
console.log(
JSON.stringify({
mouse: run(["pointerdown"]),
keyboard: run(["keydown"]),
none: run([]),
input: run(["pointerdown"], "INPUT"),
moved: run(["pointerdown"], "SELECT", true),
mouseThenKey: run(["pointerdown", "keydown"]),
keyThenMouse: run(["keydown", "pointerdown"]),
listeners: setupOnce.listeners.map((l) => [l.type, l.capture]),
wiring: {
main: /releaseSelectAfterPick\(window\)/.test(src("A00_Common/main.ts")),
cad: /releaseSelectAfterPick\(frame\.contentWindow\)/.test(
src("ui_template/cad_host/cad_host.ts"),
),
popout: /releaseSelectAfterPick\(next\)/.test(
src("A00_Common/design/design_profile_popout.ts"),
),
},
}),
);
@@ -134,7 +134,7 @@ const { withToolLead } = load(`${leadSrc}\nexports.withToolLead = withToolLead;`
// ---- 진짜 attachProfilePopout ----
const { attachProfilePopout } = load(read("A00_Common", "design", "design_profile_popout.ts"), {
require: () => ({ showToast() {} }),
require: () => ({ showToast() {}, releaseSelectAfterPick() {} }),
document,
window,
getComputedStyle: () => ({ backgroundColor: "" }),
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
"""280-6 드롭다운 — 마우스로 고르면 한 박자 뒤 초점 뺌 · 키보드로 고르면 그대로 · 부르는 자리 셋.
Node 헬퍼(`helper_280_6_select_release.cjs`)가 `ui_template/ui_template_native_menu.ts` 를 트랜스파일해
가짜 창 · 가짜 select · 가짜 타이머로 돌리고, 여기서 값을 판정함.
"""
import json
import os
import subprocess
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
@pytest.fixture(scope="module")
def r():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_280_6_select_release.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_마우스로_고르면_한_박자_뒤_초점_뺌(r):
assert r["mouse"] == {"before": 0, "blurs": 1}
assert r["keyThenMouse"]["blurs"] == 1
assert r["none"]["blurs"] == 1 # 입력 없이 온 change(스크립트 · 손가락)도 뺌
def test_키보드로_고르면_초점_남음(r):
assert r["keyboard"]["blurs"] == 0
assert r["mouseThenKey"]["blurs"] == 0 # 마우스로 펼친 뒤 ↑↓ + Enter
def test_select_밖_이미_옮긴_초점은_안_건드림(r):
assert r["input"]["blurs"] == 0
assert r["moved"]["blurs"] == 0
def test_창_캡처_듣기와_부르는_자리_셋(r):
assert sorted(r["listeners"]) == [["change", True], ["keydown", True], ["pointerdown", True]]
assert r["wiring"] == {"main": True, "cad": True, "popout": True}