Files
claude-skills-reference/engineering-team/playwright-pro/templates/api/rate-limiting.md
Alireza Rezvani d33d03da50 feat: add playwright-pro plugin — production-grade Playwright testing toolkit (#254)
Complete Claude Code plugin with:
- 9 skills (/pw:init, generate, review, fix, migrate, coverage, testrail, browserstack, report)
- 3 specialized agents (test-architect, test-debugger, migration-planner)
- 55 test case templates across 11 categories (auth, CRUD, checkout, search, forms, dashboard, settings, onboarding, notifications, API, accessibility)
- TestRail MCP server (TypeScript) — 8 tools for bidirectional sync
- BrowserStack MCP server (TypeScript) — 7 tools for cross-browser testing
- Smart hooks (auto-validate tests, auto-detect Playwright projects)
- 6 curated reference docs (golden rules, locators, assertions, fixtures, pitfalls, flaky tests)
- Leverages Claude Code built-ins (/batch, /debug, Explore subagent)
- Zero-config for core features; TestRail/BrowserStack via env vars
- Both TypeScript and JavaScript support throughout

Co-authored-by: Leo <leo@openclaw.ai>
2026-03-05 13:50:05 +01:00

153 lines
5.5 KiB
Markdown

# Rate Limiting Template
Tests rate limit headers, 429 response, and Retry-After handling.
## Prerequisites
- Valid auth token: `{{apiToken}}`
- Rate-limited endpoint: `{{rateLimitedEndpoint}}`
- Rate limit: `{{rateLimit}}` requests per `{{rateLimitWindow}}`
- API base: `{{apiBaseUrl}}`
---
## TypeScript
```typescript
import { test, expect } from '@playwright/test';
const headers = {
'Authorization': `Bearer {{apiToken}}`,
'Content-Type': 'application/json',
};
test.describe('Rate Limiting', () => {
// Happy path: rate limit headers present on normal requests
test('includes rate limit headers on success response', async ({ request }) => {
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
expect(res.status()).toBe(200);
expect(res.headers()['x-ratelimit-limit']).toBeTruthy();
expect(res.headers()['x-ratelimit-remaining']).toBeTruthy();
expect(Number(res.headers()['x-ratelimit-limit'])).toBe({{rateLimit}});
});
// Happy path: remaining count decrements
test('x-ratelimit-remaining decrements with each request', async ({ request }) => {
const first = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
const second = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
const remaining1 = Number(first.headers()['x-ratelimit-remaining']);
const remaining2 = Number(second.headers()['x-ratelimit-remaining']);
expect(remaining2).toBeLessThan(remaining1);
});
// Error case: 429 when limit exceeded
test('returns 429 when rate limit exceeded', async ({ request }) => {
let lastStatus = 200;
let retryAfter: string | undefined;
for (let i = 0; i <= {{rateLimit}}; i++) {
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
lastStatus = res.status();
if (lastStatus === 429) {
retryAfter = res.headers()['retry-after'];
break;
}
}
expect(lastStatus).toBe(429);
expect(retryAfter).toBeTruthy();
});
// Error case: 429 body contains error message
test('429 response body contains error and retry info', async ({ request }) => {
// Exhaust limit
for (let i = 0; i <= {{rateLimit}}; i++) {
await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
}
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
if (res.status() === 429) {
const body = await res.json();
expect(body.error ?? body.message).toMatch(/rate limit|too many requests/i);
expect(Number(res.headers()['retry-after'])).toBeGreaterThan(0);
}
});
// Happy path: different users have separate rate limit buckets
test('rate limit is per-user, not global', async ({ request }) => {
// Exhaust limit for user 1
for (let i = 0; i <= {{rateLimit}}; i++) {
await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, {
headers: { 'Authorization': `Bearer {{apiToken}}` },
});
}
// User 2 should still succeed
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, {
headers: { 'Authorization': `Bearer {{apiToken2}}` },
});
expect(res.status()).toBe(200);
});
// Edge case: reset after window expires
test('rate limit resets after window expires', async ({ page, request }) => {
// Exhaust limit
for (let i = 0; i <= {{rateLimit}}; i++) {
await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
}
// Advance clock past the window
await page.clock.install();
await page.clock.fastForward({{rateLimitWindowMs}});
// Should succeed again
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
expect(res.status()).toBe(200);
});
});
```
---
## JavaScript
```javascript
const { test, expect } = require('@playwright/test');
const headers = { 'Authorization': `Bearer {{apiToken}}` };
test.describe('Rate Limiting', () => {
test('includes rate limit headers on success', async ({ request }) => {
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
expect(res.status()).toBe(200);
expect(res.headers()['x-ratelimit-limit']).toBeTruthy();
expect(res.headers()['x-ratelimit-remaining']).toBeTruthy();
});
test('returns 429 with Retry-After when limit exceeded', async ({ request }) => {
let lastStatus = 200;
let retryAfter;
for (let i = 0; i <= {{rateLimit}}; i++) {
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
lastStatus = res.status();
if (lastStatus === 429) { retryAfter = res.headers()['retry-after']; break; }
}
expect(lastStatus).toBe(429);
expect(retryAfter).toBeTruthy();
});
test('per-user buckets: other user unaffected', async ({ request }) => {
for (let i = 0; i <= {{rateLimit}}; i++) {
await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, { headers });
}
const res = await request.get(`{{apiBaseUrl}}/{{rateLimitedEndpoint}}`, {
headers: { 'Authorization': `Bearer {{apiToken2}}` },
});
expect(res.status()).toBe(200);
});
});
```
## Variants
| Variant | Description |
|---------|-------------|
| Headers present | x-ratelimit-limit and -remaining on 200 |
| Decrement | remaining decreases each request |
| 429 triggered | Limit exceeded → 429 + Retry-After |
| 429 body | Error message + retry info in body |
| Per-user bucket | Exhausted user doesn't affect others |
| Window reset | Clock advanced → limit resets |