From d33cf220f3d9973af776e0992335d105d7cd1c9d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:31:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(=EC=A2=85=EB=8B=A8):=20325-3=20=EC=9C=A0?= =?UTF-8?q?=ED=86=A0=EA=B3=A1=EC=84=A0=20=C2=B7=20=EB=B0=B0=EC=88=98?= =?UTF-8?q?=EC=9C=A0=EC=97=AD=20[=EC=B0=BD=20=EB=B6=84=ED=95=A0]=20?= =?UTF-8?q?=E2=80=94=20=EB=AA=B8=ED=86=B5=20attachPopout=20=ED=95=9C=20?= =?UTF-8?q?=EB=B2=8C=20=C2=B7=20=EC=9C=A0=ED=86=A0=20=ED=8C=90=20=EC=A2=8C?= =?UTF-8?q?=EC=83=81=EB=8B=A8=20=C2=B7=20=EB=B0=B0=EC=88=98=20=EB=A8=B8?= =?UTF-8?q?=EB=A6=AC=EC=A4=84=20=EB=A7=A8=20=EC=95=9E=20=EB=8B=A8=EC=B6=94?= =?UTF-8?q?=20=C2=B7=20=EC=B0=BD=20=EC=9D=B4=EB=A6=84=20=EB=94=B0=EB=A1=9C?= =?UTF-8?q?=20=C2=B7=20=EB=8B=A8=EC=B6=94=EA=B0=80=20=EB=86=93=EC=9D=B8=20?= =?UTF-8?q?=EC=B0=BD=EC=97=90=EC=84=9C=20=EC=97=B6=20=C2=B7=20=EB=97=80=20?= =?UTF-8?q?=ED=8C=90=20=EC=B0=BD=20=EA=BD=89=20=EC=B1=84=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn --- A00_Common/design/design_page.css | 29 +++ A00_Common/design/design_profile_popout.ts | 129 +++++++++-- A00_Common/design/design_shared.ts | 3 +- .../tester/helper_224_1c_subpanel_popout.cjs | 215 ++++++++++++++++++ .../tester/helper_282_1_profile_toolbar.cjs | 3 + .../tester/test_224_1c_subpanel_popout.py | 107 +++++++++ 6 files changed, 462 insertions(+), 24 deletions(-) create mode 100644 resources/tester/helper_224_1c_subpanel_popout.cjs create mode 100644 resources/tester/test_224_1c_subpanel_popout.py diff --git a/A00_Common/design/design_page.css b/A00_Common/design/design_page.css index 605935355..7ffb67950 100644 --- a/A00_Common/design/design_page.css +++ b/A00_Common/design/design_page.css @@ -55,6 +55,35 @@ font-size: var(--btn-bar-font-size); } +/* 유토곡선 · 배수유역 판 [창 분할](계획서 325-3) — 종단 [창 분할]과 같은 양식 · 유토는 판 좌상단에 띄움. */ +.b05-subpanel__popout { + flex: none; + padding: var(--btn-bar-padding); + font-size: var(--btn-bar-font-size); +} + +.b05-profile__masshaul-overlay > .b05-subpanel__popout { + position: absolute; + top: 4px; + left: var(--spacing-8); + z-index: 4; +} + +/* 새 창으로 뗀 유토곡선 · 배수유역 판 — 창 전체를 채움 · 높이 · 폭 끌기와 접기 손잡이는 감춤. */ +.b05-profile__masshaul-overlay.is-popout, +.b05-drainage.is-popout { + position: relative; + width: 100%; + max-width: none; + height: 100dvh; +} + +.b05-profile__masshaul-overlay.is-popout > .ui-resizer, +.b05-drainage.is-popout > .ui-resizer, +.b05-drainage.is-popout > .ui-workflow-overlay__handle { + display: none; +} + /* 횡단 조정(카드 조정창 사본 · 폼 칸에 옮겨 붙은 행)은 횡단 보기 몫 — 종단 보기에서는 한 벌 폼 안에서 가린다(패널 뿌리 클래스가 보기를 따라 바뀐다). */ .b05-route__panel .b06-adjust-dock, diff --git a/A00_Common/design/design_profile_popout.ts b/A00_Common/design/design_profile_popout.ts index ce69d30d5..304bdb07f 100644 --- a/A00_Common/design/design_profile_popout.ts +++ b/A00_Common/design/design_profile_popout.ts @@ -1,6 +1,7 @@ /* ============================================================================= * A00_Common/design/design_profile_popout.ts * 종단 하단 패널(종단도 · 표 · 유토곡선 · 배수유역도)을 **브라우저 새 창**으로 뺀다(계획서 54-35). + * 유토곡선 · 배수유역 판은 따로 또 뗀다(계획서 325-3) — 같은 몸통(`attachPopout`), 창 이름만 다름. * * 화면 조각(DOM)만 옮긴다 — 코드 · 자료 · 가게는 메인 창 그대로, 같은 함수가 그린다. * 창 전역에 걸린 듣기(방향키 · 포인터 놓기 · 메뉴 닫기)는 메인 창에만 있으므로 새 창에서 난 @@ -13,28 +14,39 @@ import type { DesignProfile } from "./design_profile"; /** 메인 창 문서 · 창에 걸린 듣기가 받아야 할 새 창 이벤트. */ const FORWARD_TYPES = ["keydown", "keyup", "pointerdown", "pointerup", "pointercancel"]; -/** 종단 도구줄 [초기화] 왼쪽 단추(`toolLead`)를 [창 분할] 단추로 꾸민다 — 두 보기가 같은 한 벌을 쓴다. */ -export function attachProfilePopout( - profile: DesignProfile, - slot: HTMLElement, - button: HTMLButtonElement, -): void { - const { root } = profile; +/** 요소가 놓인 창(문서에 안 붙었으면 메인 창). */ +const viewOfNode = (node: Node): Window => node.ownerDocument?.defaultView ?? window; + +/** 조각 하나를 새 창으로 떼는 단추 — 창 이름은 조각마다 달라야 함(같으면 앞 창 body 를 덮어 조각이 사라짐). */ +interface PopoutSpec { + piece: HTMLElement; + button: HTMLButtonElement; + name: string; + features: string; + docTitle: string; + /** 조각을 제자리로 돌림. */ + home: () => void; + /** 뗐다 · 붙였다(조각이 옮겨 간 뒤). */ + onMove?: (popped: boolean) => void; +} + +/** 조각을 새 창으로 떼고 붙이는 몸통 — 새 창은 단추가 놓인 창에서 엶(종단 새 창 안에서 다시 떼기). */ +function attachPopout(profile: DesignProfile, spec: PopoutSpec): void { + const { piece, button } = spec; let popup: Window | null = null; button.type = "button"; - button.className = "ui-btn ui-btn--ghost b05-route-profile__popout"; button.textContent = "창 분할"; - button.title = "종단 하단 패널을 별도 창으로 분할 · 다시 누르면 결합"; button.addEventListener("click", () => (popup && !popup.closed ? closePopup() : open())); const restore = (): void => { - if (!root.classList.contains("is-popout")) return; - root.classList.remove("is-popout"); + if (!piece.classList.contains("is-popout")) return; + piece.classList.remove("is-popout"); button.textContent = "창 분할"; - slot.append(root); + spec.home(); popup = null; - requestAnimationFrame(profile.redraw); + spec.onMove?.(false); + viewOfNode(piece).requestAnimationFrame(profile.redraw); }; const closePopup = (): void => { popup?.close(); @@ -49,28 +61,31 @@ export function attachProfilePopout( }; function open(): void { - const next = window.open("", "aislo-profile-popout", "popup,width=1400,height=720"); + const opener = viewOfNode(button); + const next = opener.open("", spec.name, spec.features); if (!next) { showToast("새 창이 막힘 — 주소창의 팝업 허용을 켠 뒤 다시 누름", "error"); return; } popup = next; + const source = opener.document; const doc = next.document; - doc.title = "종단 — Aislo"; - // 스타일 · 테마 — 메인 창 시트 · html 속성을 그대로 옮겨 같은 색 · 글꼴로 그린다. + doc.title = spec.docTitle; + // 스타일 · 테마 — 단추가 놓인 창의 시트 · html 속성을 그대로 옮겨 같은 색 · 글꼴로 그린다. doc.head.replaceChildren( - ...Array.from(document.querySelectorAll('link[rel="stylesheet"], style'), (node) => + ...Array.from(source.querySelectorAll('link[rel="stylesheet"], style'), (node) => doc.importNode(node, true), ), ); - Array.from(document.documentElement.attributes).forEach((attr) => + Array.from(source.documentElement.attributes).forEach((attr) => doc.documentElement.setAttribute(attr.name, attr.value), ); - doc.body.className = document.body.className; - doc.body.style.cssText = `margin:0;height:100dvh;overflow:hidden;background:${getComputedStyle(document.body).backgroundColor}`; - root.classList.add("is-popout"); + doc.body.className = source.body.className; + doc.body.style.cssText = `margin:0;height:100dvh;overflow:hidden;background:${opener.getComputedStyle(source.body).backgroundColor}`; + piece.classList.add("is-popout"); button.textContent = "창 결합"; - doc.body.replaceChildren(root); + doc.body.replaceChildren(piece); + spec.onMove?.(true); // 새 창 키 · 포인터를 메인 문서로 — 대상은 새 창 요소 그대로(입력칸 판정 · 메뉴 안 판정). FORWARD_TYPES.forEach((type) => @@ -93,7 +108,75 @@ export function attachProfilePopout( new (next as Window & typeof globalThis).ResizeObserver(() => { next.cancelAnimationFrame(pending); pending = next.requestAnimationFrame(profile.redraw); - }).observe(root); + }).observe(piece); next.addEventListener("pagehide", restore); } } + +/** 종단 도구줄 [초기화] 왼쪽 단추(`toolLead`)를 [창 분할] 단추로 꾸민다 — 두 보기가 같은 한 벌을 쓴다. */ +export function attachProfilePopout( + profile: DesignProfile, + slot: HTMLElement, + button: HTMLButtonElement, +): void { + button.className = "ui-btn ui-btn--ghost b05-route-profile__popout"; + button.title = "종단 하단 패널을 별도 창으로 분할 · 다시 누르면 결합"; + attachPopout(profile, { + piece: profile.root, + button, + name: "aislo-profile-popout", + features: "popup,width=1400,height=720", + docTitle: "종단 — Aislo", + home: () => slot.append(profile.root), + }); +} + +/** 유토곡선 · 배수유역 판에 [창 분할] 단추를 단다 — 유토 판 좌상단 · 배수 머리줄 맨 앞(계획서 325-3). */ +export function attachSubPanelPopouts(profile: DesignProfile): void { + const subButton = (title: string): HTMLButtonElement => { + const button = document.createElement("button"); + button.className = "ui-btn ui-btn--ghost b05-subpanel__popout"; + button.title = title; + return button; + }; + + const massOverlay = profile.root.querySelector(".b05-profile__masshaul-overlay"); + const massHandle = profile.root.querySelector(".b05-profile__masshaul-handle"); + if (massOverlay && massHandle) { + const button = subButton("유토곡선 창 분할"); + massOverlay.append(button); + attachPopout(profile, { + piece: massOverlay, + button, + name: "aislo-masshaul-popout", + features: "popup,width=1400,height=420", + docTitle: "유토곡선 — Aislo", + // 제자리 = 유토 손잡이 바로 앞(종단 본문 바닥 겹침 판). + home: () => massHandle.before(massOverlay), + onMove: (popped) => { + // 뗀 동안 메인 손잡이는 숨김 · 같이 줄이기 인라인 높이는 비움(새 창은 창 높이를 다 씀). + massHandle.hidden = popped; + massOverlay.style.height = ""; + }, + }); + } + + const drainage = profile.side; + const header = drainage?.root.querySelector(".b05-drainage__header"); + if (drainage && header) { + const button = subButton("배수유역 창 분할"); + header.prepend(button); + const content = drainage.root.parentElement; + attachPopout(profile, { + piece: drainage.root, + button, + name: "aislo-drainage-popout", + features: "popup,width=1000,height=720", + docTitle: "배수유역 — Aislo", + // 제자리 = 종단 content 끝(종단 본문 오른쪽). + home: () => content?.append(drainage.root), + // 지도 크기 관찰을 놓인 창 것으로(325-1). + onMove: () => drainage.rehome(), + }); + } +} diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index 9296eb899..3374528c1 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -24,7 +24,7 @@ import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_U import { drainageSide } from "../../B05_Profile/B05_Profile_UI_Profile_SideTool"; import { createDesignPanel, type DesignPanel } from "./design_panel"; import { createDesignProfile, type DesignProfile } from "./design_profile"; -import { attachProfilePopout } from "./design_profile_popout"; +import { attachProfilePopout, attachSubPanelPopouts } from "./design_profile_popout"; import { saveDesign, type DesignSaveStep } from "./design_save"; import { resetDesign } from "./design_reset"; import type { DesignView } from "./design_dock"; @@ -179,6 +179,7 @@ export function sharedProfile(shared: DesignPageShared): DesignProfile { }); shared.profileSlot.append(profile.root); attachProfilePopout(profile, shared.profileSlot, popoutButton); + attachSubPanelPopouts(profile); shared.profile = profile; return profile; } diff --git a/resources/tester/helper_224_1c_subpanel_popout.cjs b/resources/tester/helper_224_1c_subpanel_popout.cjs new file mode 100644 index 000000000..c48685322 --- /dev/null +++ b/resources/tester/helper_224_1c_subpanel_popout.cjs @@ -0,0 +1,215 @@ +/* 224-1c 유토곡선 · 배수유역 [창 분할] — 진짜 `attachSubPanelPopouts` · `attachProfilePopout` + * (design_profile_popout.ts)를 가짜 DOM · 가짜 창 위에서 돌려 JSON 으로 냄. + * · 단추 자리 · 글 · 클래스 · 툴팁 · 창 이름 셋 다름 + * · 뗌 = 새 창 body · 유토 손잡이 숨김 · 인라인 높이 비움 · 배수 rehome + * · 붙임(단추 · 새 창 닫힘 · dispose) = 제자리(유토 손잡이 앞 · content 끝) + * · 종단 새 창 안의 단추 = 그 창에서 엶 */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8"); + +// ---- 가짜 DOM — 부모 사슬 끝이 어느 문서 body 냐로 ownerDocument 를 정함 ---- +let mainDoc = null; +function el(tag, className = "") { + const node = { + tagName: tag.toUpperCase(), + className, + textContent: "", + title: "", + type: "", + hidden: false, + children: [], + parent: null, + listeners: {}, + style: { height: "", cssText: "" }, + }; + node.classList = { + list: () => node.className.split(/\s+/).filter(Boolean), + contains: (c) => node.classList.list().includes(c), + add: (c) => + !node.classList.contains(c) && (node.className = [...node.classList.list(), c].join(" ")), + remove: (c) => + (node.className = node.classList + .list() + .filter((x) => x !== c) + .join(" ")), + }; + Object.defineProperty(node, "parentElement", { get: () => node.parent }); + Object.defineProperty(node, "ownerDocument", { + get: () => { + let top = node; + while (top.parent) top = top.parent; + return top.doc ?? mainDoc; + }, + }); + node.detach = () => { + if (node.parent) node.parent.children = node.parent.children.filter((c) => c !== node); + node.parent = null; + }; + node.append = (...kids) => + kids.forEach((k) => (k.detach(), (k.parent = node), node.children.push(k))); + node.prepend = (...kids) => + kids.reverse().forEach((k) => (k.detach(), (k.parent = node), node.children.unshift(k))); + node.replaceChildren = (...kids) => { + node.children.slice().forEach((c) => c.detach()); + node.append(...kids); + }; + node.before = (other) => { + const parent = node.parent; + other.detach(); + other.parent = parent; + parent.children.splice(parent.children.indexOf(node), 0, other); + }; + node.querySelector = (sel) => { + const cls = sel.replace(/^\./, ""); + for (const kid of node.children) { + if (kid.classList.contains(cls)) return kid; + const deep = kid.querySelector(sel); + if (deep) return deep; + } + return null; + }; + node.addEventListener = (type, fn) => (node.listeners[type] ??= []).push(fn); + node.click = () => (node.listeners.click ?? []).forEach((fn) => fn({})); + return node; +} +function fakeDoc(view) { + const doc = { title: "", defaultView: view }; + doc.head = el("head"); + doc.body = el("body"); + doc.body.doc = doc; + doc.documentElement = Object.assign(el("html"), { attributes: [], setAttribute() {} }); + doc.querySelectorAll = () => []; + doc.importNode = (n) => n; + doc.createElement = (tag) => el(tag); + return doc; +} +const opened = []; // [여는 창 이름, 새 창 이름] +function fakeWindow(label) { + const win = { label, listeners: {}, closed: false, frames: 0 }; + win.document = fakeDoc(win); + win.addEventListener = (t, fn) => (win.listeners[t] ??= []).push(fn); + win.removeEventListener = () => {}; + win.getComputedStyle = () => ({ backgroundColor: "" }); + win.requestAnimationFrame = () => (win.frames += 1); + win.cancelAnimationFrame = () => {}; + win.ResizeObserver = class { + observe() {} + }; + win.close = () => (win.closed = true); + win.fire = (t) => (win.listeners[t] ?? []).forEach((fn) => fn({})); + win.open = (_url, name) => { + const next = fakeWindow(name); + opened.push([label, name]); + return next; + }; + return win; +} +const main = fakeWindow("main"); +mainDoc = main.document; + +const js = ts.transpileModule(read("A00_Common", "design", "design_profile_popout.ts"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, +}).outputText; +const mod = {}; +new Function("exports", "require", "window", "document", js)( + mod, + () => ({ showToast() {} }), + main, + mainDoc, +); + +// ---- 종단 뿌리 = [content = [bodyWrap = [유토 판, 유토 손잡이], 배수 판 = [머리줄 = [제목]]]] ---- +const slot = el("div"); +mainDoc.body.append(slot); +const root = el("div", "b05-route-profile"); +slot.append(root); +const content = el("div", "b05-route-profile__content"); +const bodyWrap = el("div"); +const massOverlay = el("div", "b05-profile__masshaul-overlay"); +const massHandle = el("div", "b05-profile__masshaul-handle"); +bodyWrap.append(massOverlay, massHandle); +const drainageRoot = el("aside", "b05-drainage"); +const header = el("div", "b05-drainage__header"); +header.append(el("h3")); +drainageRoot.append(header); +content.append(bodyWrap, drainageRoot); +root.append(content); +let rehomes = 0; +let disposed = 0; +const profile = { + root, + redraw() {}, + dispose: () => (disposed += 1), + side: { root: drainageRoot, rehome: () => (rehomes += 1) }, +}; +const profileButton = el("button"); +mod.attachProfilePopout(profile, slot, profileButton); +mod.attachSubPanelPopouts(profile); + +const massButton = massOverlay.children.find((c) => c.tagName === "BUTTON"); +const drainButton = header.children[0]; +const btn = (b) => ({ text: b.textContent, title: b.title, className: b.className, type: b.type }); +const out = { mass: btn(massButton), drainage: btn(drainButton), profile: btn(profileButton) }; + +// 유토 뗌 +massOverlay.style.height = "123px"; +massButton.click(); +const massPopup = massOverlay.parent?.doc?.defaultView; +out.massOpened = { + inPopupBody: massOverlay.parent === massPopup?.document.body, + isPopout: massOverlay.classList.contains("is-popout"), + handleHidden: massHandle.hidden, + inline: massOverlay.style.height, + text: massButton.textContent, + docTitle: massPopup?.document.title, +}; +// 배수 뗌 +drainButton.click(); +const drainPopup = drainageRoot.parent?.doc?.defaultView; +out.drainOpened = { + inPopupBody: drainageRoot.parent === drainPopup?.document.body, + rehomes, + text: drainButton.textContent, +}; +// 유토 새 창 닫힘 → 제자리(손잡이 바로 앞) +massPopup.fire("pagehide"); +out.massClosed = { + order: bodyWrap.children.map((c) => c.className), + handleHidden: massHandle.hidden, + isPopout: massOverlay.classList.contains("is-popout"), + text: massButton.textContent, +}; +// 배수 단추 다시 누름 → 창 닫고 content 끝 +drainButton.click(); +out.drainClosed = { + popupClosed: drainPopup.closed, + last: content.children[content.children.length - 1] === drainageRoot, + rehomes, + text: drainButton.textContent, +}; + +// 종단 새 창 → 그 안에서 유토 뗌 = 종단 새 창이 엶 +profileButton.click(); +const profilePopup = root.parent?.doc?.defaultView; +massButton.click(); +const massPopup2 = massOverlay.parent?.doc?.defaultView; +out.nested = { profileInPopup: !!profilePopup && profilePopup !== main }; +// 페이지 떠남(dispose) → 셋 다 닫히고 제자리 +drainButton.click(); +const drainPopup2 = drainageRoot.parent?.doc?.defaultView; +profile.dispose(); +out.disposed = { + closed: [profilePopup.closed, massPopup2.closed, drainPopup2.closed], + profileInSlot: root.parent === slot, + massBeforeHandle: + bodyWrap.children.indexOf(massOverlay) === bodyWrap.children.indexOf(massHandle) - 1, + drainInContent: drainageRoot.parent === content, + handleHidden: massHandle.hidden, + original: disposed, +}; +out.opened = opened; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_282_1_profile_toolbar.cjs b/resources/tester/helper_282_1_profile_toolbar.cjs index e24598ca9..1fe0671b6 100644 --- a/resources/tester/helper_282_1_profile_toolbar.cjs +++ b/resources/tester/helper_282_1_profile_toolbar.cjs @@ -84,6 +84,9 @@ const window = { addEventListener: (t, fn) => (window.listeners[t] ??= []).push(fn), removeEventListener: () => {}, open: () => popup, + document, + getComputedStyle: () => ({ backgroundColor: "" }), + requestAnimationFrame: (fn) => frames.push(fn), }; const popupListeners = {}; const popup = { diff --git a/resources/tester/test_224_1c_subpanel_popout.py b/resources/tester/test_224_1c_subpanel_popout.py new file mode 100644 index 000000000..df926a112 --- /dev/null +++ b/resources/tester/test_224_1c_subpanel_popout.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""224-1c(PLAN 325-3) 유토곡선 · 배수유역 [창 분할] — 종단 [창 분할]과 같은 양식 · 따로 새 창. + +- 단추 = 유토 판 좌상단 · 배수 머리줄 맨 앞 · 글 [창 분할]/[창 결합] · 툴팁에 판 이름. +- 창 이름 셋 다름(같으면 앞 창 body 를 덮어 조각이 사라짐). +- 뗌 = 새 창 body · 유토 손잡이 숨김 · 인라인 높이 비움 · 배수 지도 관찰 rehome. +- 붙임(단추 · 새 창 닫힘 · 페이지 떠남) = 제자리 — 유토 손잡이 바로 앞 · 배수 content 끝. +- 종단 새 창 안에서 다시 떼면 그 창이 새 창을 엶(메인 창 팝업 차단 피함). + +`helper_224_1c_subpanel_popout.cjs` 가 진짜 TS 를 가짜 DOM · 가짜 창 위에서 돌림. +""" + +import json +import os +import re +import subprocess +from pathlib import Path + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = Path(HERE).parents[1] +NAMES = ["aislo-profile-popout", "aislo-masshaul-popout", "aislo-drainage-popout"] + + +@pytest.fixture(scope="module") +def r(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_224_1c_subpanel_popout.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_단추_글_툴팁_클래스(r): + cls = "ui-btn ui-btn--ghost b05-subpanel__popout" + assert r["mass"] == { + "text": "창 분할", + "title": "유토곡선 창 분할", + "className": cls, + "type": "button", + } + assert r["drainage"] == { + "text": "창 분할", + "title": "배수유역 창 분할", + "className": cls, + "type": "button", + } + assert "b05-route-profile__popout" in r["profile"]["className"] + + +def test_유토_뗌(r): + assert r["massOpened"] == { + "inPopupBody": True, + "isPopout": True, + "handleHidden": True, + "inline": "", + "text": "창 결합", + "docTitle": "유토곡선 — Aislo", + } + + +def test_배수_뗌은_rehome(r): + assert r["drainOpened"] == {"inPopupBody": True, "rehomes": 1, "text": "창 결합"} + + +def test_새_창_닫힘과_단추로_제자리(r): + assert r["massClosed"] == { + "order": ["b05-profile__masshaul-overlay", "b05-profile__masshaul-handle"], + "handleHidden": False, + "isPopout": False, + "text": "창 분할", + } + assert r["drainClosed"] == {"popupClosed": True, "last": True, "rehomes": 2, "text": "창 분할"} + + +def test_창_이름_셋_다름과_여는_창(r): + names = [name for _, name in r["opened"]] + assert set(names) == set(NAMES) and len(set(NAMES)) == 3 + # 종단 새 창 안의 단추는 종단 새 창이 엶 + assert r["opened"][3] == ["aislo-profile-popout", "aislo-masshaul-popout"] + assert r["nested"]["profileInPopup"] is True + + +def test_페이지_떠나면_셋_다_닫고_제자리(r): + assert r["disposed"] == { + "closed": [True, True, True], + "profileInSlot": True, + "massBeforeHandle": True, + "drainInContent": True, + "handleHidden": False, + "original": 1, + } + + +def test_배선과_양식(): + shared = (ROOT / "A00_Common/design/design_shared.ts").read_text(encoding="utf-8") + assert "attachSubPanelPopouts(profile);" in shared + css = (ROOT / "A00_Common/design/design_page.css").read_text(encoding="utf-8") + block = re.search(r"\n\.b05-subpanel__popout \{([^}]*)\}", css).group(1) + assert "var(--btn-bar-padding)" in block and "var(--btn-bar-font-size)" in block + assert ".b05-profile__masshaul-overlay.is-popout," in css + assert ".b05-drainage.is-popout {" in css