fix(498-1 옛 페이지 풀림): 노선 [저장] 뒤 다시 세운 옛 페이지를 붙드는 전역 듣개 넷 · 코리도 캐시 — 요구 298-3
- 새 공용 ui_template_window_watch(listenWhileAlive) — 창 · 문서 듣개는 종류마다 하나 · 화면 노드는 약하게 쥠 · 노드가 버려지면 저절로 빠짐 - design_leave beforeunload · overlay_drag pointermove · pointerup · pointercancel · resize · Panel_Settings document pointerup · keyup · side_panels scroll · resize 를 노드 수명으로 - side_panels 크기 감시가 place 를 약하게 쥠(감시가 노드를 안 붙듦) - 코리도 모듈 캐시 = 지금 노선 한 칸 - 시험 test_298_3_page_dispose(다섯 번 다시 세움 뒤 창 듣개 수 그대로 · 옛 노드 풀림) · helper_346_4 새 모듈 읽기 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/* 298-3 옛 페이지 풀림 단위검증 헬퍼 — `ui_template_window_watch` · `ui_template_overlay_drag` ·
|
||||
* `design_leave` 를 그 자리에서 트랜스파일 · 페이지를 다섯 번 다시 세운 뒤 창 듣개 수 · 옛 노드 풀림.
|
||||
* `node --expose-gc` 로 부름 · 결과 JSON 을 stdout 으로.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
const session = new Map();
|
||||
const storage = {
|
||||
getItem: (key) => (session.has(key) ? session.get(key) : null),
|
||||
setItem: (key, value) => session.set(key, String(value)),
|
||||
removeItem: (key) => session.delete(key),
|
||||
key: (index) => [...session.keys()][index] ?? null,
|
||||
get length() {
|
||||
return session.size;
|
||||
},
|
||||
};
|
||||
const win = new EventTarget();
|
||||
const doc = new EventTarget();
|
||||
const counts = { window: 0, document: 0 };
|
||||
for (const [name, target] of [
|
||||
["window", win],
|
||||
["document", doc],
|
||||
]) {
|
||||
const add = target.addEventListener.bind(target);
|
||||
target.addEventListener = (...args) => {
|
||||
counts[name] += 1;
|
||||
return add(...args);
|
||||
};
|
||||
}
|
||||
Object.assign(win, {
|
||||
sessionStorage: storage,
|
||||
localStorage: storage,
|
||||
setTimeout,
|
||||
innerWidth: 1500,
|
||||
innerHeight: 900,
|
||||
});
|
||||
globalThis.window = win;
|
||||
globalThis.document = doc;
|
||||
globalThis.sessionStorage = storage;
|
||||
globalThis.localStorage = storage;
|
||||
globalThis.requestAnimationFrame = (fn) => (fn(), 1);
|
||||
|
||||
let guard = null;
|
||||
const STUBS = {
|
||||
"@config/config_frontend": {
|
||||
ROUTES: { B05_PROFILE: "/b05-profile", B06_SECTION: "/b06-section" },
|
||||
},
|
||||
"@ui/ui_template_locale": { t: (key) => key },
|
||||
"@ui/ui_template_leave_dialog": { leaveOrStay: async () => "stay" },
|
||||
"../router": { currentRoute: () => "/b05-profile", setLeaveGuard: (fn) => (guard = fn) },
|
||||
};
|
||||
|
||||
const loaded = new Map();
|
||||
function loadTs(file) {
|
||||
if (loaded.has(file)) return loaded.get(file);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
loaded.set(file, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (request in STUBS) return STUBS[request];
|
||||
if (request.startsWith("@ui/"))
|
||||
return loadTs(path.join(ROOT, "ui_template", `${request.slice(4)}.ts`));
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(file), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const drag = loadTs(path.join(ROOT, "ui_template", "ui_template_overlay_drag.ts"));
|
||||
const leave = loadTs(path.join(ROOT, "A00_Common", "design", "design_leave.ts"));
|
||||
|
||||
let settles = 0;
|
||||
function fakeElement() {
|
||||
const node = new EventTarget();
|
||||
node.style = {};
|
||||
node.isConnected = true;
|
||||
node.offsetWidth = 300;
|
||||
node.offsetHeight = 40;
|
||||
node.classList = {
|
||||
contains: () => false,
|
||||
add() {},
|
||||
toggle() {},
|
||||
remove: () => (settles += 1), // detach 가 부름 = 자리 다시 앉힘
|
||||
};
|
||||
node.querySelector = () => null;
|
||||
node.getBoundingClientRect = () => ({ left: 10, top: 80 });
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 페이지 한 벌 — 떠남 확인 + 진행단계 패널 · 옛 페이지 노드의 약한 손잡이를 돌려줌. */
|
||||
function mountPage() {
|
||||
const page = fakeElement();
|
||||
const panel = fakeElement();
|
||||
page.panel = panel; // 페이지 나무 = 패널을 품음
|
||||
leave.guardDesignLeave("p1", page, async () => {});
|
||||
page.dragHandle = drag.makePanelDraggable(panel, fakeElement(), "pos");
|
||||
return { page: new WeakRef(page), panel: new WeakRef(panel), keep: page };
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const out = {};
|
||||
let live = mountPage();
|
||||
out.afterFirst = { ...counts };
|
||||
const old = [];
|
||||
for (let index = 0; index < 4; index += 1) {
|
||||
live.keep.isConnected = false; // 다시 세움 — 옛 페이지는 화면에서 빠지고 버려짐
|
||||
old.push({ page: live.page, panel: live.panel });
|
||||
live = mountPage();
|
||||
}
|
||||
out.afterFive = { ...counts };
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
globalThis.gc();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
globalThis.gc();
|
||||
out.oldAlive = old.filter((pair) => pair.page.deref() || pair.panel.deref()).length;
|
||||
out.liveAlive = Boolean(live.page.deref() && live.panel.deref());
|
||||
// 살아 있는 패널은 창 크기 바뀜을 여전히 들음 · 버려진 패널은 안 부름
|
||||
settles = 0;
|
||||
win.dispatchEvent(new Event("resize"));
|
||||
out.resizeSettles = settles;
|
||||
// 떠남 확인 — 살아 있는 페이지만(고침 없음 = 안 막음)
|
||||
const event = new Event("beforeunload", { cancelable: true });
|
||||
win.dispatchEvent(event);
|
||||
out.unloadBlocked = event.defaultPrevented;
|
||||
out.guardSet = typeof guard === "function";
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})();
|
||||
@@ -66,6 +66,8 @@ function loadTs(file) {
|
||||
const box = { exports: {} };
|
||||
const localRequire = (request) => {
|
||||
if (request in STUBS) return STUBS[request];
|
||||
if (request === "@ui/ui_template_window_watch")
|
||||
return loadTs(path.join(ROOT, "ui_template", "ui_template_window_watch.ts"));
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(file), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""298-3 노선 [저장] 뒤 옛 페이지 풀림 — 페이지를 다시 세워도 창 · 문서 듣개 수 그대로 · 코리도 캐시 한 칸.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_298_3_page_dispose.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).resolve().parent / "helper_298_3_page_dispose.cjs"
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", "--expose-gc", str(HELPER)], capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_다시_세워도_창_듣개_수_그대로(out) -> None:
|
||||
assert (
|
||||
out["afterFirst"]["window"] == 5
|
||||
) # beforeunload · pointermove · pointerup · pointercancel · resize
|
||||
assert out["afterFive"] == out["afterFirst"]
|
||||
|
||||
|
||||
def test_옛_페이지_풀림_지금_페이지는_들음(out) -> None:
|
||||
assert out["oldAlive"] == 0
|
||||
assert out["liveAlive"] is True
|
||||
assert out["resizeSettles"] == 1 # 살아 있는 패널 하나만 다시 앉힘
|
||||
assert out["unloadBlocked"] is False and out["guardSet"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("rel", "types"),
|
||||
[
|
||||
("A00_Common/design/design_leave.ts", "beforeunload"),
|
||||
("ui_template/ui_template_overlay_drag.ts", "pointermove|pointerup|pointercancel|resize"),
|
||||
(
|
||||
"ui_template/ui_template_side_panels.ts",
|
||||
"scroll|resize",
|
||||
), # 끌기 동안 pointer 듣기는 끝에 뗌
|
||||
("B05_Profile/B05_Profile_UI_Panel_Settings.ts", "pointerup|keyup"),
|
||||
],
|
||||
)
|
||||
def test_전역_듣기는_노드_수명으로만(rel: str, types: str) -> None:
|
||||
code = _read(rel)
|
||||
assert not re.search(rf'\b(window|document)\.addEventListener\(\s*"({types})"', code)
|
||||
assert "listenWhileAlive(" in code
|
||||
|
||||
|
||||
def test_옆_패널_크기_감시는_노드를_안_붙듦() -> None:
|
||||
side = _read("ui_template/ui_template_side_panels.ts")
|
||||
assert "new ResizeObserver(() => ref.deref()?.())" in side
|
||||
assert "const ref = new WeakRef(place);" in side
|
||||
|
||||
|
||||
def test_코리도_캐시는_지금_노선_한_칸() -> None:
|
||||
code = _read("B05_Profile/B05_Profile_UI_Corridor.ts")
|
||||
assert (
|
||||
"function remember(key: string, entry: CacheEntry): void {\n cache.clear();\n cache.set(key, entry);\n}"
|
||||
in code
|
||||
)
|
||||
assert code.count("cache.set(") == 1
|
||||
Reference in New Issue
Block a user