Merge remote-tracking branch 'origin/dev' into sub_laptop_2
This commit is contained in:
@@ -128,7 +128,7 @@ function loadTs(full) {
|
||||
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
|
||||
const store = loadTs(B06("B06_Section_Section_Store"));
|
||||
const { writeState } = loadTs(path.join(ROOT, "A00_Common", "b_page_state.ts"));
|
||||
const storeDetail = () => store.peekSectionDetail(P);
|
||||
const storeDetail = () => store.peekSectionDetail(P, ROUTE);
|
||||
|
||||
/** 저장 정본 — 격자 0 · 20 · 40 · 60(지금 계획선으로 셈한 설계) · 계획선 평평 100. */
|
||||
const canon = () => ({
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/* 416 헬퍼 — 한 페이지 끊김 ② : 진짜 상세 가게(`B06_Section_Section_Store`) · 진짜 평면 횡단 판 자료
|
||||
* (`B05_Profile_UI_RouteEdit_Cross_Card.routeCrossSource`) · 진짜 관 길이(`design_structure_pipe_calc`) ·
|
||||
* 진짜 페이지 노선 칸. 서버 · 그림 · 묶음은 가짜. 짝 = `test_416_page_cache.py`.
|
||||
* L2 — 판 = 페이지 노선 상세(더 큰 노선 번호 상세가 캐시에 있어도)
|
||||
* M5 — 관 길이 = 공용 캐시(페이지 노선) · 저장 뒤 비우고 다시 받으면 새 값
|
||||
* M1 — 비운 뒤 두 보기가 함께 받으면 같은 새 객체 하나
|
||||
* 암선 기본 = config(기억해 둔 context 값)
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const P = "P416";
|
||||
|
||||
function memoryStorage() {
|
||||
const map = new Map();
|
||||
return {
|
||||
getItem: (k) => (map.has(k) ? map.get(k) : null),
|
||||
setItem: (k, v) => map.set(k, String(v)),
|
||||
removeItem: (k) => map.delete(k),
|
||||
key: (i) => [...map.keys()][i] ?? null,
|
||||
get length() {
|
||||
return map.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() };
|
||||
|
||||
const anyModule = (extra = {}) =>
|
||||
new Proxy(extra, { get: (t, key) => (key in t ? t[key] : () => undefined) });
|
||||
|
||||
/** 가짜 서버 — 노선마다 관 길이 하나(40 m 측점 관) · 받을 때마다 새 객체 · 받은 수. */
|
||||
const server = { pipeLength: { 3: 8, 9: 30 }, fetches: 0 };
|
||||
const detailOf = (routeId) => ({
|
||||
longitudinal: { stations: [], design_profiles: [] },
|
||||
cross_sections: [0, 20, 40].map((chainage_m) => ({
|
||||
station_id: `r${routeId}-${chainage_m}`,
|
||||
chainage_m,
|
||||
samples: [],
|
||||
culvert: chainage_m === 40 ? { chainage_m: 40 } : undefined,
|
||||
design: { pipe_length_m: chainage_m === 40 ? server.pipeLength[routeId] : undefined },
|
||||
})),
|
||||
});
|
||||
let rockDefault = null; // 종단 보기가 기억한 context 값
|
||||
let draftRoute = null; // 묶음 노선 초안
|
||||
|
||||
const STUBS = {
|
||||
"design_pipeline_page.ts": {
|
||||
reconcilePagePipeline: async () => 0,
|
||||
pipelineOwnsCross: () => false,
|
||||
pagePipeline: () => null,
|
||||
pipelineCross: () => undefined,
|
||||
pipelineDetail: () => null,
|
||||
},
|
||||
"design_pipeline_cross.ts": { forceCross: () => undefined },
|
||||
"B06_Section_Section_Drafts.ts": {
|
||||
forgetDraftStations: () => undefined,
|
||||
withDraftShapes: async () => undefined,
|
||||
withDraftStations: async () => undefined,
|
||||
},
|
||||
"B05_Profile_Api_PlusStations.ts": {
|
||||
loadSavedPlusStations: async () => undefined,
|
||||
plusStationsOf: () => [],
|
||||
},
|
||||
"B05_Profile_Api_Pipes_Draft.ts": { readPendingPipes: () => null },
|
||||
"design_page_data.ts": { pagePipes: async () => ({ pipe_points: [] }) },
|
||||
"B06_Section_Api_Fetch.ts": anyModule({
|
||||
fetchSectionDetail: async (_projectId, routeId) => {
|
||||
server.fetches += 1;
|
||||
return detailOf(routeId);
|
||||
},
|
||||
}),
|
||||
"B06_Section_Api_Culvert_Options.ts": anyModule({ STRUCTURE_ADJUST_KEYS: [] }),
|
||||
"B06_Section_Swap_Bus.ts": anyModule(),
|
||||
"B06_Section_Station_Keys.ts": anyModule({ KEYED_STATES: [] }),
|
||||
"B06_Section_UI_Page_Persist.ts": anyModule(),
|
||||
"B06_Section_UI_Standard_Panel.ts": anyModule({
|
||||
readRockBoundaryDefault: () => rockDefault,
|
||||
}),
|
||||
"B06_Section_Cross_Design_Session.ts": anyModule(),
|
||||
"common_util_cross_design.ts": anyModule(),
|
||||
"B06_Section_Structure_Layouts.ts": anyModule({ computeStoredLayouts: () => null }),
|
||||
"B06_Section_UI_Cross_Culvert_Wire.ts": anyModule(),
|
||||
"B06_Section_UI_Cross_Ford_Geom.ts": anyModule(),
|
||||
"B06_Section_UI_Cross_Box_Geom.ts": anyModule(),
|
||||
"B06_Section_UI_Cross_View.ts": anyModule(),
|
||||
"B06_Section_UI_Section_Common.ts": anyModule({ designElevationAt: () => undefined }),
|
||||
"B06_Section_Display_Width.ts": anyModule({ displayHalfWidth: () => 10 }),
|
||||
"B06_Section_UI_Style_Cross_Controls.css": {},
|
||||
"B06_Section_UI_Cross_Culvert_Basin.ts": anyModule(),
|
||||
"B06_Section_UI_Cross_Culvert_Const.ts": anyModule(),
|
||||
"B06_Section_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
|
||||
"B05_Profile_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
|
||||
"config_frontend.ts": anyModule({ PIPE_TYPES: [] }),
|
||||
};
|
||||
|
||||
const cache = new Map();
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
) ?? `${base}.ts`;
|
||||
function loadTs(full) {
|
||||
const stub = STUBS[path.basename(full)];
|
||||
if (stub) return stub;
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const near = (request) => {
|
||||
if (CATALOG.isCatalogRequest(request)) return CATALOG;
|
||||
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
||||
for (const [prefix, dir] of Object.entries(alias))
|
||||
if (request.startsWith(prefix))
|
||||
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), request)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const store = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Section_Store.ts"));
|
||||
const pageRoute = loadTs(path.join(ROOT, "A00_Common", "design", "design_page_route.ts"));
|
||||
const pipeCalc = loadTs(path.join(ROOT, "A00_Common", "design", "design_structure_pipe_calc.ts"));
|
||||
const card = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Cross_Card.ts"));
|
||||
const fakePipe = { input: (name) => (name === "route" ? draftRoute : null) };
|
||||
|
||||
async function run() {
|
||||
const out = {};
|
||||
// 열 때 — 페이지 노선 3 · 캐시에 더 큰 노선 9 도 있음(다른 탭 · 옛 판)
|
||||
pageRoute.setPageRouteId(P, 3);
|
||||
await store.loadSectionDetail(P, 9);
|
||||
const shown = await store.loadSectionDetail(P, 3);
|
||||
const source = card.routeCrossSource(P, fakePipe, 40, "cur");
|
||||
out.l2 = {
|
||||
station: source?.section.station_id ?? null,
|
||||
noRoute: store.peekSectionDetail(P, null),
|
||||
exact: store.peekSectionDetail(P, 3) === shown,
|
||||
};
|
||||
draftRoute = { v: 1 };
|
||||
out.l2.draftNoSaved = card.routeCrossSource(P, fakePipe, 40, "cur"); // 노선 초안 중 = 저장 상세 안 씀
|
||||
draftRoute = null;
|
||||
|
||||
// 암선 기본 = config(기억값) · 기억 전 = 서버 Schema 기본과 같은 값
|
||||
out.rock = { pending: source.rockDefaultM };
|
||||
rockDefault = -0.8;
|
||||
out.rock.config = card.routeCrossSource(P, fakePipe, 40, "cur").rockDefaultM;
|
||||
|
||||
// M5 — 관 길이 = 공용 캐시(페이지 노선) 한 벌(design_page 가 거는 그 함수)
|
||||
pipeCalc.setPipeSectionsSource(
|
||||
() => store.peekSectionDetail(P, pageRoute.pageRouteId(P))?.cross_sections ?? null,
|
||||
);
|
||||
out.m5 = { before: pipeCalc.pipeLengthM(40) };
|
||||
// [저장] — 서버가 관 길이를 다시 셈(8 → 11) · 한 벌이 캐시를 한 번 비움 → 두 보기가 함께 다시 받음
|
||||
server.pipeLength[3] = 11;
|
||||
store.invalidateSectionDetail(P);
|
||||
out.m5.cleared = pipeCalc.pipeLengthM(40);
|
||||
const fetchesBefore = server.fetches;
|
||||
const [profileView, crossView] = await Promise.all([
|
||||
store.loadSectionDetail(P, 3),
|
||||
store.loadSectionDetail(P, 3),
|
||||
]);
|
||||
out.m1 = {
|
||||
sameObject: profileView === crossView,
|
||||
fetched: server.fetches - fetchesBefore,
|
||||
fresh: profileView !== shown,
|
||||
panelSeesFresh: card.routeCrossSource(P, fakePipe, 40, "cur")?.section.station_id ?? null,
|
||||
};
|
||||
out.m5.after = pipeCalc.pipeLengthM(40);
|
||||
return out;
|
||||
}
|
||||
run()
|
||||
.then((out) => console.log(JSON.stringify(out)))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
/* 421 추천 창 「종단 반영」 셈 헬퍼 — `_Initial_Model` 을 그 자리에서 트랜스파일해 Node 로 돌려
|
||||
* 종단 반영 값이 서버 답 · 보낸 값 · 옛 캐시 · 서버 기록에서 어떻게 정해지는지 JSON 한 벌로.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
|
||||
function memoryStorage() {
|
||||
const map = new Map();
|
||||
return {
|
||||
getItem: (key) => (map.has(key) ? map.get(key) : null),
|
||||
setItem: (key, value) => map.set(key, String(value)),
|
||||
removeItem: (key) => map.delete(key),
|
||||
key: (index) => [...map.keys()][index] ?? null,
|
||||
get length() {
|
||||
return map.size;
|
||||
},
|
||||
_map: map,
|
||||
};
|
||||
}
|
||||
const session = memoryStorage();
|
||||
globalThis.window = { localStorage: memoryStorage(), sessionStorage: session };
|
||||
globalThis.fetch = () => Promise.resolve({ ok: false });
|
||||
|
||||
const STUBS = { "@ui/ui_template_palette": { themeColor: (_name, fallback) => fallback } };
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (STUBS[request]) return STUBS[request];
|
||||
if (request.endsWith(".css")) return {};
|
||||
if (request.startsWith("@ui/"))
|
||||
return loadTs(path.join(ROOT, "ui_template", `${request.slice(4)}.ts`));
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const model = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Initial_Model.ts"));
|
||||
|
||||
const answer = {
|
||||
nodes: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 10, y: 5 },
|
||||
],
|
||||
curves: [],
|
||||
};
|
||||
const fromSent = model.normalizeRouteInitial(answer, "earthwork", null, null, false);
|
||||
const fromServer = model.normalizeRouteInitial(
|
||||
{ ...answer, with_profile: true, unsnapped: { planned: [[0, 0]] } },
|
||||
"earthwork",
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
const oldFollow = model.normalizeRouteInitial(answer, "follow", null);
|
||||
const oldEarth = model.normalizeRouteInitial(answer, "earthwork", null);
|
||||
const base = model.serverBase({ logic: "gentle", with_profile: false });
|
||||
const oldBase = model.serverBase({ logic: "gentle" });
|
||||
model.writeInitialChoice("p421", fromSent);
|
||||
const readBack = model.readInitialChoice("p421");
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
sent: fromSent.withProfile,
|
||||
server: fromServer.withProfile,
|
||||
serverHasUnsnapped: "unsnapped" in fromServer,
|
||||
oldFollow: model.choiceWithProfile(oldFollow),
|
||||
oldEarth: model.choiceWithProfile(oldEarth),
|
||||
base: base.withProfile,
|
||||
oldBase: oldBase.withProfile ?? null,
|
||||
baseProfile: model.choiceWithProfile(base),
|
||||
oldBaseProfile: model.choiceWithProfile(oldBase),
|
||||
sameOff: model.sameBase({ ...fromSent, logic: "gentle" }, base),
|
||||
sameOn: model.sameBase({ ...fromServer, logic: "gentle" }, base),
|
||||
sameOld: model.sameBase({ ...fromServer, logic: "gentle" }, oldBase),
|
||||
readBack: readBack && readBack.withProfile,
|
||||
}),
|
||||
);
|
||||
@@ -142,11 +142,6 @@ def test_recommend_result_on_stations(hill_runs: dict, mode: str) -> None:
|
||||
if mode != "follow": # 갈래는 기울기 상한 안(언덕은 비켜 갈 수 있음)
|
||||
assert result["metrics"]["over_limit"]["total"] == 0
|
||||
assert result["metrics"]["max_grade_pct"] <= 14.0 + 1e-6
|
||||
# 측점 맞춤 끔 = 맞추기 전 선(같으면 None) — 비교값 따로
|
||||
raw = result["unsnapped"]
|
||||
if raw is not None:
|
||||
assert raw["planned"] != result["planned"] and raw["metrics"]["length_m"] > 0
|
||||
assert set(raw) == {"planned", "nodes", "curves", "off_station_curves", "metrics"}
|
||||
|
||||
|
||||
def _project_terrain():
|
||||
@@ -171,7 +166,7 @@ def _project_terrain():
|
||||
|
||||
|
||||
def test_project_modes_before_after() -> None:
|
||||
"""검증_sub1 실지형(14 % · R12) — 갈래별 측점 맞춤 전후 넘은 곳 · 최대 기울기 · 측점 밖 곡선."""
|
||||
"""검증_sub1 실지형(14 % · R12) — 갈래별 맞춘 선의 넘은 곳 · 최대 기울기 · 측점 밖 곡선(421)."""
|
||||
if not (PROJECT / "B05_Profile/route/cost_surface_classification_dtm_smooth.npz").is_file():
|
||||
pytest.skip("검증_sub1 storage 없음")
|
||||
terrain, streams = _project_terrain()
|
||||
@@ -189,17 +184,15 @@ def test_project_modes_before_after() -> None:
|
||||
min_radius_m=12.0,
|
||||
with_profile=False,
|
||||
)
|
||||
raw = result["unsnapped"] or result
|
||||
after, before = result["metrics"], raw["metrics"]
|
||||
after = result["metrics"]
|
||||
curves = len(result["curves"])
|
||||
on_total += curves - result["off_station_curves"]
|
||||
curves_total += curves
|
||||
print(
|
||||
f"{mode}: 넘은 곳 {before['over_limit']['total']} → {after['over_limit']['total']}"
|
||||
f" · 최대 기울기 {before['max_grade_pct']:.1f} → {after['max_grade_pct']:.1f} %"
|
||||
f" · 곡선 {len(raw['curves'])} → {curves}(측점 밖 {result['off_station_curves']})"
|
||||
f"{mode}: 넘은 곳 {after['over_limit']['total']}"
|
||||
f" · 최대 기울기 {after['max_grade_pct']:.1f} %"
|
||||
f" · 곡선 {curves}(측점 밖 {result['off_station_curves']})"
|
||||
)
|
||||
assert raw["off_station_curves"] >= result["off_station_curves"]
|
||||
print(f"측점 위 곡선 {on_total}/{curves_total} = {100 * on_total / curves_total:.0f}%")
|
||||
assert on_total / curves_total >= 0.75
|
||||
|
||||
|
||||
@@ -100,7 +100,6 @@ def test_follow_nodes_equal_initial(tmp_path: Path) -> None:
|
||||
assert all(abs(a[0] - b[0]) <= 1e-3 and abs(a[1] - b[1]) <= 1e-3 for a, b in zip(nodes, saved))
|
||||
misses = [v for v in result["metrics"]["violations"] if v["kind"] == "station"]
|
||||
assert len(misses) == result["metrics"]["over_limit"]["station"] == result["off_station_curves"]
|
||||
assert result["unsnapped"] is not None # 「측점 맞춤」 끔 = 맞추기 전 선
|
||||
|
||||
|
||||
def test_old_copy_flag_and_line_unchanged(tmp_path: Path) -> None:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""383-3 노선 추천 모달 [재계산] — 측점 맞춤 오른쪽 같은 줄 · 늘 보임 · 바꾼 것 있을 때만 켜짐.
|
||||
"""383-3 노선 추천 모달 [재계산] — 종단 반영 오른쪽 같은 줄 · 늘 보임 · 늘 켜짐(421).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_383_3_rerun_row.py -q
|
||||
"""
|
||||
@@ -10,11 +10,11 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
B05 = ROOT / "B05_Profile"
|
||||
|
||||
|
||||
def test_재계산_한_줄_늘_보임_바꿨을_때만_켜짐() -> None:
|
||||
def test_재계산_한_줄_늘_보임_늘_켜짐() -> None:
|
||||
ts = (B05 / "B05_Profile_UI_RouteEdit_Recommend.ts").read_text(encoding="utf-8")
|
||||
assert re.search(r'<div class="b05-routerec__row"><button[^>]*data-act="rerun">재계산', ts)
|
||||
assert 'data-act="rerun" hidden' not in ts and "rerun.hidden" not in ts
|
||||
assert "rerun.before(snapToggle)" in ts # 토글이 같은 줄 왼쪽
|
||||
assert "rerun.disabled = busy || !dirty || invalid.size > 0;" in ts
|
||||
assert "rerun.before(profileToggle)" in ts # 종단 반영 토글이 같은 줄 왼쪽(421)
|
||||
assert "rerun.disabled = busy || invalid.size > 0;" in ts
|
||||
css = (B05 / "B05_Profile_UI_Style_RouteRecommend.css").read_text(encoding="utf-8")
|
||||
assert re.search(r"\.b05-routerec__row \{\s*display: flex;", css)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""PLAN 416 sub5 — 한 페이지 끊김 ② : 저장 뒤 새 자료(M1) · 관 길이 출처(M5) · 오른쪽 판 노선 id(L2) · 암선 기본 config.
|
||||
|
||||
짝: `helper_416_page_cache.cjs`(진짜 상세 가게 · 평면 횡단 판 자료 · 관 길이 · 페이지 노선 칸 · 서버 · 그림 가짜).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r() -> dict:
|
||||
proc = subprocess.run( # noqa: S603, S607 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_416_page_cache.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_L2_판은_페이지_노선_상세(r):
|
||||
assert r["l2"]["station"] == "b05-cur-r3-40" # 캐시에 더 큰 노선 9 가 있어도
|
||||
assert r["l2"]["noRoute"] is None and r["l2"]["exact"] is True
|
||||
assert r["l2"]["draftNoSaved"] is None # 노선 초안 중 = 저장 상세 안 씀
|
||||
|
||||
|
||||
def test_암선_기본은_config(r):
|
||||
assert r["rock"] == {"pending": -0.5, "config": -0.8}
|
||||
src = _read("B05_Profile/B05_Profile_UI_RouteEdit_Cross_Card.ts")
|
||||
assert "readRockBoundaryDefault(projectId)" in src
|
||||
assert "?? -0.5" not in src
|
||||
|
||||
|
||||
def test_M5_관_길이는_공용_캐시(r):
|
||||
assert r["m5"] == {"before": 8, "cleared": None, "after": 11}
|
||||
page = _read("A00_Common/design/design_page.ts")
|
||||
assert "peekSectionDetail(projectId, pageRouteId(projectId))?.cross_sections" in page
|
||||
found = subprocess.run( # noqa: S603, S607 — 고정 실행 파일
|
||||
["git", "grep", "-l", "setPipeSectionsSource(", "--", "*.ts"],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
).stdout.split()
|
||||
# 거는 곳 = 페이지 하나(보기마다 걸면 마지막 연 보기 사본)
|
||||
assert sorted(found) == [
|
||||
"A00_Common/design/design_page.ts",
|
||||
"A00_Common/design/design_structure_pipe_calc.ts",
|
||||
]
|
||||
|
||||
|
||||
def test_M5_횡단_다시_셈_뒤_좌측_폼_다시_그림():
|
||||
shared = _read("A00_Common/design/design_shared.ts")
|
||||
body = shared[shared.index("function crossDesignsUpdated(") :]
|
||||
body = body[: body.index("\n}\n")]
|
||||
assert "shared.structures?.facility.recount()" in body
|
||||
# 종단도 알림 · 저장 노선 다시 셈 두 길 다 이 함수로
|
||||
assert "onCrossDesignsUpdated: (updated) => crossDesignsUpdated(shared, updated)" in shared
|
||||
assert "if (updated.length) crossDesignsUpdated(shared, updated);" in shared
|
||||
assert 'toAll(shared, "onCrossDesignsUpdated")' in body
|
||||
|
||||
|
||||
def test_M1_비운_뒤_두_보기는_새_객체_하나(r):
|
||||
assert r["m1"] == {
|
||||
"sameObject": True,
|
||||
"fetched": 1,
|
||||
"fresh": True,
|
||||
"panelSeesFresh": "b05-cur-r3-40",
|
||||
}
|
||||
|
||||
|
||||
def test_M1_저장_끝_한_번_비우고_종단_다시_읽음():
|
||||
shared = _read("A00_Common/design/design_shared.ts")
|
||||
done = shared[shared.index("done: async (isConfirm)") :]
|
||||
assert done.index("invalidateSectionDetail(shared.projectId)") < done.index("profileSave?.done")
|
||||
actions = _read("B05_Profile/B05_Profile_UI_Page_Actions.ts")
|
||||
tail = actions[actions.index("done: async") :]
|
||||
assert tail.index("ctx.loadLatest(true)") < tail.index("ctx.restoreSections(routeId)")
|
||||
assert "invalidateSectionDetail" not in actions
|
||||
persist = _read("B06_Section/B06_Section_UI_Page_Persist.ts")
|
||||
assert (
|
||||
"invalidateSectionDetail" not in persist
|
||||
) # 보기마다 비우면 앞 보기가 받은 새 상세를 또 버림
|
||||
@@ -0,0 +1,67 @@
|
||||
"""421 추천 창 — 「측점 맞춤」 → 「종단 반영」 · [재계산] 늘 켜짐 + `recompute`.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_421_recommend_profile.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
B05 = ROOT / "B05_Profile"
|
||||
HELPER = Path(__file__).with_name("helper_421_profile_toggle.cjs")
|
||||
|
||||
|
||||
def _read(name: str) -> str:
|
||||
return (B05 / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_종단_반영_값_정하기() -> None:
|
||||
out = json.loads(
|
||||
subprocess.run(
|
||||
["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", check=True
|
||||
).stdout
|
||||
)
|
||||
assert out["sent"] is False # 옛 서버 = 보낸 값
|
||||
assert out["server"] is True # 서버 답 `with_profile` 이 먼저
|
||||
assert out["serverHasUnsnapped"] is False # 맞추기 전 선은 화면이 안 씀
|
||||
assert out["oldFollow"] is False and out["oldEarth"] is True # 옛 캐시 = 갈래로
|
||||
assert out["base"] is False and out["baseProfile"] is False
|
||||
assert out["oldBase"] is None and out["oldBaseProfile"] is True
|
||||
assert out["sameOff"] is True and out["sameOn"] is False and out["sameOld"] is True
|
||||
assert out["readBack"] is False # 캐시에 남아 다시 열 때 되살림
|
||||
|
||||
|
||||
def test_창_단추와_재계산() -> None:
|
||||
rec = _read("B05_Profile_UI_RouteEdit_Recommend.ts")
|
||||
assert "측점 맞춤" not in rec and "snapOn" not in rec and "unsnapped" not in rec
|
||||
assert 'toggleButton("종단 반영", profileOn,' in rec
|
||||
assert "let profileOn = params.withProfile;" in rec
|
||||
toggle = rec[rec.index('toggleButton("종단 반영"') :][:200]
|
||||
assert "profileOn = active;" in toggle and "void run();" in toggle # 누르면 바로
|
||||
assert "rerun.before(profileToggle);" in rec
|
||||
assert "rerun.disabled = busy || invalid.size > 0;" in rec # 늘 켜짐
|
||||
assert 'rerun.addEventListener("click", () => void run(true));' in rec
|
||||
assert "const withProfile = profileOn;" in rec
|
||||
assert "params.onApply(preview);" in rec
|
||||
|
||||
|
||||
def test_요청과_적용에_종단_반영() -> None:
|
||||
init = _read("B05_Profile_UI_RouteEdit_Initial.ts")
|
||||
assert "with_profile: withProfile," in init
|
||||
assert "...(recompute ? { recompute: true } : {})," in init
|
||||
assert "withProfile: choice?.withProfile ?? base.withProfile ?? true," in init
|
||||
on_apply = init[init.index("onApply: (picked) => {") :][:900]
|
||||
assert "with_profile: choiceWithProfile(picked)," in on_apply
|
||||
assert on_apply.count("choiceWithProfile(picked)") == 2 # 저장 칸 · 균형 종단
|
||||
assert "withProfileOf(" not in init
|
||||
assert "with_profile: choiceWithProfile(now)" in init # [저장]에도 실음
|
||||
api = _read("B05_Profile_Api_RouteInitial.ts")
|
||||
assert "recompute?: boolean;" in api
|
||||
|
||||
|
||||
def test_도움말_글() -> None:
|
||||
help_ts = _read("B05_Profile_UI_RouteEdit_Recommend_Help.ts")
|
||||
assert "「측점 맞춤」을 끄면" not in help_ts and "끔 = 옮기기 전 선" not in help_ts
|
||||
assert 'name: "재계산",' in help_ts and 'name: "측점 맞춤(늘)",' in help_ts
|
||||
assert "단추로 켬 · 끔(기본 켬 · 다섯 갈래 모두)" in help_ts
|
||||
Reference in New Issue
Block a user