"""244-10 B08 거리이정표 경로 지도 — 테일스케일 주소(http://100.72.32.27:5173 · 보안 문맥 아님). ① getDisplayMedia 없음 = 지도 그대로 · 캡처 안내 없음([지도 캡처]는 244-11 에서 없앰 · 그림 = 서버 자동) ② SDK 못 받음(script onerror · 도메인 미등록 401) = 지금 주소(location.origin) 등록 안내 한 줄 · 지도 숨김 (가짜 kakao.maps · document 로 node 에서 돌림) """ import json import re import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) LAYOUT = "B08_Quantity/B08_Quantity_Milestone_Layout.ts" KMAP = "B08_Quantity/B08_Quantity_UI_Milestone_KakaoMap.ts" ORIGIN = "http://100.72.32.27:5173" def _read(rel): return (ROOT / rel).read_text(encoding="utf-8") STUBS = f""" const el = (tag: string, o: any = {{}}) => ({{ tag, hidden: false, textContent: o.text ?? "", className: o.className, dataset: {{}}, children: o.children ?? [] }}); const requestAnimationFrame = (f: () => void) => setTimeout(f, 0); const request = async () => ({{ key: "fake-js-key" }}); const injected: string[] = []; (globalThis as any).window = globalThis; (globalThis as any).location = {{ origin: {json.dumps(ORIGIN)} }}; const liveMilestone = () => ({{ site: {{}}, base: {{ enabled: false }}, routes: [{{ id: "r1", name: "거창군청", lon: 127.9, lat: 35.68, via_base: false }}] }}); const siteInfo = () => ({{ start_lonlat: [127.83, 35.64] }}); const routePaths = () => [[[35.68, 127.9], [35.64, 127.83]]]; """ FAKE_KAKAO = """ class Fake { setMap() {} relayout() {} setBounds() {} extend() {} } (globalThis as any).kakao = { maps: { load: (f: () => void) => f(), LatLng: Fake, LatLngBounds: Fake, Map: Fake, Polyline: Fake, Marker: Fake, CustomOverlay: Fake, event: { addListener: () => {} } } }; """ def _view(setup): script = ( STUBS + setup + IMPORTS.sub("", _read(KMAP)) + """ // 156 경로 탭 지도 칸 없앰 — 같은 SDK 싣기 · 안내 길 = 연결선 모달 지도 칸(editMap) const view = editMap("p1", { onMove: () => {}, hint: "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" }); view.set([{ key: "r1", name: "거창군청", lon: 127.9, lat: 35.68, movable: true }], routePaths()); await new Promise((r) => setTimeout(r, 30)); const [hint, box] = view.root.children; console.log(JSON.stringify({ injected, hint: hint.textContent, boxHidden: box.hidden, children: view.root.children.length })); """ ) src = IMPORTS.sub("", _read(LAYOUT)) + script # 254 — 글이 길어 명령 줄(Windows 32767자)을 넘지 않게 stdin 으로 넘김 result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-"], input=src, capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert result.returncode == 0, result.stderr return json.loads(result.stdout) NO_MEDIA = """ Object.defineProperty(globalThis, "navigator", { configurable: true, value: {} }); (globalThis as any).document = { createElement: () => ({}), head: { append: () => {} } }; """ def test_보안_문맥_아님_지도_그대로_캡처_안내_없음(): got = _view(NO_MEDIA + FAKE_KAKAO) assert not got["boxHidden"] and got["children"] == 2 # 지도는 그대로 · 캡처 단추 없음 assert got["hint"] == "길 선 = 카카오 길찾기 · 경로 그림은 서버가 자동으로 만듦" assert "캡처는 localhost" not in got["hint"] def test_SDK_401_도메인_등록_안내(): sdk_401 = """ (globalThis as any).document = { createElement: () => ({}), head: { append: (s: any) => (injected.push(s.src), setTimeout(() => s.onerror(), 0)) } }; """ got = _view(NO_MEDIA + sdk_401) # window.kakao 없음 → script 넣음 → 401 = onerror assert len(got["injected"]) == 1 and "sdk.js?appkey=fake-js-key" in got["injected"][0] assert got["hint"] == ( f"카카오 JavaScript 키 도메인에 지금 주소({ORIGIN}) 등록 필요 — 지도 대신 [그림 변경]으로 넣으세요" ) assert got["boxHidden"]