"""PLAN 332-2 — ☰ [설정] 창(`A00_Common/app_settings.ts` · `app_settings_cad.ts` · `app_shell.ts`). ① CAD 별칭 검사(Node) — 영문 · 숫자만 · 겹침 금지 · 명령 이름 금지 · 칸 글 나눔 ② 실제 브라우저(sub2 화면 서버 5177 · 로그인 없음 = 이 브라우저만 · 새 창이라 남는 것 없음) ☰ → [설정] = 서랍 닫히고 창 · 탭 셋 전환 · 고쳐도 [저장] 전엔 안 씀 · 닫을 때 물음(취소 = 머묾 · 버림 = 안 씀 · 저장 = 한 번) · [저장] 한 번 = 한 번 씀 · 저장 뒤 읽기 층 방향 뒤집힘 · 잘못 별칭은 안 씀 """ from __future__ import annotations import json import os import subprocess import urllib.request import pytest HERE = os.path.dirname(__file__) ROOT = os.path.join(HERE, "..", "..") BASE = "http://localhost:5177" # sub2 화면 서버(OWNERS 자기 줄) KEY = "aislo:pref:input-prefs" # 앱 스크립트보다 먼저 — 설정 키에 쓰는 횟수를 셈 COUNT_WRITES = f""" window.__writes = []; const set = Storage.prototype.setItem; Storage.prototype.setItem = function (k, v) {{ if (k === {json.dumps(KEY)}) window.__writes.push(v); return set.call(this, k, v); }}; """ # 앱이 불러온 그 읽기 층 사본(vite 가 ?t= 를 붙였을 수 있음) WHEEL_IN = ( "async (d) => (await import(performance.getEntriesByType('resource')" ".map((e) => e.name).find((n) => n.includes('/input_prefs.ts')))).wheelIn(d)" ) @pytest.fixture(scope="module") def node() -> dict: proc = subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", os.path.join(HERE, "helper_app_settings.cjs")], capture_output=True, text=True, encoding="utf-8", timeout=60, ) assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) def test_별칭_검사(node): assert node["commands"] == 153 assert node["parse"] == [["LL", "CC", "X1"], [], []] assert node["ok"] == {} # 남의 기본 별칭(C = CIRCLE)을 내 것으로 덮는 건 됨(acad.pgp 식) assert node["chars"] == {"LINE": "chars", "CIRCLE": "chars"} assert node["dup"] == {"LINE": "dup", "CIRCLE": "dup", "ARC": "dup"} assert node["name"] == {"CIRCLE": "name"} def _up() -> bool: try: urllib.request.urlopen(BASE, timeout=3) return True except Exception: return False def _open_settings(page): page.click(".app-burger") page.click(".app-drawer__settings") page.wait_for_selector(".app-settings") assert not page.evaluate( "document.querySelector('.app-header, header')?.classList.contains('is-drawer-open')" ) def _writes(page) -> list: return page.evaluate("window.__writes") def _visible_panel_has(page, selector: str) -> bool: return page.evaluate( "(s) => [...document.querySelectorAll('.app-settings .ui-modal__body > div:not([role]), " ".app-settings .ui-modal__body > table')].some((p) => !p.hidden && p.querySelector(s))", selector, ) def _save(page): page.click(".app-settings .ui-modal__actions button") def _close(page): page.click(".app-settings .ui-modal__top > button") @pytest.mark.skipif(not _up(), reason="sub2 화면 서버(5177) 꺼짐") def test_설정_창_탭_저장_물음(): from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(channel="msedge", headless=True) ctx = browser.new_context( viewport={"width": 390, "height": 844}, has_touch=True, is_mobile=True ) ctx.add_init_script(COUNT_WRITES) page = ctx.new_page() page.goto(f"{BASE}/#/a01-home") page.wait_for_selector(".app-burger") assert page.evaluate(WHEEL_IN, 100) is True # 기본 = 당김 # ☰ → [설정] · 탭 셋 · 첫 탭 = 마우스 _open_settings(page) tabs = page.evaluate( "[...document.querySelectorAll('.app-settings [role=tab]')]" ".map((b) => [b.dataset.settingsTab, b.getAttribute('aria-selected')])" ) assert tabs == [["mouse", "true"], ["cad", "false"], ["keys", "false"]] assert _visible_panel_has(page, "input[type=radio]") page.click("[data-settings-tab=cad]") assert _visible_panel_has(page, "input[data-cad-command]") assert not _visible_panel_has(page, "input[type=radio]") assert page.locator(".app-settings input[data-cad-command]").count() == 153 page.click("[data-settings-tab=keys]") assert ( page.evaluate( "document.querySelector('[data-settings-tab=keys]').getAttribute('aria-selected')" ) == "true" ) page.click("[data-settings-tab=mouse]") # 밀기 고름 — [저장] 전엔 안 씀 · 닫으면 물음 · 취소 = 머묾 page.check("input[name=app-settings-wheel][value=push]") assert _writes(page) == [] page.keyboard.press("Escape") page.wait_for_selector(".ui-confirm") page.click(".ui-confirm button >> nth=0") # 취소 assert page.locator(".app-settings").count() == 1 and _writes(page) == [] # [저장] = 한 번 씀 · 읽기 층 방향 뒤집힘 · 닫을 때 안 물음 _save(page) assert len(_writes(page)) == 1 assert json.loads(_writes(page)[0]) == {"wheelZoom": "push", "cadAliases": {}} assert page.evaluate(WHEEL_IN, 100) is False and page.evaluate(WHEEL_IN, -100) is True _save(page) # 고친 것 없음 — 또 안 씀 assert len(_writes(page)) == 1 _close(page) assert page.locator(".app-settings").count() == 0 assert page.locator(".ui-confirm").count() == 0 # CAD 별칭 — 잘못이면 안 씀 · 칸 표시 _open_settings(page) assert page.is_checked("input[name=app-settings-wheel][value=push]") # 저장값으로 엶 page.click("[data-settings-tab=cad]") line = "input[data-cad-command=LINE]" circle = "input[data-cad-command=CIRCLE]" for line_v, circle_v in (("L!", ""), ("LL", "ll"), ("", "LINE")): page.fill(line, line_v) page.fill(circle, circle_v) _save(page) assert page.inner_text(".app-settings__error").strip(), (line_v, circle_v) assert page.locator(".app-settings input[aria-invalid=true]").count() >= 1 assert len(_writes(page)) == 1 page.fill(line, "ll") page.fill(circle, "") _save(page) assert page.inner_text(".app-settings__error").strip() == "" assert len(_writes(page)) == 2 assert json.loads(_writes(page)[1]) == {"wheelZoom": "push", "cadAliases": {"LINE": ["LL"]}} # 닫을 때 물음 — 버림 = 안 씀 · 저장 = 한 번 더 page.click("[data-settings-tab=mouse]") page.check("input[name=app-settings-wheel][value=pull]") _close(page) page.click(".ui-confirm button >> nth=1") # 버리고 넘어감 assert page.locator(".app-settings").count() == 0 and len(_writes(page)) == 2 assert page.evaluate(WHEEL_IN, 100) is False _open_settings(page) page.check("input[name=app-settings-wheel][value=pull]") _close(page) page.click(".ui-confirm button >> nth=2") # 저장 후 넘어감 assert page.locator(".app-settings").count() == 0 and len(_writes(page)) == 3 assert json.loads(_writes(page)[2])["wheelZoom"] == "pull" assert page.evaluate(WHEEL_IN, 100) is True browser.close()