Automating the web UI with Playwright
With a thick, fast API suite in place, add the thin layer on top: automated end-to-end tests that drive the AgentPay web console through a real browser, proving the critical journeys work as a user experiences them. This is the web-automation module applied — with the discipline that keeps a UI suite reliable rather than flaky. This lesson walks the web-automation phase of the capstone.
Decide what to automate at the UI (and what not)
First, the judgement (the test-at-the-right-layer lesson): you have already covered the rules, validation, auth, permissions and idempotency exhaustively at the API. Do not re-test all of that through the UI. The UI suite covers the journeys and rendering that need a browser:
- Login and logout — the auth journey a user actually takes.
- Viewing transactions — the table renders, with the right data, and filters/sorts/paginates on screen.
- Creating a transaction — through the form, with confirmation (and one invalid case showing an error, to prove the form surfaces validation).
- Role-based visibility — an operator sees only their agent's data in the UI.
A handful of critical journeys — not a UI test for every rule. That thin, meaningful layer is the goal.
Set up Playwright reliably
In the AgentPay web app, Playwright is configured to start both servers and reset data before each test (the
fixtures lesson). Reuse that: baseURL, the webServer array, screenshot: 'only-on-failure', trace: 'on-first-retry', and the resetData helper. Write a loginAs helper so no test repeats the login:
export async function loginAs(page, email, password) {
await page.goto('/login');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
}
Use user-facing locators throughout (getByRole, getByLabel — the locators lesson), never brittle CSS
paths.
Automate the journeys — waiting for conditions
Write each journey with auto-waiting actions and web-first assertions, never a sleep (the
actions-and-assertions lesson). The login journey:
test('signs in and reaches the dashboard', async ({ page, request }) => {
await resetData(request);
await loginAs(page, 'admin@agentpay.test', 'admin123');
await expect(page.getByTestId('current-user')).toContainText('admin');
});
The transactions journey — and here is where reliability is won or lost. After an action that triggers an async re-render, wait for the new state before reading it:
test('paginates without repeating rows', async ({ page }) => {
await expect(page.getByRole('row')).toHaveCount(11); // wait for page 1 to load
const firstIds = await page.locator('tbody tr td:first-child').allInnerTexts();
await page.getByRole('button', { name: 'Next' }).click();
await expect(page.getByTestId('page-indicator')).toContainText('Page 2');
await expect(page.locator('tbody tr td:first-child').first()).not.toHaveText(firstIds[0]); // wait for re-render
const secondIds = await page.locator('tbody tr td:first-child').allInnerTexts();
expect(firstIds.filter((id) => secondIds.includes(id))).toHaveLength(0);
});
That "wait for the first row to change" line is the difference between a reliable test and a flaky one — the
lesson AgentPay's own suite learned. Use expect.poll for computed conditions like "the amounts are sorted
ascending". And test the money journey — creating a transaction through the form and seeing confirmation — as
a first-class critical path.
Keep it reliable and maintainable
The habits that keep this layer trustworthy (the maintaining-trust lesson):
- Reset data before each test (
resetData) so tests are independent; run serially if they share the API (AgentPay'sworkers: 1). - Wait for conditions, never sleep — every timing bug you avoid is a flake you prevent.
- Extract helpers / page objects as the suite grows, so a screen change is a one-place edit.
- Keep it thin — resist adding UI tests for things the API already covers.
- Debug with traces when something fails (the traces lesson) — enable
trace: 'on-first-retry'and open the trace, do not guess.
Run the suite a few times to confirm it is stable, not just passing once — a suite that passes intermittently is not done.
Compare with the reference, and reflect
Compare your journeys with AgentPay's apps/web/tests (12 E2E tests: auth, transactions
list/filter/sort/paginate/roles, create-transaction). Did you cover the critical journeys? Are your tests
reliable across repeated runs? Did you keep the layer thin, pushing rules to the API? A good outcome is a
small Playwright suite — the critical journeys, each reliable, each a real user path — sitting on top of your
thick API base. That is a healthy pyramid, and it is what a trustworthy AgentPay suite looks like.
Check your work
What to automate at the UI: the journeys/rendering that need a browser — login/logout, viewing + filter/sort/paginate on screen, creating a transaction (with one invalid case surfacing an error), role visibility. Not the rules/auth/idempotency already covered at the API. A handful of critical journeys.
Set up reliably: reuse baseURL, webServer, screenshot/trace config, and resetData; a loginAs
helper; user-facing locators (getByRole/getByLabel), never brittle CSS.
Automate with waiting: auto-waiting actions + web-first assertions, never sleep; after an async
re-render, wait for the new state before reading (the "first row changed" pattern); expect.poll for
computed conditions; test the create-transaction money journey as a first-class path.
Keep it reliable/maintainable: reset data per test (serial if sharing the API), wait not sleep, extract helpers/page objects, keep the layer thin, debug with traces. Run repeatedly to confirm stability, not a one-off pass.
Compare with the reference apps/web/tests (12 E2E) — did you cover the journeys, stay reliable, keep it
thin? Aim for a small reliable Playwright suite over a thick API base — a healthy pyramid.
Practice
- Set up Playwright in the AgentPay web app with
resetDataand aloginAshelper; get the login journey green. - Automate the transactions journey: view, filter by status, and paginate — waiting for conditions, no sleeps.
- Automate the create-transaction journey through the form, asserting the confirmation; add one invalid case.
- Automate role visibility: an operator sees only agent-1 rows.
- Run your suite five times and confirm it is stable; fix any flakiness by waiting on real conditions.
- Compare with the reference
apps/web/tests; check you kept the UI layer thin and did not duplicate API rules.
Official documentation
- AgentPay reference repository — Its
apps/web/testsis the reference E2E suite. - Playwright — Writing tests & locators — Actions, assertions and user-facing locators.
- Playwright — Best practices — Keeping the UI suite thin and reliable.
Next: mobile tests, and running the whole suite in CI.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship