fix(468 값 안 변함 ③): 되돌리기 빈 칸 · 접힘 파생값 · 배지 소수 한 자리
- 사진첩: 「{}」 · 「null」 · 「""」 = 칸 없음 — 빈 맵 쓰기가 걸음이 되어 ↷ 를 버리던 흠(R4) · 「[]」(관 · 구조물 모두 지움)은 값으로 둠
- 세션 맵 · 연동 플래그: 빈 맵 persist = 칸 지움(갈아끼움 이동이 「{}」 여섯 칸을 만들지 않음)
- 종단도: 본문 0 크기(판 접힘)에도 낡음 예약 · 유토 총괄값 · 배분 선반입 · 측점 자료를 셈 · 그리기만 건너뜀 — 떼어 낸 유토 판은 그림
- 배지: 소수 한 자리 고정(−17,570.0㎥)
- 시험: test_272_history_empty_cells · test_272_hidden_summary(옛 코드 7 실패)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
/* 272 시험 묶음 진입점(468 · 466-C) — 사진첩 빈 칸 · 세션 맵 빈 쓰기 · 숨은 종단도 파생값 · 배지 자릿수(진짜 모듈 · vite ssr). */
|
||||
export { createProfileHistory } from "../../B05_Profile/B05_Profile_UI_Profile_History";
|
||||
export {
|
||||
createSessionMap,
|
||||
moveSessionMaps,
|
||||
} from "../../B06_Section/B06_Section_UI_Page_Link_Session";
|
||||
export { renderProfile } from "../../B05_Profile/B05_Profile_UI_Profile_Render";
|
||||
export { createRouteMassHaulPanel } from "../../B05_Profile/B05_Profile_UI_Profile_MassHaul";
|
||||
export { createMassHaulBadge } from "../../common_util/common_util_mass_haul_badge";
|
||||
export { computeMassHaulSeries } from "../../common_util/common_util_mass_haul";
|
||||
export { staleKey } from "../../B06_Section/B06_Section_UI_Section_Common";
|
||||
@@ -0,0 +1,353 @@
|
||||
/* 272 헬퍼(468 · 466-C) — 사진첩 빈 칸 · 세션 맵 빈 쓰기 · 숨은 종단도 파생값 · 배지 자릿수.
|
||||
* node helper_272_value_hold.mjs <묶음.mjs>
|
||||
* vite ssr 로 묶은 진짜 모듈(helper_272_entry.ts) · 표준출력 JSON 한 벌.
|
||||
* 짝 = `test_272_history_empty_cells.py` · `test_272_hidden_summary.py`.
|
||||
*/
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const store = () => {
|
||||
const m = new Map();
|
||||
return {
|
||||
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
||||
setItem: (k, v) => m.set(k, String(v)),
|
||||
removeItem: (k) => m.delete(k),
|
||||
key: (i) => [...m.keys()][i] ?? null,
|
||||
get length() {
|
||||
return m.size;
|
||||
},
|
||||
clear: () => m.clear(),
|
||||
};
|
||||
};
|
||||
// 손길 듣기(pointerdown · keydown)가 붙는 창 — 사진첩 시험(346-4)과 같은 꼴
|
||||
const win = new EventTarget();
|
||||
win.setTimeout = setTimeout;
|
||||
win.clearTimeout = clearTimeout;
|
||||
win.sessionStorage = store();
|
||||
win.localStorage = store();
|
||||
win.innerHeight = 900;
|
||||
win.matchMedia = () => ({ matches: false, addEventListener: () => undefined });
|
||||
win.requestAnimationFrame = (run) => setTimeout(run, 0);
|
||||
globalThis.window = win;
|
||||
globalThis.screen = {};
|
||||
globalThis.sessionStorage = win.sessionStorage;
|
||||
globalThis.localStorage = win.localStorage;
|
||||
globalThis.requestAnimationFrame = win.requestAnimationFrame;
|
||||
const any = new Proxy(function () {}, {
|
||||
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
|
||||
apply: () => any,
|
||||
construct: () => any,
|
||||
});
|
||||
globalThis.document = any;
|
||||
localStorage.setItem("frd_current_project_id", "p1");
|
||||
|
||||
const m = await import(pathToFileURL(process.argv[2]).href);
|
||||
const out = {};
|
||||
const S = sessionStorage;
|
||||
const key = (name) => `aislo:draft:${name}:p1:7`;
|
||||
const SIX = ["basinadjust", "crossw", "extraspan", "extrawall", "inletstruct", "revetx"];
|
||||
const touch = () => win.dispatchEvent(new Event("keydown"));
|
||||
|
||||
/* ── H1 · 되돌린 뒤 빈 칸이 생김(R4) — 「{}」 · 「null」 · 「""」 쓰기는 걸음 아님 · ↷ 남음 ── */
|
||||
{
|
||||
const h = m.createProfileHistory(() => {});
|
||||
touch();
|
||||
S.setItem(key("alignment-draft"), JSON.stringify({ station_offsets: { 100: 1 } }));
|
||||
h.record();
|
||||
const edited = h.canUndo();
|
||||
h.undo();
|
||||
const undone = { undo: h.canUndo(), redo: h.canRedo(), grade: S.getItem(key("alignment-draft")) };
|
||||
// B06 로 넘어가는 누름(손길) 뒤 첫 마운트 · 갈아끼움 이동(moveSessionMaps)이 빈 맵 여섯을 씀 · 다른 칸은 null · 빈 글
|
||||
touch();
|
||||
SIX.forEach((name) => S.setItem(key(name), "{}"));
|
||||
S.setItem(key("rockb"), "null");
|
||||
S.setItem(key("cutslope"), "");
|
||||
h.record();
|
||||
out.h1 = { edited, undone, after: { undo: h.canUndo(), redo: h.canRedo() } };
|
||||
// ↷ = 사용자 조작 그대로 한 걸음
|
||||
out.h1.redone = h.redo();
|
||||
out.h1.redoGrade = S.getItem(key("alignment-draft"));
|
||||
out.h1.final = { undo: h.canUndo(), redo: h.canRedo() };
|
||||
S.clear();
|
||||
localStorage.setItem("frd_current_project_id", "p1");
|
||||
}
|
||||
|
||||
/* ── H2 · 「[]」 = 목록 초안(관 · 구조물 모두 지움) — 걸음 그대로 · 되돌리면 칸 없음 ── */
|
||||
{
|
||||
const h = m.createProfileHistory(() => {});
|
||||
touch();
|
||||
S.setItem("aislo:draft:pipes:p1", "[]");
|
||||
h.record();
|
||||
const step = h.canUndo();
|
||||
h.undo();
|
||||
out.h2 = { step, undonePipes: S.getItem("aislo:draft:pipes:p1") };
|
||||
h.redo();
|
||||
out.h2.redonePipes = S.getItem("aislo:draft:pipes:p1");
|
||||
S.clear();
|
||||
localStorage.setItem("frd_current_project_id", "p1");
|
||||
}
|
||||
|
||||
/* ── H3 · 빈 칸이 있던 걸음으로 되돌림 = 빈 칸(없음과 같음) · 값 칸은 그대로 ── */
|
||||
{
|
||||
S.setItem(key("crossw"), "{}");
|
||||
S.setItem(key("revetx"), JSON.stringify({ "100.00:inlet": { x: 1 } }));
|
||||
const h = m.createProfileHistory(() => {});
|
||||
touch();
|
||||
S.setItem(key("crossw"), JSON.stringify({ "100.00": 6 }));
|
||||
h.record();
|
||||
const step = h.canUndo();
|
||||
h.undo();
|
||||
out.h3 = { step, crossw: S.getItem(key("crossw")), revetx: S.getItem(key("revetx")) };
|
||||
S.clear();
|
||||
localStorage.setItem("frd_current_project_id", "p1");
|
||||
}
|
||||
|
||||
/* ── H4 · 세션 맵 — 빈 맵 쓰기 = 칸 지움 · 값 있으면 씀 · 빈 맵 이동은 칸을 안 만듦 ── */
|
||||
{
|
||||
const width = m.createSessionMap(
|
||||
() => key("crossw"),
|
||||
(v) => (typeof v === "number" ? v : undefined),
|
||||
);
|
||||
width.persist();
|
||||
const emptyWrite = S.getItem(key("crossw"));
|
||||
width.values.set("100.00", 6);
|
||||
width.persist();
|
||||
const filledWrite = S.getItem(key("crossw"));
|
||||
width.values.clear();
|
||||
width.persist();
|
||||
const clearedWrite = S.getItem(key("crossw"));
|
||||
const others = SIX.filter((n) => n !== "crossw").map((n) =>
|
||||
m.createSessionMap(
|
||||
() => key(n),
|
||||
(v) => v,
|
||||
),
|
||||
);
|
||||
m.moveSessionMaps(new Map([["100.00", "120.00"]]));
|
||||
const moved = SIX.map((n) => S.getItem(key(n)));
|
||||
// 값 있는 맵 이동 = 새 자리로 옮겨 씀
|
||||
S.setItem(key("extrawall"), JSON.stringify({ "100.00": 2 }));
|
||||
m.moveSessionMaps(new Map([["100.00", "120.00"]]));
|
||||
out.h4 = {
|
||||
emptyWrite,
|
||||
filledWrite,
|
||||
clearedWrite,
|
||||
moved,
|
||||
movedFilled: S.getItem(key("extrawall")),
|
||||
made: others.length,
|
||||
};
|
||||
S.clear();
|
||||
localStorage.setItem("frd_current_project_id", "p1");
|
||||
}
|
||||
|
||||
/* ── 숨은 종단도(본문 0 크기) — 낡음 예약 · 유토곡선 셈 · 배분 선반입 여전히 · 펼침과 같은 값 ── */
|
||||
const samples = [0, 20, 40, 60, 80, 100].map((c) => ({
|
||||
chainage_m: c,
|
||||
elevation_m: 100 + c * 0.05,
|
||||
valid: true,
|
||||
}));
|
||||
const plan = [0, 20, 40, 60, 80, 100].map((c) => ({ chainage_m: c, elevation_m: 100 + c * 0.04 }));
|
||||
const design = (c, cut, fill, z) => ({
|
||||
station_id: `station_${c}`,
|
||||
chainage_m: c,
|
||||
design: {
|
||||
cut_area_m2: cut,
|
||||
fill_area_m2: fill,
|
||||
ground_type: "soil",
|
||||
design_elevation_m: z,
|
||||
},
|
||||
});
|
||||
const detail = {
|
||||
longitudinal: {
|
||||
length_m: 100,
|
||||
samples,
|
||||
stations: samples.map((s) => ({
|
||||
station_id: `station_${s.chainage_m}`,
|
||||
chainage_m: s.chainage_m,
|
||||
})),
|
||||
design_profiles: [{ id: "design_grade_line", samples: plan }],
|
||||
},
|
||||
cross_sections: [
|
||||
design(0, 2, 0, 100),
|
||||
design(20, 3, 1, 100.8),
|
||||
design(40, 1, 4, 101.6),
|
||||
design(60, 0, 6, 999), // 낡음 — 계획선과 어긋난 측점(다시 셈 예약 대상)
|
||||
design(80, 2, 2, 103.2),
|
||||
design(100, 4, 0, 104),
|
||||
],
|
||||
};
|
||||
const alignment = {
|
||||
samples: plan,
|
||||
edits: { station_offsets: {}, curve_radii: {} },
|
||||
stations: plan.map((p) => ({
|
||||
chainage_m: p.chainage_m,
|
||||
plan_elevation_m: p.elevation_m,
|
||||
kind: "regular",
|
||||
})),
|
||||
curves: [],
|
||||
balance: { cut_area_m2: 0, fill_area_m2: 0, net_area_m2: 0 },
|
||||
policy: { station_interval_m: 20, edit_step_m: 0.1 },
|
||||
};
|
||||
const ONES = {
|
||||
soil: { loose: 1, compacted: 1 },
|
||||
ripping_rock: { loose: 1, compacted: 1 },
|
||||
blasting_rock: { loose: 1, compacted: 1 },
|
||||
};
|
||||
|
||||
function ctxFor(width, rec) {
|
||||
const body = {
|
||||
clientWidth: width,
|
||||
clientHeight: width ? 400 : 0,
|
||||
scrollLeft: 0,
|
||||
parentElement: null,
|
||||
replaceChildren: () => (rec.painted += 1),
|
||||
addEventListener: () => undefined,
|
||||
};
|
||||
return {
|
||||
body,
|
||||
onStaleDesigns: (k) => (rec.stale.push(k), false),
|
||||
callbacks: undefined,
|
||||
massHaul: { draw: (p) => rec.mass.push(p) },
|
||||
tableOverlay: { isOpen: () => false, contentHeight: () => 0, setTable: () => undefined },
|
||||
store: { edits: () => ({}), savedRadii: () => ({}), resetStation: () => undefined },
|
||||
detail: () => detail,
|
||||
alignment: () => alignment,
|
||||
base: () => null,
|
||||
stationInterval: () => 20,
|
||||
irregularStations: () => [],
|
||||
minCoverTargets: () => [],
|
||||
structures: () => [],
|
||||
structureTypes: () => [],
|
||||
selectedStationId: () => null,
|
||||
setSelectedStationId: () => undefined,
|
||||
selectedStructureId: () => null,
|
||||
setSelectedStructureId: () => undefined,
|
||||
stationDisplay: () => ({ station: 0, cumulative: 0 }),
|
||||
setLastSize: () => (rec.sized += 1),
|
||||
renderBalance: () => (rec.balance += 1),
|
||||
applyHeightCascade: () => ({ chartHeight: 300 }),
|
||||
clearMainDragCooldown: () => undefined,
|
||||
selectStation: () => undefined,
|
||||
applyEdits: () => undefined,
|
||||
handleToolPick: () => false,
|
||||
zoom: () => ({ x: 1 }),
|
||||
holdElevationRange: (next) => next ?? undefined,
|
||||
toolActive: () => false,
|
||||
tools: { selectedRuns: () => [], mountOverlay: () => undefined },
|
||||
stationIdAtStructure: () => null,
|
||||
redraw: () => undefined,
|
||||
setElevationWindowUpdater: () => undefined,
|
||||
};
|
||||
}
|
||||
const pick = (p) =>
|
||||
p && {
|
||||
pendingRecalc: p.pendingRecalc,
|
||||
staleChainages: p.staleChainages,
|
||||
crossSections: p.crossSections.length,
|
||||
longitudinal: p.longitudinal,
|
||||
stations: p.stationSource.stations.map((s) => s.chainage_m),
|
||||
maxChainageM: p.axis.maxChainageM,
|
||||
};
|
||||
const hiddenRec = { stale: [], mass: [], painted: 0, sized: 0, balance: 0 };
|
||||
m.renderProfile(ctxFor(0, hiddenRec));
|
||||
const shownRec = { stale: [], mass: [], painted: 0, sized: 0, balance: 0 };
|
||||
let shownError = null;
|
||||
try {
|
||||
m.renderProfile(ctxFor(1200, shownRec));
|
||||
} catch (error) {
|
||||
shownError = String(error?.stack ?? error);
|
||||
}
|
||||
out.hidden = {
|
||||
stale: hiddenRec.stale,
|
||||
mass: pick(hiddenRec.mass[0]),
|
||||
massCount: hiddenRec.mass.length,
|
||||
painted: hiddenRec.painted,
|
||||
sized: hiddenRec.sized,
|
||||
};
|
||||
out.shown = {
|
||||
stale: shownRec.stale,
|
||||
mass: pick(shownRec.mass[0]),
|
||||
massCount: shownRec.mass.length,
|
||||
painted: shownRec.painted,
|
||||
error: shownError,
|
||||
};
|
||||
out.expectedStale = m.staleKey({
|
||||
longitudinal: { design_profiles: [{ samples: plan }] },
|
||||
cross_sections: detail.cross_sections,
|
||||
});
|
||||
|
||||
/* 유토곡선 판(진짜) — 판 접힘 · 펼침인데 본문 숨음(자리 0) = 총괄값 · 배분 선반입만 · 그림 없음 */
|
||||
function massPanel(open) {
|
||||
localStorage.setItem("aislo:pref:masshaul-open", String(open));
|
||||
const rec = { summary: [], scheduled: 0 };
|
||||
const prefetch = {
|
||||
schedule: () => (rec.scheduled += 1),
|
||||
current: () => null,
|
||||
};
|
||||
const panel = m.createRouteMassHaulPanel(
|
||||
() => undefined,
|
||||
(s) => rec.summary.push(s),
|
||||
prefetch,
|
||||
);
|
||||
panel.setContext({ conversion: ONES });
|
||||
return { panel, rec };
|
||||
}
|
||||
const series = m.computeMassHaulSeries(
|
||||
{ length_m: 100, design_profiles: [{ samples: plan }] },
|
||||
detail.cross_sections,
|
||||
ONES,
|
||||
undefined,
|
||||
["cross"],
|
||||
);
|
||||
const finalM3 = series[0]?.result.points.at(-1)?.cumulative_volume_m3 ?? null;
|
||||
const massParams = hiddenRec.mass[0];
|
||||
out.panel = {};
|
||||
for (const open of [false, true]) {
|
||||
const { panel, rec } = massPanel(open);
|
||||
let error = null;
|
||||
try {
|
||||
if (massParams) panel.draw({ ...massParams, pendingRecalc: false });
|
||||
} catch (e) {
|
||||
error = String(e?.stack ?? e);
|
||||
}
|
||||
out.panel[open ? "open" : "closed"] = {
|
||||
summary: rec.summary.map((s) => s?.finalM3 ?? null),
|
||||
scheduled: rec.scheduled,
|
||||
error,
|
||||
};
|
||||
}
|
||||
out.finalM3 = finalM3;
|
||||
|
||||
/* ── 배지 표기 — 소수 한 자리 고정 ── */
|
||||
{
|
||||
const make = () => {
|
||||
const el = {
|
||||
textContent: "",
|
||||
title: "",
|
||||
hidden: false,
|
||||
style: { setProperty: () => undefined },
|
||||
classList: { toggle: () => undefined, add: () => undefined },
|
||||
append: () => undefined,
|
||||
className: "",
|
||||
};
|
||||
return el;
|
||||
};
|
||||
const made = [];
|
||||
globalThis.document = { createElement: () => (made.push(make()), made.at(-1)) };
|
||||
const badge = m.createMassHaulBadge();
|
||||
const texts = [];
|
||||
for (const v of [-17570, -17570.7, 1234.56, 0]) {
|
||||
badge.set({
|
||||
finalM3: v,
|
||||
cutNaturalM3: 0,
|
||||
cutCompactedM3: 0,
|
||||
fillCompactedM3: 0,
|
||||
surplusM3: 0,
|
||||
shortageM3: 0,
|
||||
});
|
||||
texts.push(made[2].textContent);
|
||||
}
|
||||
out.badge = texts;
|
||||
globalThis.document = any;
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,55 @@
|
||||
"""272 · 468 (C2) 종단 판 접힘(본문 0 크기) 파생값 · (C3) 배지 자릿수.
|
||||
|
||||
R3 — 판 접은 채 교점 ↓ → 배지 옛 값 그대로 → 이제 접힘 동안도 낡음 예약 · 유토 총괄값 · 배분 선반입 · 측점 자료를 셈 ·
|
||||
그리기(본문 갈아 끼움 · 크기 기록)만 건너뜀 · 펼친 그림과 같은 값. 배지 = 소수 한 자리 고정.
|
||||
짝: `helper_272_value_hold.mjs` · 묶음 짓기 = `test_272_history_empty_cells.run_helper`.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_272_hidden_summary.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from test_272_history_empty_cells import pytestmark, run_helper # noqa: E402, F401
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out(tmp_path_factory):
|
||||
return run_helper(tmp_path_factory)
|
||||
|
||||
|
||||
def test_숨은_본문도_낡음_예약은_펼침과_같은_열쇠(out):
|
||||
assert out["shown"]["error"] is None
|
||||
assert out["shown"]["stale"] == [out["expectedStale"]]
|
||||
assert out["hidden"]["stale"] == out["shown"]["stale"]
|
||||
|
||||
|
||||
def test_숨은_본문도_유토곡선_입력은_펼침과_같음(out):
|
||||
hidden, shown = out["hidden"], out["shown"]
|
||||
assert hidden["massCount"] == shown["massCount"] == 1
|
||||
assert hidden["mass"] == shown["mass"]
|
||||
assert hidden["mass"]["staleChainages"] == [60]
|
||||
|
||||
|
||||
def test_숨은_본문은_그리기만_건너뜀(out):
|
||||
assert out["hidden"]["painted"] == 0 and out["hidden"]["sized"] == 0
|
||||
assert out["shown"]["painted"] == 1
|
||||
|
||||
|
||||
def test_유토_판_접힘_펼침_모두_총괄값과_배분_선반입(out):
|
||||
final = out["finalM3"]
|
||||
assert final is not None
|
||||
for state in ("closed", "open"):
|
||||
panel = out["panel"][state]
|
||||
assert panel["error"] is None, panel["error"]
|
||||
assert panel["summary"] == [final], state
|
||||
assert panel["scheduled"] == 1, state
|
||||
|
||||
|
||||
def test_배지는_소수_한_자리(out):
|
||||
assert out["badge"] == ["-17,570.0㎥", "-17,570.7㎥", "1,234.6㎥", "0.0㎥"]
|
||||
@@ -0,0 +1,104 @@
|
||||
"""272 · 468 (C1) 되돌리기 빈 칸 — 「{}」 · 「null」 · 「""」 = 칸 없음 · 세션 맵 빈 쓰기 = 칸 지움.
|
||||
|
||||
R4 — 교점 ↓ → ↶ → B06(누름 뒤 첫 마운트가 빈 맵 여섯 「{}」를 씀) → ↶ 켜짐 · ↷ 사라짐 → 이제 ↶ 꺼짐 · ↷ 남음.
|
||||
「[]」 는 목록 초안(관 · 구조물 모두 지움 = 저장분과 다름)이라 걸음 그대로.
|
||||
짝: `helper_272_value_hold.mjs`(vite ssr 로 묶은 진짜 모듈 · `helper_272_entry.ts`).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_272_history_empty_cells.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def run_helper(tmp_path_factory: pytest.TempPathFactory) -> dict:
|
||||
work = tmp_path_factory.mktemp("b272")
|
||||
build = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_272_entry.ts",
|
||||
"--outDir",
|
||||
str(work),
|
||||
"--logLevel",
|
||||
"error",
|
||||
],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=300,
|
||||
)
|
||||
assert build.returncode == 0, build.stderr
|
||||
bundle = work / "bundle.mjs"
|
||||
shutil.copy(work / "helper_272_entry.js", bundle)
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_272_value_hold.mjs"), str(bundle)],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out(tmp_path_factory):
|
||||
return run_helper(tmp_path_factory)
|
||||
|
||||
|
||||
def test_되돌린_뒤_빈_칸이_생겨도_다시하기_남음(out):
|
||||
h1 = out["h1"]
|
||||
assert h1["edited"] is True
|
||||
assert h1["undone"] == {"undo": False, "redo": True, "grade": None}
|
||||
# 빈 맵 여섯 「{}」 · 「null」 · 「""」 = 걸음 아님
|
||||
assert h1["after"] == {"undo": False, "redo": True}
|
||||
|
||||
|
||||
def test_사용자_조작은_한_걸음_그대로(out):
|
||||
h1 = out["h1"]
|
||||
assert h1["redone"] is True
|
||||
assert h1["redoGrade"] == json.dumps({"station_offsets": {"100": 1}}, separators=(",", ":"))
|
||||
assert h1["final"] == {"undo": True, "redo": False}
|
||||
|
||||
|
||||
def test_빈_목록_초안은_걸음(out):
|
||||
# 관 모두 지움(「[]」) ≠ 칸 없음(저장분 관) — 되돌리기 · 다시하기 그대로
|
||||
assert out["h2"] == {"step": True, "undonePipes": None, "redonePipes": "[]"}
|
||||
|
||||
|
||||
def test_빈_칸_걸음으로_되돌리면_빈_칸(out):
|
||||
h3 = out["h3"]
|
||||
assert h3["step"] is True
|
||||
assert h3["crossw"] in (None, "{}") # 빈 칸 = 칸 없음(읽는 쪽은 빈 맵)
|
||||
assert h3["revetx"] == json.dumps({"100.00:inlet": {"x": 1}}, separators=(",", ":"))
|
||||
|
||||
|
||||
def test_세션_맵_빈_쓰기는_칸_지움(out):
|
||||
h4 = out["h4"]
|
||||
assert h4["emptyWrite"] is None
|
||||
assert h4["filledWrite"] == '{"100.00":6}'
|
||||
assert h4["clearedWrite"] is None
|
||||
# 빈 맵 이동은 칸을 안 만듦 · 값 있는 맵은 새 자리로 옮겨 씀
|
||||
assert h4["moved"] == [None] * 6
|
||||
assert h4["movedFilled"] == '{"120.00":2}'
|
||||
Reference in New Issue
Block a user