feat(설정): 332-2 ☰ [설정] 창 — 마우스 · CAD 단축키 · 다른 곳 단축키 탭 · [저장] 때만 계정 pref · 닫을 때 물음
- app_shell ☰ 설정 묶음에 [설정] 단추(서랍 닫고 창은 처음 누를 때 불러옴) - 새 app_settings.ts(틀 · 탭 셋 · [저장] · askLeave) · app_settings_cad.ts(명령 표 · 내 별칭 · 검사) · app_settings.css - 새 openwebcad/src/commands/command-catalog.json(명령 153 · 한글 이름 · 기본 별칭) + 거울 시험 - locale 문구(ui_template_locale_common) - 시험: test_app_settings · cad/command-catalog 새로 · test_device_audit · test_touch_bridge 화면 주소 환경값(AISLO_UI_BASE · AISLO_UI_PROJECT) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* CAD 명령 목록 거울(PLAN 332-2) — `commands/command-catalog.json` 이 registry(`commands.*.ts`)와 같은지.
|
||||
* ☰ [설정] CAD 단축키 탭은 xstate 가 딸려 오지 않게 JSON 만 읽음 — 명령을 더하거나 별칭을 바꾸면 JSON 도 같이.
|
||||
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad command-catalog
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import catalog from "../../../B07_DesignDetail/openwebcad/src/commands/command-catalog.json";
|
||||
import { getAllCommands } from "../../../B07_DesignDetail/openwebcad/src/commands/registry";
|
||||
|
||||
describe("command-catalog.json", () => {
|
||||
it("registry 와 같은 명령 · 한글 이름 · 기본 별칭 · 차례", () => {
|
||||
const live = getAllCommands().map((c) => ({
|
||||
id: c.id,
|
||||
label: c.label,
|
||||
aliases: c.aliases ?? [],
|
||||
}));
|
||||
expect(catalog).toEqual(live);
|
||||
});
|
||||
|
||||
it("기본 별칭 = 영문 · 숫자 · 안 겹침 · 명령 이름과 다름", () => {
|
||||
const ids = new Set(catalog.map((c) => c.id));
|
||||
const aliases = catalog.flatMap((c) => c.aliases);
|
||||
expect(aliases.every((a) => /^[A-Z0-9]+$/.test(a))).toBe(true);
|
||||
expect(new Set(aliases).size).toBe(aliases.length);
|
||||
expect(aliases.filter((a) => ids.has(a))).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/* PLAN 332-2 단위검증 헬퍼 — ☰ [설정] CAD 별칭 검사(`A00_Common/app_settings_cad.ts`)를 Node 로 돌림.
|
||||
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_app_settings.py`. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const file = path.join(ROOT, "A00_Common", "app_settings_cad.ts");
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
const req = (p) =>
|
||||
p.endsWith(".json")
|
||||
? require(path.join(ROOT, "A00_Common", p))
|
||||
: p === "@ui/ui_template_locale"
|
||||
? { t: (k) => k }
|
||||
: {};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
const m = box.exports;
|
||||
|
||||
const check = (aliases) => m.checkCadAliases(aliases);
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
commands: m.CAD_COMMANDS.length,
|
||||
parse: [m.parseAliases(" ll, Cc x1 "), m.parseAliases(""), m.parseAliases(",, ")],
|
||||
ok: check({ LINE: ["LL"], CIRCLE: ["CC", "C2"], COPY: ["C"] }),
|
||||
chars: check({ LINE: ["L!"], CIRCLE: ["원"] }),
|
||||
dup: check({ LINE: ["LL"], CIRCLE: ["LL"], ARC: ["AA", "AA"] }),
|
||||
name: check({ CIRCLE: ["LINE"] }),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,187 @@
|
||||
"""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()
|
||||
@@ -12,14 +12,16 @@ sub1 서버(5174)에서 모든 페이지를 네 크기로 열어 문제 수를
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BASE = "http://localhost:5174"
|
||||
PROJECT_NAME = "검증_sub1"
|
||||
# 다른 창 서버로 돌릴 때 — AISLO_UI_BASE · AISLO_UI_PROJECT(OWNERS 자기 줄)
|
||||
BASE = os.environ.get("AISLO_UI_BASE", "http://localhost:5174")
|
||||
PROJECT_NAME = os.environ.get("AISLO_UI_PROJECT", "검증_sub1")
|
||||
PAGES = [
|
||||
"a01-home",
|
||||
"a02-prog-detail",
|
||||
|
||||
@@ -8,11 +8,12 @@ sub1 화면 서버(5174) 첫 화면을 터치 기기로 열고 시험용 상자
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
BASE = "http://localhost:5174"
|
||||
BASE = os.environ.get("AISLO_UI_BASE", "http://localhost:5174") # 다른 창 서버 = 환경값
|
||||
|
||||
|
||||
def _up() -> bool:
|
||||
|
||||
Reference in New Issue
Block a user