feat(common): B · M 페이지 브라우저 기본 오른쪽 클릭 메뉴 막기 — 공용 진입 창 캡처 한 곳 · CAD iframe 도 · 글 입력 칸은 살림
- 새 ui_template_native_menu.ts — 주소 /^[bm]\d/ · 글 입력 칸(input · textarea · contenteditable · 읽기 전용 포함) 제외 · preventDefault 만(우리 메뉴 그대로) - main.ts 공용 진입 한 줄 · cad_host.ts iframe load 때 같은 함수 - 「브라우저 메뉴 그대로 둔다」 주석 셋 바로잡음(B04 지도 · B05 배수 · 3D) - 시험 test_common_native_menu.py (PLAN 39-9a) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
/* 브라우저 기본 메뉴 막기(`ui_template/ui_template_native_menu.ts`) 단위검증 헬퍼 —
|
||||
* 그 자리에서 트랜스파일해 Node 로 돌리고 값을 JSON 으로 냄. 가짜 창 · 가짜 요소로 판정. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const file = path.join(__dirname, "..", "..", "ui_template", "ui_template_native_menu.ts");
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "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;
|
||||
|
||||
const targets = {
|
||||
div: { tagName: "DIV" },
|
||||
canvas: { tagName: "CANVAS" },
|
||||
input: { tagName: "INPUT", type: "text" },
|
||||
inputNoType: { tagName: "INPUT", type: "" },
|
||||
number: { tagName: "INPUT", type: "number" },
|
||||
readonly: { tagName: "INPUT", type: "text", readOnly: true },
|
||||
checkbox: { tagName: "INPUT", type: "checkbox" },
|
||||
range: { tagName: "INPUT", type: "range" },
|
||||
textarea: { tagName: "TEXTAREA" },
|
||||
editable: { tagName: "SPAN", isContentEditable: true },
|
||||
textNode: { nodeType: 3 },
|
||||
none: null,
|
||||
};
|
||||
|
||||
const editable = {};
|
||||
for (const [k, t] of Object.entries(targets)) editable[k] = m.isTextEditable(t);
|
||||
|
||||
// 가짜 창 — 캡처 듣기 하나를 받아 둠
|
||||
function fakeWindow() {
|
||||
const win = { calls: [] };
|
||||
win.addEventListener = (type, fn, capture) => win.calls.push({ type, fn, capture });
|
||||
return win;
|
||||
}
|
||||
function fire(win, target) {
|
||||
const ev = { target, defaultPrevented: false, stopped: false };
|
||||
ev.preventDefault = () => (ev.defaultPrevented = true);
|
||||
ev.stopPropagation = () => (ev.stopped = true);
|
||||
win.calls.forEach((c) => c.fn(ev));
|
||||
return ev;
|
||||
}
|
||||
|
||||
let on = true;
|
||||
const win = fakeWindow();
|
||||
m.blockNativeMenu(win, () => on);
|
||||
const blocked = {};
|
||||
for (const k of ["div", "canvas", "input", "readonly", "textarea", "editable", "checkbox"])
|
||||
blocked[k] = fire(win, targets[k]).defaultPrevented;
|
||||
on = false;
|
||||
const off = fire(win, targets.div).defaultPrevented;
|
||||
const stopped = fire(win, targets.div).stopped;
|
||||
|
||||
const route = (r) => m.NATIVE_MENU_ROUTE.test(r);
|
||||
const routes = {};
|
||||
for (const r of ["a01-home", "a06-login", "b01-account", "b05-profile", "b11-loading", "m01-master-data", "m02-master-template", "bogus"])
|
||||
routes[r] = route(r);
|
||||
|
||||
console.log(JSON.stringify({ editable, blocked, off, stopped, listener: win.calls.map((c) => [c.type, c.capture]), routes }));
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""브라우저 기본 오른쪽 클릭 메뉴 막기(PLAN 39-9a) 회귀검증 — 글 입력 칸 판정 · 막기 · 꺼짐 · 대상 주소.
|
||||
|
||||
Node 헬퍼(`helper_common_native_menu.cjs`)가 `ui_template/ui_template_native_menu.ts` 를 트랜스파일해
|
||||
가짜 창 · 가짜 요소로 돌리고, 여기서 값을 판정함.
|
||||
"""
|
||||
|
||||
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_common_native_menu.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):
|
||||
e = results["editable"]
|
||||
assert e["input"] and e["inputNoType"] and e["number"] and e["readonly"]
|
||||
assert e["textarea"] and e["editable"]
|
||||
assert not (e["div"] or e["canvas"] or e["checkbox"] or e["range"])
|
||||
assert not (e["textNode"] or e["none"])
|
||||
|
||||
|
||||
def test_입력_칸_밖은_막음(results):
|
||||
b = results["blocked"]
|
||||
assert b["div"] and b["canvas"] and b["checkbox"]
|
||||
assert not (b["input"] or b["readonly"] or b["textarea"] or b["editable"])
|
||||
|
||||
|
||||
def test_꺼지면_안_막고_이벤트는_안_멈춤(results):
|
||||
assert results["off"] is False
|
||||
assert results["stopped"] is False # 우리 메뉴 듣기가 그대로 돌아야 함
|
||||
assert results["listener"] == [["contextmenu", True]] # 창 캡처 단계
|
||||
|
||||
|
||||
def test_대상_주소는_B_M_만(results):
|
||||
r = results["routes"]
|
||||
assert r["b01-account"] and r["b05-profile"] and r["b11-loading"]
|
||||
assert r["m01-master-data"] and r["m02-master-template"]
|
||||
assert not (r["a01-home"] or r["a06-login"] or r["bogus"])
|
||||
Reference in New Issue
Block a user