HyperDesign 에서 Warhead 원자가 선택 보호되어 있는지 검증해야 했다
처음엔 선택해서 반응이 없으면 보호가 정상이라고 판단했다
틀렸다
force=True 가 보호를 우회하기 때문이다
상황: Covalent HyperDesign 의 Warhead 보호
약물 설계 도구에서 분자 구조를 수정할 수 있다
그런데 Warhead (반응기) 부분은 수정하면 안 된다
UI 에서 Warhead 원자를 선택해도 반응되지 않아야 한다
이걸 E2E 자동화로 검증해야 한다
첫 번째 시도: 선택 기반 검증 (실패)
# ❌ Bad — 선택해서 반응 없으면 보호 정상이라고 판단
def test_warhead_protection(page):
warhead_atom = page.locator(".warhead-atom").first
warhead_atom.click()
selected = page.locator(".selected-atom")
if selected.count() == 0:
is_protected = True
else:
is_protected = False
assert is_protected, "Warhead 보호 실패"
문제가 2가지다
1. force=True 가 보호를 우회한다
Playwright 에서 요소가 선택 불가능하면 자동으로 재시도하다가 timeout 이 터진다
그래서 force=True 를 쓰고 싶은 유혹이 생긴다
warhead_atom.click(force=True) # pointerEvents: none 을 무시한다
force=True 는 Playwright 의 actionability check 를 전부 건너뛴다
pointerEvents: none 으로 보호된 요소도 선택이 된다
보호가 정상인데 검증에서 선택이 되니까 검증이 실패한다
2. 선택 결과가 비결정적이다
선택 후 "선택 안 됨" 을 확인하는 건 네거티브 검증이다
반응이 늦은 건지 없는 건지 구분이 안 된다
MCP snapshot 으로 실제 DOM 구조 확인
코드만 보고 "Warhead 원자 = 노란색 ellipse" 라고 가정했다
MCP browser_snapshot 으로 실제 DOM 을 확인했다
발견 1: 모든 ellipse 에 data-fragment-color 속성이 있다 (Fragment 전용)
발견 2: data-fragment-color 가 없는 ellipse = 0개
발견 3: Warhead 원자는 ellipse 가 아니라 path 로 렌더링된다
Warhead: path[class^="bond-"], path[class^="atom-"] (pointerEvents: none)
Fragment: ellipse[data-fragment-color] (pointerEvents: auto)
코드만 봤으면 이 차이를 절대 못 찾았다
해결: DOM 구조 검증
선택하지 않고 DOM 자체를 검사한다
Warhead 영역에 선택 가능한 ellipse 가 0개인지 확인한다
로케이터 상수 (hyper_design_locators.py)
FRAGMENT_ATOM_SELECTOR = "ellipse"
FRAGMENT_COLOR_ATTR = "data-fragment-color"
WARHEAD_SELECTABLE_SELECTOR = "ellipse:not([data-fragment-color])"
WARHEAD_BOND_SELECTOR = 'path[class^="bond-"]'
WARHEAD_ATOM_LABEL_SELECTOR = 'path[class^="atom-"]'
HIGHLIGHT_ATTR = "data-highlight"
MIN_MOLECULE_SVG_SIZE = 100
Page Object 메서드 (hyper_design_page.py)
def get_warhead_selectable_count(self) -> int:
"""Warhead 선택 가능 ellipse 수 반환 (0이면 보호 정상, -1이면 SVG 없음)"""
modal = self._get_modal_or_fail()
svg = self._find_molecule_svg(modal)
if svg is None:
return -1
non_fragment = svg.locator(self.locators.WARHEAD_SELECTABLE_SELECTOR)
# "ellipse:not([data-fragment-color])" — Fragment 아닌 ellipse
return non_fragment.count()
def get_total_fragment_ellipse_count(self) -> int:
"""Fragment ellipse 수 반환 (0보다 커야 정상)"""
modal = self._get_modal_or_fail()
svg = self._find_molecule_svg(modal)
if svg is None:
return 0
selector = f"{self.locators.FRAGMENT_ATOM_SELECTOR}[{self.locators.FRAGMENT_COLOR_ATTR}]"
# → "ellipse[data-fragment-color]"
return svg.locator(selector).count()
def get_highlighted_fragment_count(self) -> int:
"""하이라이트된 요소 수 반환 (선택 후 선택 확인용)"""
modal = self._get_modal_or_fail()
svg = self._find_molecule_svg(modal)
if svg is None:
return 0
return svg.locator(f"[{self.locators.HIGHLIGHT_ATTR}='true']").count()
SVG 탐색 헬퍼
SVG 태그가 페이지에 여러 개 있을 수 있다 (아이콘 등)
분자 구조 SVG 만 찾기 위해 bounding_box() 크기를 체크한다
def _find_molecule_svg(self, container) -> Locator | None:
"""컨테이너 내에서 분자 구조 SVG 찾기 (아이콘 제외)"""
svg_candidates = container.locator(self.locators.MOLECULE_SVG_TAG)
# MOLECULE_SVG_TAG = "svg"
for attempt in range(2):
for i in range(svg_candidates.count()):
try:
box = svg_candidates.nth(i).bounding_box()
if box and box["width"] > self.locators.MIN_MOLECULE_SVG_SIZE:
# MIN_MOLECULE_SVG_SIZE = 100
return svg_candidates.nth(i)
except PlaywrightTimeoutError:
continue
# 첫 시도 실패 시 1초 대기 후 재시도
self.driver.page.wait_for_timeout(1000)
return None
Step 정의 (test_hyper_design_steps.py)
@then("Warhead 영역에 선택 가능한 요소가 없다")
def verify_warhead_not_selectable(hyper_design_page):
_assert_cond(
lambda: hyper_design_page.get_warhead_selectable_count() == 0,
success_msg="Warhead 보호 정상",
fail_msg="Warhead 영역에 선택 가능 요소 존재",
screenshot_name="warhead_protection",
)
@then("Fragment ellipse가 존재한다")
def verify_fragment_exists(hyper_design_page):
_assert_cond(
lambda: hyper_design_page.get_total_fragment_ellipse_count() > 0,
success_msg="Fragment ellipse 확인",
fail_msg="Fragment ellipse 없음",
screenshot_name="fragment_ellipse",
)
@then("Fragment가 정상 선택된다")
def verify_fragment_selected(hyper_design_page):
_assert_cond(
lambda: hyper_design_page.get_highlighted_fragment_count() > 0,
success_msg="Fragment 선택 확인",
fail_msg="Fragment highlight 없음",
screenshot_name="fragment_selected",
)
_assert_cond 는 lambda 조건 + 성공/실패 메시지 + 스크린샷명을 받는 assertion 헬퍼다
추가 문제: evaluate() hang
4워커 병렬 실행 시 evaluate() 가 무한 블로킹되는 문제도 있었다
Chromium 리소스 경합이 원인이었다
# Before — timeout 없음 → 무한 블로킹
frag_color = atom.evaluate(f"e => e.getAttribute('{color_attr}')")
# After — 5초 타임아웃 추가
frag_color = atom.evaluate(f"e => e.getAttribute('{color_attr}')", timeout=5000)
근본적으로는 4워커 동시 실행 → 2+2 순차 배치로 전환하여 Chromium 2개만 동시 실행되게 했다
언제 DOM 구조 검증을 쓰면 좋은가
선택 기반이 맞는 경우
- 버튼 선택 후 화면 전환 (포지티브 검증)
- 입력 필드에 값 입력 후 결과 확인
- 드롭다운 선택 후 선택값 검증
DOM 구조 검증이 맞는 경우
- 요소가 선택 불가능한지 확인 (네거티브 검증)
- CSS 속성으로 상태가 결정되는 경우 (disabled, pointerEvents)
- SVG 내부 요소의 속성 검증
- 렌더링 구조 자체가 검증 대상인 경우
핵심 정리
| 방식 | 적합한 상황 | 부적합한 상황 |
|---|---|---|
| 선택 기반 | 버튼 동작 확인 | 선택 불가 검증 |
| DOM 구조 | 속성/상태 검증 | 사용자 인터랙션 흐름 |
SVG 검증에서 기억할 것
force=True는 보호 메커니즘을 우회한다 — 검증에서 쓰면 거짓 결과- 네거티브 검증은 DOM 속성을 직접 검사하는 게 안정적이다
- 가정하지 말고 MCP snapshot 으로 실제 DOM 을 확인해라
- SVG 탐색 시 아이콘 SVG 를 제외하려면 bounding_box 크기 체크
'TIL > Playwright' 카테고리의 다른 글
| 로그인 인증 상태 저장 및 재사용 방법 w/ Playwright (6) | 2025.08.12 |
|---|---|
| Playwright 로 API Test 해보기 w/ OpenAPI (0) | 2025.06.30 |
| Playwright Python: Tutorial #5 - API Testing (0) | 2025.02.28 |
| Playwright Python: Tutorial #4 - Browser Context (0) | 2025.02.27 |
| Playwright Python: Tutorial #3 - Test Generator (0) | 2025.02.24 |