Skip to content

Admin E2E: Testing Role and Bidang Guards Through the UI

Adityo Guni Waluyo

Three Playwright specs for the admin area: login through the real UI, CRUD down to permanent delete with API verification, and role/bidang guards tested up to the server's 403.

TL;DR

We moved admin E2E tests beyond happy paths to guardrails, logging in via UI and reusing the localStorage token. It covers full CRUD in the Tiptap editor and trash flow, verifying permanent deletes via API not just UI. It also validates authorization, hiding user management from editors and blocking bidang users with a 403.

Testing the Invisible Guardrails in Our Admin CMS

I was reviewing the production logs for the DISPORAPAR Depok "MuDe" project when I was reviewing the admin area of the DISPORAPAR Depok "MuDe" project and realized our end-to-end tests only covered the happy path. We were testing that features worked, but we were completely ignoring the boundaries of who was allowed to use them.

When defining the scope for this spec module, I initially assumed that writing a few scripts to click through the admin dashboard would be sufficient. I thought if the UI rendered the correct components and the backend returned a 200 OK for the admin, the backend guards were inherently working. I was wrong.

It turned out that UI tests alone cannot prove the absence of a vulnerability. You cannot reliably assert that a restricted menu does not exist in the DOM without also verifying that the backend actually rejects the unauthorized action. E2E for the admin module must test the guardrails, not just the happy path. Role and bidang guards are behaviors users never see until the day it matters. Permanent delete must be verified at the API level because the UI cannot prove absence.

Login through the real UI, token from localStorage

I started by building a new frontend/e2e/admin/ folder containing three specs and a helpers.ts file, all runnable via run_page.sh admin. The first hurdle was authentication. I decided to log in via the real UI rather than injecting cookies, because I wanted to test the actual user flow and catch any regression in the login form itself.

In helpers.ts, I used page.getByLabel("Email") and getByLabel("Kata sandi", { exact: true }). The exact: true flag was non-negotiable here because a "Tampilkan kata sandi" toggle button shares the same label, which would otherwise cause Playwright to throw a strict mode violation. After a successful login, the helper reads the token back from localStorage under the key mude_admin_token. This token is then reused for subsequent API checks, keeping the UI flow fast and stable while maintaining realistic state.

The login.spec.ts file now comprehensively covers failed login attempts, successful login followed by a reload and logout, and a negative-control helper to ensure a clean state between tests.

CRUD down to permanent delete

Playwright best practices dictate that we must test user-visible behavior and avoid implementation details like function names or fragile CSS classes 4. Before any click() action, Playwright automatically checks that the locator resolves to exactly one element, is visible, stable, receives events, and is enabled. These auto-retrying assertions remove flakiness from our tests, which is critical when dealing with dynamic admin dashboards 5.

This philosophy directly guided the informasi-crud.spec.ts. Creating an article via the editor form required a specific approach because the Tiptap rich text editor is not a native input field. The spec clicks the .ProseMirror node and then types the content. It expects a redirect to /admincms/informasi/{id}, verifies the title appears in the list, and tests the entire trash flow.

Crucially, for the permanent delete action, the UI alone is not enough. The UI can show that a record disappeared from the list — but it cannot prove absence in the database. Therefore, the spec verifies via a direct API call that the record is truly gone. It also includes an empty-title validation case to ensure the form rejects invalid data. All test data is prefixed with "E2E" and meticulously cleaned up afterward to prevent database pollution.

The guards that are tested at the server

The most critical addition was users-guard.spec.ts. I firmly believe that authorization bugs are the most dangerous because they silently expose data without throwing obvious errors. This spec verifies that the Pengguna menu is hidden for the editor role. It checks the route guard redirect and ensures the bidang chip displays in proper Title Case.

We also test creating a bidang-scoped user. When a bidang user attempts to POST a new user, the test expects a strict 403 response. Playwright allows us to send API requests directly from UI tests to establish preconditions and validate these exact postconditions 6. This hybrid approach gives us the confidence that the guardrails hold firm under pressure, even when the UI tries to mislead us.

I closed the INDEX.md row for the admin module, knowing that we are no longer flying blind. Testing the invisible boundaries is what separates a fragile demo from a production-ready system.

Sources

[1] https://playwright.dev/docs/best-practices

[2] https://playwright.dev/docs/actionability

[3] https://playwright.dev/docs/api-testing

Related articles