"""손가락 → 마우스 번역(`A00_Common/touch_bridge.ts` · PLAN_기기대응 3-1) — 실제 브라우저 터치. sub1 화면 서버(5174) 첫 화면을 터치 기기로 열고 시험용 상자에 CDP 터치를 넣어 봄: ① 길게 누름 → contextmenu 한 번 · 뒤따르는 click 없음 ② 짧은 톡 → contextmenu 없음 ③ 두 번 톡 → dblclick 한 번 ④ 두 손가락 벌림 → Ctrl+휠(deltaY < 0) · 오므림 → deltaY > 0 ⑤ [data-touch-own] 상자는 번역 안 함 ⑥ 마우스 우클릭 · 더블클릭은 그대로(번역이 끼지 않음) ⑦ 고름 반경 `pickPx` — 손가락이면 24px 이상 · 마우스면 그대로 """ import json import urllib.request import pytest BASE = "http://localhost:5174" def _up() -> bool: try: urllib.request.urlopen(BASE, timeout=2) return True except Exception: return False SETUP = """ () => { window.__log = []; for (const id of ['tb-box', 'tb-own']) { const b = document.createElement('div'); b.id = id; Object.assign(b.style, { position: 'fixed', left: id === 'tb-box' ? '20px' : '220px', top: '120px', width: '180px', height: '180px', background: '#eee', zIndex: 99999, touchAction: 'none' }); if (id === 'tb-own') b.dataset.touchOwn = 'x'; for (const t of ['contextmenu', 'click', 'dblclick', 'wheel']) b.addEventListener(t, (e) => { window.__log.push({ id, t, ctrl: !!e.ctrlKey, dy: e.deltaY || 0, trusted: e.isTrusted }); if (t === 'contextmenu' || t === 'wheel') e.preventDefault(); }, { passive: false }); document.body.append(b); } } """ def _touch(cdp, kind, points): cdp.send( "Input.dispatchTouchEvent", { "type": kind, "touchPoints": [{"x": x, "y": y, "id": i} for i, (x, y) in enumerate(points)], }, ) def _log(page): out = page.evaluate("() => window.__log.splice(0)") return [(e["id"], e["t"]) for e in out], out @pytest.mark.skipif(not _up(), reason="sub1 화면 서버(5174) 꺼짐") def test_touch_bridge_translates_finger_gestures(): 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 ) page = ctx.new_page() page.goto(f"{BASE}/#/a01-home") page.wait_for_timeout(1500) assert page.evaluate("() => document.documentElement.dataset.touch") == "on" page.evaluate(SETUP) cdp = ctx.new_cdp_session(page) # ① 길게 누름 _touch(cdp, "touchStart", [(100, 200)]) page.wait_for_timeout(700) _touch(cdp, "touchEnd", []) page.wait_for_timeout(500) names, _ = _log(page) assert names.count(("tb-box", "contextmenu")) == 1, names assert ("tb-box", "click") not in names, names # ② 짧은 톡 page.wait_for_timeout(400) _touch(cdp, "touchStart", [(100, 200)]) _touch(cdp, "touchEnd", []) page.wait_for_timeout(700) names, _ = _log(page) assert ("tb-box", "contextmenu") not in names, names assert ("tb-box", "click") in names, names # ③ 두 번 톡 page.wait_for_timeout(400) for _ in range(2): _touch(cdp, "touchStart", [(100, 200)]) _touch(cdp, "touchEnd", []) page.wait_for_timeout(80) page.wait_for_timeout(600) names, _ = _log(page) assert names.count(("tb-box", "dblclick")) == 1, names # ④ 두 손가락 벌림 · 오므림 def pinch(box_x, start, end): _touch(cdp, "touchStart", [(box_x - start, 210), (box_x + start, 210)]) steps = 8 for i in range(1, steps + 1): d = start + (end - start) * i / steps _touch(cdp, "touchMove", [(box_x - d, 210), (box_x + d, 210)]) _touch(cdp, "touchEnd", []) page.wait_for_timeout(200) return _log(page) names, raw = pinch(110, 20, 80) wheels = [e for e in raw if e["t"] == "wheel" and e["id"] == "tb-box"] assert wheels and all(e["ctrl"] and e["dy"] < 0 for e in wheels), raw names, raw = pinch(110, 80, 20) wheels = [e for e in raw if e["t"] == "wheel" and e["id"] == "tb-box"] assert wheels and all(e["ctrl"] and e["dy"] > 0 for e in wheels), raw # ⑤ 자체 처리 상자 — 길게 누름 · 벌림 번역 없음 page.wait_for_timeout(400) _touch(cdp, "touchStart", [(300, 200)]) page.wait_for_timeout(700) _touch(cdp, "touchEnd", []) names, raw = pinch(310, 20, 70) assert not [e for e in raw if not e["trusted"] and e["id"] == "tb-own"], raw # ⑦ 고름 반경(4-4) — 손가락으로 누른 뒤 = 접촉 크기(최소 24) · 마우스로 누른 뒤 = 그대로 # 앱이 불러온 그 주소(vite 가 ?t= 를 붙였을 수 있음) — 다른 사본이면 상태가 따로 pick = ( "async () => (await import(performance.getEntriesByType('resource')" ".map((e) => e.name).find((n) => n.includes('/touch_bridge.ts')))).pickPx(4)" ) _touch(cdp, "touchStart", [(100, 200)]) _touch(cdp, "touchEnd", []) assert page.evaluate(pick) >= 24 page.mouse.click(100, 400) assert page.evaluate(pick) == 4 page.wait_for_timeout(400) _log(page) # ⑥ 마우스 — 우클릭 · 더블클릭은 브라우저 것 하나씩만 page.mouse.click(100, 200, button="right") page.mouse.dblclick(100, 200) page.wait_for_timeout(500) _, raw = _log(page) assert all(e["trusted"] for e in raw), json.dumps(raw) browser.close()