Loading...
Loading...
Optimize Playwright E2E tests by removing anti-patterns, implementing smart waits, enabling test sharding, and improving reliability.
npx skill4agent add d-oit/do-novelist-ai e2e-test-optimizerPriority 1: Remove anti-patterns (waitForTimeout)
↓
Priority 2: Implement smart waits (toBeVisible, toBeAttached)
↓
Priority 3: Optimize selectors (data-testid, locators)
↓
Priority 4: Enable test sharding (parallel execution)
↓
Priority 5: Add performance monitoring// ❌ BAD - Flaky, slow
await page.waitForTimeout(2000);
await expect(element).toBeVisible();
// ✅ GOOD - Reliable, fast
await expect(element).toBeVisible({ timeout: 5000 });// ❌ BAD - Brittle
await page.locator('div:nth-child(2) > button').click();
await page.locator('.btn.primary').click();
// ✅ GOOD - Stable
await page.getByTestId('submit-button').click();
await page.getByRole('button', { name: 'Submit' }).click();// ❌ BAD - Race condition
await page.click('button');
await expect(page.getByText('Loaded')).toBeVisible();
// ✅ GOOD - Waits for network
await page.click('button');
await page.waitForLoadState('networkidle');
await expect(page.getByText('Loaded')).toBeVisible();// ❌ BAD - Too short for slow tests, too long for fast
test.setTimeout(10000); // Global timeout
// ✅ GOOD - Per-test or per-assertion timeout
await expect(element).toBeVisible({ timeout: 5000 });
await expect(slowElement).toBeVisible({ timeout: 30000 });expect().toBeVisible()data-testidpage.waitForLoadState('networkidle')waitForTimeout()// For navigation
await page.waitForLoadState('load');
// For AJAX requests
await page.waitForLoadState('networkidle');
// For dynamic content
await page.waitForLoadState('domcontentloaded');// Wait for element to appear
await expect(page.getByTestId('element')).toBeVisible();
// Wait for element to disappear
await expect(page.getByTestId('loading')).toBeHidden();
// Wait for text content
await expect(page.getByText('Success')).toBeVisible();
// Wait for element to be attached
await expect(page.getByTestId('element')).toBeAttached();// Wait for specific element in list
await expect(
page.getByTestId('item').filter({ hasText: 'Target' })
).toBeVisible();
// Wait for enabled button
await expect(
page.getByRole('button', { name: 'Submit' })
.and(page.getByRole('button', { disabled: false })
).toBeVisible();strategy:
matrix:
shard_index: [0, 1, 2, 3]
total_shards: [4]
steps:
- name: Run E2E tests
run: |
pnpm exec playwright test \
--project=chromium \
--shard=${{ matrix.shard_index }}/${{ matrix.total_shards }} \
--retries=2# Calculate shards based on test count
SHARD_COUNT=4
TEST_COUNT=$(pnpm exec playwright test --list 2>/dev/null | grep -c '›')
SHARD_SIZE=$((TEST_COUNT / SHARD_COUNT + 1))
for i in $(seq 0 $((SHARD_COUNT - 1))); do
pnpm exec playwright test \
--shard=$i/$SHARD_COUNT \
--output=test-results/shard-$i
done# Required for Playwright in CI
export CI=true
export PLAYWRIGHT_BROWSERS_PATH=~/.cache/ms-playwright
export NODE_ENV=test
export NODE_OPTIONS=--max-old-space-size=4096pnpm exec playwright test \
--project=chromium \
--reporter=list,html,json \
--retries=2 \
--timeout=30000 \
--workers=2 \
--max-failures=5- name: Cache Playwright browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}// ❌ DON'T - Wait arbitrary time
await page.waitForTimeout(500);
// ✅ DO - Wait for animation to complete
await page.waitForLoadState('domcontentloaded');// ❌ DON'T - Element might not exist yet
await expect(page.getByTestId('lazy-content')).toBeVisible();
// ✅ DO - Scroll into view first
await page.getByTestId('lazy-container').scrollIntoViewIfNeeded();
await expect(page.getByTestId('lazy-content')).toBeVisible();// Wait for loading state to complete
await expect(page.getByTestId('loading')).toBeVisible();
await expect(page.getByTestId('loading')).toBeHidden();
await expect(page.getByTestId('content')).toBeVisible();test.describe('Feature', () => {
test('slow test', async ({ page }) => {
const startTime = Date.now();
// ... test code ...
const duration = Date.now() - startTime;
if (duration > 5000) {
console.warn(`Test took ${duration}ms - consider optimization`);
}
});
});test.afterEach(async () => {
if (test.info().status !== 'passed') {
// Take screenshot on failure
await page.screenshot({
path: `failures/${test.info().title}.png`,
fullPage: true,
});
}
});