TIL/Playwright

[TIL][Playwright] AI 챗봇 응답 완료를 E2E 로 감지하는 방법

아람2 2026. 6. 14. 20:00
반응형

자동화 커버리지를 넓히다가 이제 AI Assistant 의 차례가 됐다 

일반적인 UI 검증과 달리 AI 챗봇은 응답이 언제 끝나는지 알 수 없다 

일반 UI: 버튼 선택 → 모달 열림 (100ms) 
AI 챗봇: 메시지 전송 → 응답 스트리밍 → 완료 (2초~30초)

wait_for_timeout(5000) 으로 때우면 빠른 응답은 시간 낭비가 되고, 느린 응답은 타임아웃이다 

문제: 응답 완료 시점을 어떻게 아는가 

AI 챗봇의 응답은 스트리밍으로 한 글자씩 나온다 

응답 중간에 텍스트를 읽으면 불완전한 내용이 캡처된다 

시도한 접근법들

1. wait_for_timeout(10000)
   → 빠른 응답도 10초 대기, 느린 응답은 실패

2. 텍스트 길이 변화 감시 (polling)
   → 스트리밍 중 잠깐 멈추는 구간을 완료로 오인

3. networkidle 대기
   → SSE 스트리밍은 연결이 유지되므로 networkidle 이 안 됨

해결: UI 가 알려주는 완료 신호를 찾는다

MCP 의 browser_snapshot 으로 채팅 UI 를 분석했다

응답이 완료되면 "Copy message" 버튼이 나타난다

응답 스트리밍 중: 텍스트만 표시
응답 완료 후:     텍스트 + [Copy message] 버튼 등장

이 버튼의 등장이 곧 응답 완료 신호다

응답 완료 감지

def is_response_visible(self, timeout_ms: int = 30000) -> bool:
    """응답 완료 대기 — Copy message 버튼 등장을 신호로 사용"""
    try:
        copy_btn = self.driver.page.get_by_role(
            "button", name="Copy message"
        )
        copy_btn.wait_for(state="visible", timeout=timeout_ms)
        return True
    except PlaywrightTimeoutError:
        return False

고정 대기 시간이 아니라 UI 상태 변화 를 기다린다

응답이 2초면 2초만, 25초면 25초만 기다린다

최대 30초를 넘기면 타임아웃으로 실패 처리한다

응답 텍스트 추출

def get_response_text(self) -> str:
    """마지막 응답의 텍스트를 추출"""
    copy_btn = self.driver.page.get_by_role(
        "button", name="Copy message"
    ).last  # 여러 응답 중 마지막

    # Copy 버튼의 부모 컨테이너 = 응답 메시지 블록
    container = copy_btn.locator("..")
    paragraphs = container.get_by_role("paragraph")
    text_list = paragraphs.all_text_contents()
    response_text = "\n".join(text_list)
    return response_text

핵심은 Copy 버튼과 응답 텍스트가 같은 컨테이너에 있다 는 점이다

Copy 버튼을 찾으면 부모(..)로 올라가서 응답 전체를 가져올 수 있다

채팅 페이지 초기 상태 처리

AI Assistant 페이지는 마지막으로 열었던 채팅을 자동으로 보여준다

새 채팅 화면을 보장하려면 초기화가 필요하다

def _ensure_new_chat_screen(self) -> None:
    """새 채팅 화면 보장"""
    # 인사말 키워드가 보이면 이미 새 채팅
    try:
        heading = self.driver.page.locator("h1").filter(
            has_text=self.locators.GREETING_KEYWORD
        )
        heading.wait_for(state="visible", timeout=3000)
        return
    except PlaywrightTimeoutError:
        pass

    # 이전 채팅이 열려 있으면 새 채팅 버튼 선택
    new_chat_btn = self.driver.page.get_by_role(
        "button", name=self.locators.NEW_CHAT_BUTTON_NAME
    )
    new_chat_btn.click()
    self.driver.page.wait_for_load_state("networkidle")

오버레이 처리

AI Assistant 페이지에 진입하면 2가지 모달이 겹칠 수 있다

def _dismiss_overlays(self) -> None:
    """페이지 진입 시 방해 오버레이 닫기"""

    # 1. Covalent Feature Release 다이얼로그
    try:
        close_btn = self.driver.page.get_by_role(
            "button", name="닫기"
        )
        if close_btn.is_visible():
            close_btn.click()
    except PlaywrightTimeoutError:
        pass

    # 2. 1:1 상담 모달 (z-index 100003)
    try:
        modal = self.driver.page.locator(
            "[class*='z-[100003]']"
        ).last  # dim layer 제외, 모달 본체만
        if modal.is_visible():
            close_btn = modal.locator("button").first
            close_btn.click()
    except PlaywrightTimeoutError:
        pass

채팅 목록 카운트

def get_chat_list_count(self) -> int:
    """채팅 목록에 표시된 항목 수"""
    chat_label = self.driver.page.get_by_text("채팅 목록")
    parent = chat_label.locator("..")
    links = parent.get_by_role("link")
    count = links.count()
    return count

"채팅 목록" 레이블의 부모에서 role="link" 를 센다

채팅 항목이 <a> 태그로 렌더링되므로 link role 로 잡을 수 있다

시나리오 설계

@smoke
Scenario: ai_assistant_01 AI Assistant 페이지 초기 로드
    Given AI Assistant 페이지에 진입한다
    Then 인사 메시지가 표시된다
    And 새 채팅 버튼이 표시된다
    And 프롬프트 탭이 표시된다

Scenario: ai_assistant_02 AI Assistant 에게 질문한다
    Given AI Assistant 페이지에 진입한다
    When "안녕하세요" 메시지를 전송한다
    Then AI 응답이 표시된다

첫 번째 시나리오에 @smoke 를 붙였다

AI Assistant 페이지가 로드되는지만 매일 확인하면 충분하다

질의-응답 시나리오는 LLM 응답 시간 변동이 크므로 일일 Smoke 에는 넣지 않았다

follow-up 시나리오의 함정

is_response_visible 메서드는 첫 응답에 한해서만 안전하다

같은 채팅에서 두 번째 질문을 보내면 함정이 있다

이전 응답의 Copy 버튼이 이미 visible 상태로 남아 있어서, 새 응답이 도착하기도 전에 wait_for(state="visible") 가 즉시 통과한다

When "첫 질문" 메시지를 전송한다
Then AI 응답이 표시된다              # ✅ 정상 통과

When "두 번째 질문" 메시지를 전송한다   # follow-up
Then AI 응답이 표시된다              # ⚠️ 이전 응답 Copy 버튼이 잡혀 즉시 통과

해결은 "전송 전 Copy 버튼 개수 기록 → nth(이전 개수) 대기" 패턴이다

자세한 구현은 다음 편에서 다룬다

핵심 정리

패턴 설명
Copy 버튼 등장 대기 응답 완료의 명확한 UI 신호
부모 컨테이너 탐색 Copy 버튼 → 부모 → paragraph 수집
새 채팅 보장 인사말 h1 확인 → 없으면 새 채팅 버튼 선택
오버레이 순차 닫기 Feature Release → 1:1 상담 순서

AI 챗봇 E2E 의 핵심은 "응답 완료를 타이머가 아닌 UI 상태로 감지하는 것" 이다

고정 대기 시간은 항상 너무 짧거나 너무 길다

UI 가 제공하는 완료 신호를 찾으면 빠른 응답에도 느린 응답에도 정확하게 대응할 수 있다

반응형