Test Automation in Practice
    end-to-end testingSPA testingQA

    How to Test Single-Page Apps

    Part 9 of Test Automation in Practice: SPAs move routing, state, and rendering into the browser, which breaks assumptions that older test approaches relied on — what changes and how to test them reliably.

    Last updated: July 22, 2026
    by Manta AI Team4 min read

    Single-page applications — React, Vue, Angular, Svelte — handle routing, state, and rendering on the client. The server sends one HTML shell and a bundle of JavaScript; everything after that happens in the browser. That makes SPAs fast and app-like, and it breaks several assumptions that test tooling built for server-rendered pages relied on. Tests that were reliable against a traditional multi-page site turn flaky against a SPA for reasons that aren't obvious.

    What changes with a SPA

    • Navigation doesn't reload the page. Clicking a link changes the URL and swaps the view, but there's no full document load. Tests that wait for a "page loaded" or "navigation complete" event wait forever, or fire immediately and then act on the old view.
    • Content appears asynchronously, in stages. The shell renders, then a loading state, then data arrives and the real content renders — often in several waves as different components resolve their own requests. The element you want to interact with may not exist for a few hundred milliseconds after the "page" appears to be there.
    • State lives in memory, not in the document. A server-rendered app persists a lot of state in the URL or in server sessions. A SPA often holds it in a client-side store that a refresh wipes. Deep-linking into a mid-flow state — step 3 of a wizard, a filtered table — may not restore the way a tester expects, or may not be supported at all.
    • The DOM is volatile. Frameworks re-render freely in response to state changes. An element reference or a query result captured a moment ago can be stale by the time the test uses it, because the framework replaced that part of the tree.
    • Routing has guards and lazy loading. Route changes can trigger auth checks, redirects, and on-demand bundle downloads, each of which adds a pause the test has to account for.

    How to test them reliably

    • Wait on application state, never on time or on page-load events. Wait for the specific element to be present and interactable (visible, enabled, not covered). Wait for the network request that populates the view to resolve. Never sleep() — the app's timing depends on latency and data size, so a fixed wait is both slow and flaky. This is the single biggest source of flaky SPA tests.
    • Select by role or intentional test IDs, never by DOM structure. Frameworks re-render and restructure the tree constantly; a data-testid the team commits to keeping, or an accessible role and name, survives that. A div:nth-child(3) > span does not.
    • Test client-side routing explicitly. Navigate the way a user does — click the link — and then verify both that the URL changed and that the expected view rendered. A URL that updates without the view following is a real and common SPA bug.
    • Check refresh and deep-link behaviour. Reload the page partway through a flow and confirm the app recovers gracefully — restores the state, or redirects somewhere sensible — rather than showing a blank screen or an error. Do the same for pasting a deep link into a fresh tab.
    • Account for the loading states. Assert that spinners and skeletons appear and then disappear, not just that the final content shows up. A stuck loading state is a bug your "wait for content" logic might mask.
    • Handle auth as a first-class case. Token expiry, silent refresh, and route guards all behave differently in a SPA than in a server-rendered app — a token can expire mid-session with no page load to trigger a redirect. See testing authentication flows.

    Where autonomous testing helps

    Most of the friction above comes from a scripted test carrying assumptions about timing and structure that a SPA violates. An autonomous agent doesn't carry those assumptions. It navigates client-side routes the way a user does, waits for the app to reach a stable rendered state before acting, and re-derives how to reach and interact with each element on every run rather than replaying stored selectors. Re-renders, async content, and lazy-loaded routes are handled because the agent is reacting to the app's actual state, not a recording of it.

    See how to test a React app end to end, and the framework-specific equivalents for Next.js, Vue, and Angular — each covers the gotchas particular to that stack.

    Bottom line

    Testing a SPA reliably comes down to a few disciplines: wait on state instead of on loads, select by role or test ID instead of by structure, test client-side routing and refresh behaviour explicitly, verify loading states resolve, and treat auth as its own problem. Tools that assume server-rendered navigation will fight you the whole way; ones that model the app as a user actually experiences it won't. The end-to-end testing guide has the full context.