"""346-4 되돌리기 앞일 — 사진 대상 = aislo:draft: 전부 · 누름 중간은 안 찍고 놓을 때 한 번 · 크기. 349 — 열기만(손길 없음) = 걸음 0 · 열 때 쓰는 초안은 첫 사진 · 조작 한 번 = 걸음 하나. ./venv/Scripts/python.exe -m pytest resources/tester/test_346_4_profile_history.py -q """ from __future__ import annotations import json import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] TS_FILE = ROOT / "B05_Profile" / "B05_Profile_UI_Profile_History.ts" TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" DRIVER = r""" import { writeFileSync } from "node:fs"; // 세션 흉내 — getItem 은 진짜처럼 매번 새 글을 돌려줌(사진끼리 글을 같이 쓰는지 재려고). const data = new Map(); globalThis.sessionStorage = { get length() { return data.size; }, key: (i) => [...data.keys()][i] ?? null, getItem: (k) => (data.has(k) ? Buffer.from(data.get(k)).toString() : null), setItem: (k, v) => data.set(k, String(v)), removeItem: (k) => data.delete(k), }; globalThis.window = new EventTarget(); window.setTimeout = setTimeout; const { createProfileHistory } = await import("./B05_Profile_UI_Profile_History.js"); const tick = () => new Promise((r) => setTimeout(r, 5)); const out = {}; // 1. 대상 — 초안 통 전부 + 옛 키 · 보기 설정 · pref · result 는 뺌 const K = { route: "aislo:draft:route-draft:p1", cross: "aislo:draft:crossdesign:p1:7", pipes: "aislo:draft:pipes:p1", legacy: "b05:uphill:p1", layout: "b05-panel-collapsed", pref: "aislo:pref:table-open", result: "aislo:result:latest:p1", }; for (const k of Object.values(K)) sessionStorage.setItem(k, "0"); let restored = 0; const h = createProfileHistory(() => (restored += 1)); window.dispatchEvent(new Event("keydown")); // 손길 — 열 때가 끝남 for (const k of Object.values(K)) sessionStorage.setItem(k, "1"); h.record(); out.undo = h.undo(); out.after = Object.fromEntries(Object.entries(K).map(([n, k]) => [n, sessionStorage.getItem(k)])); out.restored = restored; // 2. 누르고 있는 동안 기록 미룸 · 놓으면 한 번 data.clear(); sessionStorage.setItem(K.cross, "a"); const h2 = createProfileHistory(() => {}); window.dispatchEvent(new Event("pointerdown")); for (const v of ["b", "c", "d", "e"]) { sessionStorage.setItem(K.cross, v); h2.record(); } out.heldCanUndo = h2.canUndo(); window.dispatchEvent(new Event("pointerup")); await tick(); out.releasedCanUndo = h2.canUndo(); h2.undo(); out.oneUndo = sessionStorage.getItem(K.cross); out.noMoreUndo = !h2.canUndo(); // 3. 크기 — 300 KB 칸 셋 · 작은 칸만 바꾸며 100단 data.clear(); const big = (c) => c.repeat(300 * 1024); sessionStorage.setItem(K.route, big("r")); sessionStorage.setItem(K.cross, big("x")); sessionStorage.setItem(K.pipes, big("p")); globalThis.gc(); const before = process.memoryUsage().heapUsed; const h3 = createProfileHistory(() => {}); window.dispatchEvent(new Event("keydown")); for (let i = 0; i < 100; i += 1) { sessionStorage.setItem("aislo:draft:alignment-draft:p1:7", JSON.stringify({ i })); h3.record(); } globalThis.gc(); out.heapMb = (process.memoryUsage().heapUsed - before) / 1024 / 1024; let steps = 0; while (h3.undo()) steps += 1; out.steps = steps; // 4. 열기만 — 메뉴 누름(세우기 전) · 열 때 초안 쓰기 · 되살림 찍기 · 놓은 뒤 기다림 = 걸음 0 data.clear(); sessionStorage.setItem(K.cross, "saved"); window.dispatchEvent(new Event("pointerdown")); const h4 = createProfileHistory(() => {}); sessionStorage.setItem("aislo:draft:std-cross:p1", "std"); sessionStorage.setItem(K.cross, "opened"); sessionStorage.setItem(K.route, "kept"); h4.record(); window.dispatchEvent(new Event("pointerup")); await new Promise((r) => setTimeout(r, 450)); out.openCanUndo = h4.canUndo(); // 조작 한 번 — 누름 · 계획고 고침 · 놓음 = 걸음 하나 · 되돌리면 열 때 모습 window.dispatchEvent(new Event("pointerdown")); sessionStorage.setItem("aislo:draft:alignment-draft:p1:7", "1"); window.dispatchEvent(new Event("pointerup")); await new Promise((r) => setTimeout(r, 450)); out.editCanUndo = h4.canUndo(); h4.undo(); out.openState = { grade: sessionStorage.getItem("aislo:draft:alignment-draft:p1:7"), cross: sessionStorage.getItem(K.cross), route: sessionStorage.getItem(K.route), std: sessionStorage.getItem("aislo:draft:std-cross:p1"), }; out.oneStep = !h4.canUndo(); writeFileSync(process.argv[2], JSON.stringify(out)); """ @pytest.fixture(scope="module") def result(tmp_path_factory: pytest.TempPathFactory) -> dict: out = tmp_path_factory.mktemp("ts_history") subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", str(TSC), str(TS_FILE), "--outDir", str(out), "--module", "esnext", "--target", "es2022", "--moduleResolution", "bundler", "--ignoreConfig", "--noCheck", "--noResolve"], cwd=str(ROOT), check=True, capture_output=True, ) # fmt: skip (out / "driver.mjs").write_text(DRIVER, encoding="utf-8") run = subprocess.run( # noqa: S603 ["node", "--expose-gc", str(out / "driver.mjs"), str(out / "out.json")], capture_output=True, text=True, cwd=str(out), ) # fmt: skip assert run.returncode == 0, run.stderr return json.loads((out / "out.json").read_text(encoding="utf-8")) def test_초안_통_전부를_되돌림(result) -> None: after = result["after"] assert result["undo"] and result["restored"] == 1 assert after["route"] == after["cross"] == after["pipes"] == after["legacy"] == "0" # 보기 설정 · pref · result 는 사진 밖 — 되돌리기가 안 건드림 assert after["layout"] == after["pref"] == after["result"] == "1" def test_누름_중간은_안_찍고_놓을_때_한_번(result) -> None: assert result["heldCanUndo"] is False assert result["releasedCanUndo"] is True assert result["oneUndo"] == "a" and result["noMoreUndo"] def test_큰_칸_100단도_바뀐_칸만_늚(result) -> None: assert result["steps"] == 99 # 한도 100(첫 사진 포함) # 같은 글을 같이 쓰지 않으면 900 KB × 100 ≈ 90 MB(UTF-8 1바이트 글 기준 · 실제는 더) assert result["heapMb"] < 10, result["heapMb"] def test_열기만은_걸음_0(result) -> None: assert result["openCanUndo"] is False def test_조작_한_번은_걸음_하나_되돌리면_열_때(result) -> None: assert result["editCanUndo"] is True and result["oneStep"] is True assert result["openState"] == {"grade": None, "cross": "opened", "route": "kept", "std": "std"}