Single-Page Application (SPA) Testing Guide
Single-page apps (SPAs) replace server-rendered HTML with client-side routing and JavaScript-heavy UI. They need different testing strategies than traditional web apps. This guide covers the concerns
Single-page apps (SPAs) replace server-rendered HTML with client-side routing and JavaScript-heavy UI. They need different testing strategies than traditional web apps. This guide covers the concerns unique to SPAs and how to address them.
What makes SPAs tricky
- Client-side routing (pushState, no page reloads)
- Async data loading
- State hydration from server-rendered snapshots (some SPAs)
- Service workers may cache aggressively
- SEO considerations (crawlers see empty shell)
- Bundle loading / lazy chunks
Test tiers
Unit
React / Vue / Angular components with Jest, Vitest, Jasmine. Fast, no browser.
Integration
Component trees with React Testing Library or Vue Test Utils. Mock API, assert rendered output.
E2E
Playwright / Cypress. Real browser, real bundle, real API (or realistic mock).
Visual regression
Percy / Applitools / Chromatic. Snapshot each route / state.
Routing
Test every route
Navigation works:
test('navigates to user profile', async ({ page }) => {
await page.goto('/');
await page.click('text=Profile');
expect(page.url()).toMatch(/\/profile/);
});
Browser back / forward
await page.click('text=Profile');
await page.click('text=Settings');
await page.goBack();
expect(page.url()).toMatch(/\/profile/);
Deep link
Direct URL navigation should work:
await page.goto('/user/123');
// Assert page loaded, data fetched
State management
Redux / Zustand / Pinia / Vuex
Test stores in isolation (unit tests). Integration: mount component, dispatch actions, assert state.
URL as state
Filters, pagination in URL params. Test: change filter → URL updates → reload → same state.
Data loading
Loading states
Every async boundary has loading UI. Test: mock slow API, assert spinner shown.
Error states
API returns 500 → user sees error with retry.
Empty states
No data → meaningful empty state.
Stale data
Cache then navigate away / back. Is cached data shown or refetched?
Authentication
Login / logout routes
Protected routes redirect to login. After login, redirect to intended destination.
Token refresh
Silent refresh mid-navigation works.
Session expiry mid-use
Graceful handling.
SEO
SPAs need special SEO consideration:
- Server-side rendering (SSR) via Next.js, Nuxt, SvelteKit, Remix
- Static site generation (SSG) for content pages
- Prerender for crawlers (Rendertron)
Test: curl https://myapp.com | grep " should show meaningful title, not generic SPA shell.
Accessibility
Focus management
Route change → focus moves to h1 / main (announced to screen readers).
Live regions
Dynamic content updates announced.
Keyboard navigation
Every interactive element reachable via Tab.
Performance
Bundle size
Main bundle < 200KB ideally. Code splitting for routes.
Core Web Vitals
- LCP < 2.5s
- FID < 100ms (or INP for new metric)
- CLS < 0.1
Lazy loading
Non-critical chunks load after initial paint. Test: Chrome DevTools network tab, verify below-the-fold chunks deferred.
Service worker
If using:
- SW registers after load
- Cache strategies tested (cache-first, network-first)
- Update handling clear to user
SPA-specific bugs
Memory leak on route change
Listeners not cleaned up. Long session → sluggishness.
Fix: useEffect cleanup, unmount handlers.
Global state leak between tests
Singleton stores persist between tests. Test isolation broken.
Fix: Reset state in beforeEach.
Race condition on rapid navigation
User navigates fast; pending API responses update wrong page.
Fix: AbortController per request, cancel on unmount.
Cached data wrong after login
Previous user's data cached; after login, new user sees it briefly.
Fix: Clear cache on login / logout.
URL out of sync with UI
User changes filter, URL does not update, or vice versa.
Fix: Single source of truth for state in URL.
How SUSA tests SPAs
SUSA drives SPAs via Playwright. Detects:
- Route changes and state changes
- Missing loading / error states
- Broken back navigation
- Console errors
- Accessibility issues (via axe-core)
susatest-agent test https://myapp.com --persona curious --steps 200
Common antipatterns
- Too much in main bundle — slow first paint
- No SSR for content-heavy routes — SEO suffers
- useEffect without dependency array — infinite loops
- Refetching on every navigation — no cache
- Over-caching — stale data
SPAs are the default for web in 2026. Test well; performance and correctness compound.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free