TIL/Playwright

[TIL][Playwright] AI 챗봇 응답 감지 #2 — follow-up 함정과 nth 인덱스

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

#1에서 Copy 버튼 등장을 응답 완료 신호로 정리했다

첫 질문에는 잘 동작했다

follow-up 질문에서 함정이 드러났다

함정 재현

Scenario: AI 챗봇에 두 번 질문한다 
    Given AI Assistant 페이지에 진입한다 
    When "Aspirin 의 구조를 알려줘" 메시지를 전송한다 
    Then AI 응답이 표시된다              # ✅ 통과 

    When "부작용도 알려줘" 메시지를 전송한다   # follow-up 
    Then AI 응답이 표시된다              # ⚠️ 즉시 통과 — 새 응답 도착 전

두 번째 Then 이 응답 스트리밍 시작도 전에 통과한다

# 기대: "Aspirin 은 소염진통제..." 
# 실제 (검증 시점): "" (응답 비어있음) 
# 또는: "Asp" (스트리밍 첫 토큰만) 

원인: 이미 visible 인 버튼을 기다린다

#1의 메서드는 단순했다

def is_response_visible(self, timeout_ms: int = 30000) -> bool: 
    copy_btn = self.driver.page.get_by_role( 
        "button", name="Copy message" 
    ) 
    copy_btn.wait_for(state="visible", timeout=timeout_ms) 
    return True 

첫 질문에는 잘 동작한다

전송 후 Copy 버튼이 새로 등장하면 그게 응답 완료 신호다

문제는 follow-up

이전 응답의 Copy 버튼이 이미 visible 상태로 남아 있다

wait_for(state="visible") 는 "보이는 게 있는가" 만 보므로, 이전 버튼을 잡아서 즉시 True 를 반환한다

새 응답은 시작도 안 했는데 "완료" 로 판단된다

해결: 전송 전 count 기록

문제의 핵심은 "이전 응답의 버튼" 과 "새 응답의 버튼" 을 구분하지 못하는 것

전송 시점에 현재 Copy 버튼 개수를 기록하면 둘을 구분할 수 있다

def send_message(self, message: str) -> "AIAssistantPage": 
    copy_btn = self.driver.page.get_by_role( 
        "button", name=self.locators.COPY_BUTTON_NAME 
    ) 
    self._copy_btn_count_before_send = copy_btn.count() 

    # 메시지 전송 로직... 
    return self 

응답 완료 대기에서 "전송 전 개수 + 1번째" Copy 버튼을 기다린다

def wait_for_response_complete(self, timeout_ms: int = 60000) -> "AIAssistantPage": 
    copy_btn = self.driver.page.get_by_role( 
        "button", name=self.locators.COPY_BUTTON_NAME 
    ) 
    expected_index = self._copy_btn_count_before_send 
    copy_btn.nth(expected_index).wait_for( 
        state="visible", timeout=timeout_ms 
    ) 
    return self 

nth(N) 은 N+1번째 (0-indexed) 요소를 가리킨다

전송 전 count 가 1이면 nth(1) = 두 번째 Copy 버튼이 등장할 때까지 기다린다

동작 흐름

첫 번째 질문: 
  send 전 copy_btn.count() = 0 
  → nth(0) 대기 = "첫 번째 Copy 버튼 등장 = 완료" 

두 번째 질문 (follow-up): 
  send 전 copy_btn.count() = 1   # 첫 응답의 Copy 버튼 
  → nth(1) 대기 = "두 번째 Copy 버튼 등장 = 완료" 

세 번째 질문: 
  send 전 copy_btn.count() = 2 
  → nth(2) 대기 = "세 번째 Copy 버튼 등장 = 완료" 

이전 응답의 Copy 버튼은 인덱스로 격리되고, 새 응답의 등장만 정확히 잡힌다

응답 텍스트 추출 — 항상 마지막

응답 완료 후에는 article.last 로 마지막 응답을 추출한다

def get_last_response_text(self) -> str: 
    articles = self.driver.page.locator( 
        self.locators.RESPONSE_ARTICLE_SELECTOR 
    ) 
    last_article = articles.last 
    full_text = last_article.text_content() 
    return full_text if full_text else "" 

완료 대기와 텍스트 추출이 분리되어 있다

wait_for_response_complete 가 통과한 시점에 마지막 응답은 항상 새 응답이다

키워드 검증과 디버깅 로깅

def has_response_keyword(self, keyword: str) -> bool: 
    response_text = self.get_last_response_text() 
    if keyword.lower() in response_text.lower(): 
        return True 
    else: 
        logger.warning( 
            f"키워드 '{keyword}' 미발견 — 응답 앞 200자: " 
            f"{response_text[:200]}" 
        ) 
        return False 

실패 시 응답 앞 200자를 로깅한다

"왜 키워드가 없는지" 를 즉시 확인할 수 있다

Feature 시나리오 — 완료와 내용을 분리

Scenario: AI 챗봇 두 번 질문하기 
    Given AI Assistant 페이지에 진입한다 
    When "Aspirin 의 구조를 알려줘" 메시지를 전송한다 
    Then AI 응답이 완료된다 
    And AI 응답에 "Aspirin" 키워드가 포함되어 있다 

    When "부작용도 알려줘" 메시지를 전송한다 
    Then AI 응답이 완료된다 
    And AI 응답에 "부작용" 키워드가 포함되어 있다 

AI 응답이 완료된다키워드가 포함되어 있다 를 별도 Step 으로 나눈다

실패하면 어느 단계에서 문제가 생겼는지 명확히 드러난다

정리

시나리오 #1 방식 (단순 wait_for visible) #2 방식 (nth 인덱스)
첫 질문 ✅ 동작 ✅ 동작
follow-up 질문 ⚠️ 이전 버튼 잡혀 즉시 통과 ✅ 새 버튼만 정확히 감지
응답 텍스트 추출 article.last 사용 article.last 사용

스트리밍 UI 의 "완료 상태" 는 요소의 존재 여부 가 아니라 새로 추가된 요소의 순번 으로 판단한다

같은 패턴은 알림, 토스트, 채팅 메시지 등 누적 UI 전반에 적용된다

반응형