Enabled — disabled 속성 아님, 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();
});
프로젝트의 기존 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.