C.W.K.
Stream
Lesson 03 of 04 · published

Auto-Wait, Actionability, 그리고 sleep 안 하기

~14 min · playwright-locators, auto-wait, actionability

Level 0테스트 호기심
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"`page.waitForTimeout(2000)` 는 '내가 뭘 기다리는지 모른다' 는 뜻. 뭘 기다리는지 알아내."

다섯 actionability 체크

모든 액션 (click, fill, check 등) 전에 Playwright 가 대상 element 에 대해 일련의 actionability 체크 돌려. 액션이 다 통과까지 기다리거나 — timeout 후 명확한 메시지로 실패. 체크들:

  • Attached — element 가 DOM 에 있음.
  • Visible — non-empty 바운딩 박스 가짐, display: none 이나 visibility: hidden 아님.
  • Stable — 애니메이션 안 함 (연속 두 프레임 동안 위치 안정).
  • Enableddisabled 속성 아님, fieldset[disabled] 안 아님.
  • Receives events — 다른 element 에 가려지지 않음 (실제로 그것에 클릭이 통할 거).

버튼이 fade-in 중이면 click 이 애니메이션 끝까지 기다림. 모달이 너의 타겟 위에 열리고 있으면 click 이 모달 기다림 — 또는 안 사라지면 실패. 어느 체크가 게이팅인지 알 필요 없음; await button.click() 쓰면 Playwright 가 처리.

왜 거의 `sleep` 안 해

테스트에서 sleep(1000) 한 역사적 이유는 어떤 불확실한 UI state 가 안정되길 기다리려고. Auto-wait + web-first 단언 있으면 더 이상 필요 없음. 맞는 질문은 "얼마나 기다려야 해?" 가 아니라 "내가 실제로 뭘 기다리고 있어?".

waitForTimeout 오용으로 이끄는 흔한 세 케이스, 그리고 대신 뭘 짤지:

  • "페이지 로드 기다림."await expect(page.getByRole('heading')).toBeVisible(). 로드 후 state 를 직접 단언.
  • "애니메이션 기다림." → 다음 액션 호출. Auto-wait 가 stability 체크 처리.
  • "API 호출 기다림." → response 객체 필요하면 await page.waitForResponse('/api/users'), 아니면 결과 UI state 에 단언.
시간만큼 sleep 은 추측. State 기다림은 앎. Auto-wait + web-first 단언이 옛 조언이 'sleep 추가' 였던 거의 모든 케이스 커버. 남는 케이스 (특정 네트워크 호출 기다림, 이벤트 기다림) 는 명시적 waiter 있어.

OK 인 명시적 wait

특정 이벤트엔 정당한 명시적 wait 있어:

  • page.waitForURL(pattern) — redirect 체인이나 폼 submit 후 navigation 에 유용.
  • page.waitForResponse(predicate) — 네트워크 response 캡처 필요할 때.
  • page.waitForRequest(predicate) — 특정 request 가 발동되는지 검증 필요할 때.
  • page.waitForEvent('console') — 브라우저 레벨 이벤트에.
  • locator.waitFor({ state: 'detached' }) — '사라질 때까지 기다림' 이 단언으로 표현 안 되는 드문 케이스에.

OK 안 한 건 waitForTimeout(N). 네가 기다리는 실제 조건이 있어 — 이름 지어.

Auto-Wait 가 안 도울 때 — 숨겨진 race

Auto-wait 가 element 쪽 타이밍 커버. 앱 state race 는 커버 안 해. 'Submit' 클릭, API 호출 완료, 새 컴포넌트 렌더 — auto-wait 가 새 컴포넌트로 데려가는데, 그 컴포넌트의 후속 클릭은 React commit 에 대해 race 중. 해법은 그것과 인터랙트 전에 새 state 가 visible 한 거 단언:

await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByRole('heading', { name: 'Success' })).toBeVisible();
// NOW it's safe to interact with the success view
await page.getByRole('button', { name: 'Continue' }).click();

Code

클릭 하나, 체크 다 Playwright 가 처리·typescript
// Auto-wait in action — the actions wait for the element to be actionable
import { test, expect } from '@playwright/test';

test('button click auto-waits through actionability checks', async ({ page }) => {
  await page.goto('/');
  // Suppose the page has a Submit button that:
  // - renders 200ms after load (not yet attached)
  // - is disabled while form validation runs (not enabled)
  // - fades in over 150ms (not stable)

  // We don't need to wait for any of those — Playwright does.
  await page.getByRole('button', { name: 'Submit' }).click();

  // The next assertion also auto-waits until the success state appears.
  await expect(page.getByText(/saved successfully/i)).toBeVisible();
});
Sleep 하지 마 — 단언해·typescript
// The old anti-pattern vs the modern approach
import { test, expect } from '@playwright/test';

// ❌ The old way — sleep, hope, assert
test('waiting for load (the bad way)', async ({ page }) => {
  await page.goto('/dashboard');
  await page.waitForTimeout(3000);   // "give it time to load"
  const heading = await page.locator('h1').textContent();
  expect(heading).toBe('Dashboard');  // racing against actual render
});

// ✅ The modern way — assert what should be true
test('waiting for load (the right way)', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(
    page.getByRole('heading', { name: 'Dashboard' })
  ).toBeVisible();
});
명시적 wait — 정당한 것들·typescript
// Legitimate explicit waits — when there's no DOM state to assert against
import { test, expect } from '@playwright/test';

test('waits for the API response to verify network behavior', async ({ page }) => {
  await page.goto('/users');

  // We need the actual response object, not just the UI state
  const [response] = await Promise.all([
    page.waitForResponse((r) => r.url().includes('/api/users') && r.status() === 200),
    page.getByRole('button', { name: 'Refresh' }).click(),
  ]);

  const body = await response.json();
  expect(body.users).toHaveLength(3);
});

test('waits for post-submit redirect to settle', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('pass');
  await page.getByRole('button', { name: 'Submit' }).click();

  // Some apps redirect through /auth-callback → /dashboard.
  // waitForURL gives you the final destination.
  await page.waitForURL(/\/dashboard$/);
});
앱 state race — 단언, 그 다음 인터랙트·typescript
// Application-state race — assert before next interaction
import { test, expect } from '@playwright/test';

test('assert intermediate state before interacting with the new view', async ({ page }) => {
  await page.goto('/items/new');
  await page.getByLabel('Title').fill('New Item');
  await page.getByRole('button', { name: 'Create' }).click();

  // ⚠️ Without this assert, the next click might race against the React commit
  //    that swaps the form for the success view.
  await expect(
    page.getByRole('heading', { name: 'Item Created' })
  ).toBeVisible();

  // NOW it's safe to interact with the success view
  await page.getByRole('button', { name: 'View item' }).click();
});

External links

Exercise

프로젝트의 기존 E2E spec 에서 waitForTimeout 호출 감사. 각각에 대해 물어봐: 내가 실제로 뭘 기다리고 있어? 각각을 web-first 단언 (expect(locator).toBe...), 명시적 waiter (waitForResponse, waitForURL), 또는 — 조건 없으면 — wait 지우고 테스트 여전히 통과하는지 확인 (보통 auto-wait 이미 충분).
Hint
Sleep 지운 후 테스트가 간헐적으로 실패하면, 앱 state race 발견. 다음 액션 바로 전에 누락된 단언 (expect(thing).toBeVisible()) 추가 — 그게 네가 실제로 요청하던 wait 야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.