Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG
169 lines
6.5 KiB
Python
169 lines
6.5 KiB
Python
"""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"}
|