level 05 / hybrid-ui-api-flows
Hybrid UI + API Flows
Combine API calls with browser actions to build efficient, stable end-to-end test flows.
Why Hybrid Tests?
Pure UI tests are slow — every step goes through the browser. Pure API tests miss rendering bugs. Hybrid tests use the API for setup/teardown and the browser only for the UI assertion that matters.
Pattern: API Setup → UI Assertion
test('newly created booking appears in admin UI', async ({ page, request }) => {
// 1. API setup
const token = await getAuthToken(request);
const res = await request.post('/booking', {
headers: { Cookie: `token=${token}` },
data: {
firstname: 'Alice',
lastname: 'Smith',
totalprice: 200,
depositpaid: true,
bookingdates: { checkin: '2025-06-01', checkout: '2025-06-07' },
},
});
const { bookingid } = await res.json();
// 2. Browser assertion
await page.goto('/admin/bookings');
await expect(page.getByText('Alice Smith')).toBeVisible();
await expect(page.getByText('$200')).toBeVisible();
// 3. API teardown
await request.delete(`/booking/${bookingid}`, {
headers: { Cookie: `token=${token}` },
});
});
Pattern: UI Action → API Verification
test('UI delete updates backend', async ({ page, request }) => {
// 1. API setup
const { bookingid } = await createBooking(request);
// 2. UI action
await page.goto(`/admin/bookings/${bookingid}`);
await page.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'Confirm' }).click();
await expect(page.getByText('Booking deleted')).toBeVisible();
// 3. API verification
const check = await request.get(`/booking/${bookingid}`);
expect(check.status()).toBe(404);
});
Sharing Auth Between Browser and API
// playwright.config.ts
export default defineConfig({
use: {
storageState: 'playwright/.auth/user.json',
},
});
// The `request` fixture in a test ALSO uses storageState cookies —
// browser and API context share the same session automatically
test('shared auth', async ({ page, request }) => {
// Both use cookies from storageState
await page.goto('/dashboard'); // browser — authenticated
const res = await request.get('/api/me'); // API — same session
expect(res.ok()).toBe(true);
});
Performance Impact
| Approach | Steps in browser | Typical duration |
|---|---|---|
| Pure UI test | All steps | 8–15 s |
| Hybrid (API setup) | UI assertion only | 2–4 s |
| Pure API test | 0 | 0.2–0.5 s |
Hybrid tests are 3–5× faster than pure UI tests while still catching rendering bugs.
Database Validation
After a UI action, query the database directly to assert backend state (covered in L6):
// With a database helper
test('UI form submission persists to DB', async ({ page, db }) => {
await page.goto('/contact');
await page.fill('[name=email]', 'test@example.com');
await page.click('[type=submit]');
await expect(page.getByText('Message sent')).toBeVisible();
// DB assertion
const row = await db.query(
`SELECT * FROM contact_submissions WHERE email = $1`,
['test@example.com']
);
expect(row.rows).toHaveLength(1);
});