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

ApproachSteps in browserTypical duration
Pure UI testAll steps8–15 s
Hybrid (API setup)UI assertion only2–4 s
Pure API test00.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);
});