How to Test Pagination on Web (Complete Guide)
Pagination controls how users navigate large data sets on the web. When it fails, users cannot reach needed information, forms lose context, and infinite‑scroll loops can exhaust browser memory. In pr
Why Pagination Testing Matters
Pagination controls how users navigate large data sets on the web. When it fails, users cannot reach needed information, forms lose context, and infinite‑scroll loops can exhaust browser memory. In production, broken pagination shows up as missing pages, duplicate entries, or server errors that only appear under load. Because pagination touches routing, state management, API contracts, and UI rendering, a single defect can cascade into broken analytics, abandoned funnels, and compliance gaps (e.g., WCAG 2.4.7 Focus Visible). Testing it early prevents costly hotfixes and protects user trust.
Core Concepts of Pagination
Understanding the mechanics behind pagination helps you design effective tests.
Types of Pagination
- Numbered links – classic “1 2 3 … 10” UI.
- Load more / infinite scroll – a button or automatic fetch that appends rows.
- Cursor‑based – uses opaque tokens (often base64‑encoded) to request the next slice.
- Page size selector – lets users change how many items appear per page.
Typical Implementation Flow
- User interacts with a pagination control (click, scroll, keypress).
- Front‑end updates URL query params (
?page=2&size=20) or internal state. - A network request fetches the next slice (REST, GraphQL, or WebSocket).
- Response data is merged with existing list, UI updates, and focus may shift.
- Browser history may be updated via
pushStateorreplaceState.
Failure Modes to Watch For
- Stale state – UI shows old page number while requesting a new slice.
- Missing parameters – request drops
sizeorsort, returning wrong data. - Duplicate entries – same item appears on two pages due to off‑by‑one errors.
- Empty pages – backend returns zero results but UI still shows a next button.
- Focus loss – keyboard users lose focus after a page change, violating WCAG.
- Security leakage – pagination tokens expose internal IDs or allow enumeration.
Test Matrix for Pagination
Below is a comprehensive matrix that covers happy paths, error paths, edge cases, accessibility, and security concerns. Each cell indicates a test objective; you can mark it as PASS/FAIL during execution.
| Category | Test ID | Description | Expected Result |
|---|---|---|---|
| Happy Path | HP1 | Navigate from page 1 to the last page using numbered links. | URL updates correctly, data matches server page, no duplicate/missing items. |
| Happy Path | HP2 | Click “Load more” until all items are loaded. | Request count equals ceil(total/size); final list length equals total items. |
| Happy Path | HP3 | Change page size via dropdown and verify first page reflects new size. | Request includes new size param; UI shows correct number of items. |
| Error Path | EP1 | Request a page number beyond the last page (e.g., ?page=999). | Server returns empty array or 404; UI shows “No results” and disables next button. |
| Error Path | EP2 | Tamper with cursor token (invalid base64). | Server returns 400 Bad Request; UI shows error toast and does not crash. |
| Error Path | EP3 | Simulate network failure on a page‑change request. | UI displays retry button; no partial data is shown; state reverts to previous page. |
| Edge Case | EC1 | Rapid double‑click on next button (two clicks within 50 ms). | Only one request sent; UI does not queue duplicate requests. |
| Edge Case | EC2 | Scroll to bottom while a load‑more request is pending (infinite scroll). | No second request triggered until first resolves; UI shows loading indicator. |
| Edge Case | EC3 | Change page size while on a middle page (e.g., size=10 on page 3). | New request uses same page index but new size; UI recalculates start offset. |
| Edge Case | EC4 | Browser back/forward navigation after paginated navigation. | URL and UI reflect the exact page state at that history entry. |
| Accessibility | AC1 | Keyboard navigation: Tab into pagination controls, use Arrow keys to change page. | Focus moves predictably; screen reader announces new page number and item count. |
| Accessibility | AC2 | Verify sufficient contrast for disabled vs. enabled pagination buttons (WCAG 2.1). | Contrast ratio ≥ 4.5:1 for normal text, ≥ 3:1 for large text. |
| Accessibility | AC3 | Ensure ARIA labels or aria‑pressed reflect current page state. | Assistive tech announces “Page 3 of 10, button disabled” when appropriate. |
| Security/Privacy | SE1 | Inspect pagination tokens in URL or headers for predictable patterns. | Tokens are opaque, high‑entropy, and not guessable; no sequential IDs exposed. |
| Security/Privacy | SE2 | Attempt to enumerate users by iterating page numbers with a small page size. | Rate limiting or authentication prevents mass enumeration; logs show abnormal activity. |
| Performance | PE1 | Measure time to first byte and DOM update for a page change under 3G throttling. | Total latency ≤ 2 seconds; UI shows loading spinner; no layout thrash. |
| Performance | PE2 | Verify that rapid page changes do not cause memory leak (detach listeners). | Heap size remains stable after 50 rapid navigations (Chrome DevTools memory tab). |
Manual Testing Approach
A structured manual session catches issues that automated scripts might overlook, especially those tied to timing, visual state, or assistive tech.
Preparation
- Identify pagination variants – list all places where pagination appears (tables, lists, galleries, infinite feeds).
- Gather test data – ensure a known total count (e.g., 253 items) and ability to control page size via API or UI.
- Set up environment – disable caching, enable network throttling (Chrome DevTools → Network → Slow 3G), and turn on “Show layout shift regions”.
Execution Steps
- Load the page – verify initial request includes default
pageandsize. - Navigate via numbered links – click each link sequentially; after each click:
- Check URL updates (
?page=n). - Confirm request payload matches expected page.
- Validate that the rendered list contains exactly the items for that page (compare against a fixture).
- Ensure no duplicate items appear when moving forward then backward.
- Test “Load more” – repeatedly click until the button disappears or changes state.
- After each click, verify that the new items append without duplication.
- When the button is disabled, confirm that the total number of displayed items equals the server‑reported total.
- Alter page size – open the size dropdown, select a new value (e.g., 50).
- Verify that the request includes the new size and that the first page reflects the change.
- Paginate through the new set and ensure the total pages adjust correctly (
ceil(total/newSize)).
- Simulate error conditions – use DevTools to block specific requests or modify responses.
- For a 404 on page 5, confirm that UI shows an error message and does not attempt to load further pages.
- For a malformed JSON response, ensure the UI gracefully handles the error (toast, retry).
- Keyboard and screen‑reader checks –
- Tab into the pagination component; use Arrow‑Right/Left to change page.
- Listen with NVDA or VoiceOver: announcements should include current page, total pages, and whether the next/previous button is disabled.
- Verify focus returns to the first item of the new page after a page change.
- Visual regression – take screenshots of the pagination bar at each state (first, middle, last, disabled) and compare against a baseline with a tool like Percy or Storybook shots.
Documentation
Record each step in a test‑case management tool (e.g., TestRail) with columns for Test ID, Steps, Expected, Actual, Status, and Notes. Attach network logs and screenshots for failures. This traceability helps developers reproduce timing‑dependent bugs.
Automated Approaches and Tooling
Automation accelerates regression and enables continuous validation across browsers and devices.
Choosing a Framework
- Playwright – excellent for network interception, multiple contexts, and tracing.
- Cypress – strong for UI assertions but limited cross‑origin network control.
- Selenium/WebDriverIO – viable if you already have a Selenium grid.
Below we illustrate Playwright because it offers fine‑grained request mocking and built‑in accessibility testing.
Basic Setup
# Install dependencies
npm i -D @playwright/test playwright-axe
npx playwright install
Test File: pagination.spec.js
const { test, expect } = require('@playwright/test');
const { injectAxe, checkA11y } = require('playwright-axe');
test.describe('Pagination component', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://example.com/products');
await injectAxe(page);
});
test('navigates via numbered links', async ({ page }) => {
// Grab total count from an API stub or UI badge
const totalText = await page.textContent('.total-items');
const total = parseInt(totalText.replace(/\D/g, ''), 10);
const pageSize = 20;
const lastPage = Math.ceil(total / pageSize);
for (let p = 1; p <= lastPage; p++) {
await page.click(`text=${p}`);
await expect(page).toHaveURL(/.*[?&]page=${p}/);
const items = await page.$$eval('.product-card', els => els.length);
expect(items).toBe(p === lastPage ? total % pageSize || pageSize : pageSize);
}
});
test('load more button works', async ({ page }) => {
await page.click('text=Load more');
await page.waitForResponse(resp => resp.url().includes('/api/products') && resp.status() === 200);
const firstBatch = await page.$$eval('.product-card', els => els.length);
// Assume API returns 20 per load
expect(firstBatch).toBe(20);
await page.click('text=Load more');
await page.waitForResponse(resp => resp.url().includes('/api/products') && resp.status() === 200);
const secondBatch = await page.$$eval('.product-card', els => els.length);
expect(secondBatch).toBe(40);
});
test('handles out‑of‑range page gracefully', async ({ page }) => {
await page.goto('https://example.com/products?page=999');
await expect(page.locator('.no-results')).toBeVisible();
await expect(page.locator('button[aria-label="Next page"]')).toBeDisabled();
});
test('keyboard navigation announces page change', async ({ page }) => {
await page.focus('.pagination-next');
await page.press('ArrowLeft'); // move to previous page button
await expect(page).toHaveFocus('.pagination-prev');
const announcement = await page.locator('[aria-live="polite"]').innerText();
expect(announcement).toContain('Page');
});
test('passes basic axe accessibility checks', async ({ page }) => {
const accessibilityScanResults = await checkA11y(page, {
// exclude known false positives if any
excludedSelectors: ['.cookie-banner']
});
expect(accessibilityScanResults.violations).toEqual([]);
});
});
#### Explanation of Key Techniques
- Network assertions –
page.waitForResponseensures the request fired and succeeded before checking UI. - Dynamic total calculation – reading a UI badge or making a preliminary API call avoids hard‑coding totals, making the test robust against data changes.
- Accessibility injection –
playwright-axeruns axe-core on the page after each action, catching missing labels, contrast issues, and focus order problems. - Trace capture – add
trace: 'on'toplaywright.config.tsto record a trace for flaky test analysis.
Parallel Execution Across Browsers
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
workers: process.env.CI ? 4 : 2,
reporter: 'html',
use: {
baseURL: 'https://example.com',
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Run with npx playwright test. This matrix guarantees that pagination behaves identically across rendering engines.
Leveraging Mock Servers for Edge Cases
Use MSW (Mock Service Worker) or Playwright’s route to simulate latency page.route to inject latency, error codes, or malformed payloads.
test('shows retry UI on 500 error', async ({ page }) => {
await page.route('**/api/products', async route => {
const response = await route.fetch();
// Force a 500 on the second request
if (response.url().includes('page=2')) {
return route.fulfill({ status: 500, body: JSON.stringify({ error: 'server' }) });
}
return route.continue();
});
await page.click('text=2');
await expect(page.locator('.error-banner')).toContainText('Something went wrong');
await expect(page.locator('button:has-text("Retry")')).toBeEnabled();
});
Edge Cases That Only Show Up in Production
Some defects stay hidden in staging because they depend on real‑world data volume, user behavior patterns, or infrastructural quirks.
1. Skewed Data Distribution
When a dataset contains a few extremely large records (e.g., a user profile with a 10 MB avatar), the payload size varies per page. A page that happens to include several large items may exceed the browser’s memory budget, causing a slowdown or crash, while other pages are fine.
Test: Use a data generator that inserts a few “heavy” rows at known offsets (e.g., every 500th record). Run the pagination flow and monitor Chrome’s Performance tab for JS heap spikes (> 150 MB) or long frames (> 50 ms).
2. Race Conditions with Cache Invalidation
If the frontend caches pages in a Map or Redux store and the backend updates data while the user is paging, the UI may show stale items or duplicate entries after a refresh.
Test: While a pagination request is in flight, use DevTools to modify the API response (change an item’s ID). After the request resolves, manually trigger a cache‑busting reload (e.g., change a query param) and verify that the UI reflects the updated data without showing both old and new versions of the same record.
3. Infinite Scroll Threshold Miscalculation
Some implementations trigger the next load when the scroll position is within 200 px of the bottom. On high‑resolution screens or when the browser’s zoom level changes, the threshold may fire too early or too late, resulting in duplicate requests or a perceived “stall”.
Test: Set device pixel ratio to 2.0 (via Playwright’s use: { deviceScaleFactor: 2 }) and zoom to 150 %. Scroll to the bottom and count network requests; ensure exactly one request fires per visual viewport transition.
4. Server‑Side Cursor Exhaustion
Cursor‑based pagination can leak state if the server does not invalidate old cursors after a dataset mutation (e.g., a record deletion). Users holding an old cursor may skip items or see gaps.
Test: Create a cursor for page 3, then delete a record that lies before that cursor via an admin API. Use the old cursor to request the next page and assert that the returned set does not contain a gap (i.e., item indices are contiguous).
5. Interactions with Browser Extensions
Ad blockers or privacy extensions sometimes strip query parameters they deem tracking (e.g., utm_*). If your pagination relies on those parameters for state, the extension can break navigation.
Test: Install a popular extension like uBlock Origin in a headless Chrome variant, launch the app, and paginate through several pages. Confirm that URL parameters remain intact and that the UI does not fall back to the first page.
6. Pagination Combined with Lazy‑Loaded Images
When each page includes images that lazy‑load via IntersectionObserver, a rapid page change can leave observers attached to removed elements, causing memory leaks or stray network calls for images that are no longer in the DOM.
Test: Use Chrome’s DevTools → Memory → Take heap snapshot before and after 20 rapid page changes. Compare snapshot diffs; ensure no detached IntersectionObserver objects accumulate.
Document each of these edge cases in your test plan, and add a corresponding automated check where feasible (e.g., using Playwright to set device scale factor, inject latency, or manipulate cached responses).
Accessibility and Security Considerations
Pagination intersects with both accessibility guidelines and security best practices. Overlooking either can lead to compliance risks or exploitable flaws.
Accessibility Checklist (WCAG 2.2)
| Criterion | What to Verify | How to Test |
|---|---|---|
| 2.4.3 Focus Order | Focus moves logically when paging via keyboard. | Tab into pagination controls; verify focus order matches visual order. |
| 2.4.7 Focus Visible | Keyboard focus indicator is visible. | Ensure outline or custom focus style meets 3:1 contrast against background. |
| 1.4.3 Contrast (Minimum) | Text and icons on pagination buttons meet contrast ratio. | Use axe-core or manual colour contrast analyzer. |
| 1.3.1 Info and Relationships | Page number conveyed via ARIA label or aria‑current. | Inspect DOM: . |
| 4.1.2 Name, Role, Value | Custom pagination widgets have appropriate roles. | If using divs, add role="navigation" and aria-label="Pagination"; ensure keyboard operability. |
| 2.5.3 Label in Name | Visible label matches accessible name. | For a button showing “Next”, aria-label should be “Next page” or exactly “Next”. |
| 2.2.2 Pause, Stop, Hide | Auto‑advancing carousels (if used as pagination) can be paused. | If autoplay exists, verify a pause button is present and functional. |
Run these checks automatically with playwright-axe or eslint-plugin-jsx-a11y in your CI pipeline.
Security and Privacy Review
| Concern | Risk | Mitigation Test |
|---|---|---|
| Token enumeration | Attackers guess next/prev cursor to scrape data. | Attempt to brute‑force a cursor space (e.g., increment base64 number) and verify server responds with 429 Too Many Requests or returns empty set after a threshold. |
| Parameter injection | Malicious page or size values cause excessive load. | Send size=1000000 and ensure server caps the value (e.g., max 200) and returns a 400 if out of bounds. |
| Information leakage via headers | Content-Range or X-Total-Count reveals dataset size. | Confirm that these headers are either omitted or only exposed to authenticated users with appropriate scope. |
| Clickjacking | Malicious site frames your pagination and tricks users into clicking. | Verify X-Frame-Options: DENY or Content‑Security‑Policy: frame-ancestors 'self' is present. |
| CSRF on state‑changing pagination (e.g., “delete page” actions) | Unauthorized modification of pagination state. | Ensure any POST/PUT/DELETE that alters pagination state requires a valid CSRF token or SameSite cookie. |
Automate security checks with tools like OWASP ZAP passive scan integrated into your nightly build, or write specific Playwright tests that attempt the above payloads and assert the expected defensive responses.
Using Autonomous Persona‑Driven Exploration (SUSA Mention)
Scripted tests excel at verifying known paths, but they often miss emergent behavior that appears only when users interact with the app in unexpected ways. Autonomous QA platforms like SUSATest address this gap by simulating diverse user personas—each with its own timing, error‑prone tendencies, and exploration strategies—without requiring you to write explicit scenarios.
When you point SUSATest at a paginated web view, it will:
- Curious persona – clicks every page number, hovers over links, and opens context menus to discover hidden controls (e.g., a “Jump to page” input that appears only on long‑press).
- Impatient persona – rapidly clicks “Next” or scrolls the infinite scroll, triggering race conditions and exposing missing debounce logic.
- Novice persona – relies on visual cues alone, often missing keyboard‑only navigation, thus surfacing missing focus outlines or low‑contrast disabled states.
- Adversarial persona – deliberately sends malformed pagination parameters (negative page numbers, huge size values) and attempts to enumerate IDs via cursor tampering.
- Elderly / accessibility persona – uses simulated tremor and enlarged fonts, revealing touch‑target size problems and insufficient ARIA labeling.
Because SUSATest remembers which screens it has already visited and which actions led to dead ends, each subsequent run becomes smarter: it avoids re‑executing known‑good flows and concentrates on unexplored branches, such as a “Show 200 items” toggle that only appears after a certain number of scrolls. The platform then auto‑generates regression scripts (Appium for Android WebView, Playwright for pure web) that capture the exact sequences that produced a failure, giving developers a reproducible starting point.
Integrating SUSATest into a CI pipeline is as simple as:
pip install susatest-agent
susatest run --url https://your-app.com --mode web --output ./susatest-reports
The resulting report lists each persona’s findings, complete with screenshots, console logs, and network waterfalls. You can then convert high‑impact discoveries into deterministic tests (as shown earlier) to prevent regression.
Checklist for Pagination Testing
Use this concise list before marking a pagination feature as ready for release.
- [ ] Happy path: numbered links, load more, page size selector all update URL and display correct data slice.
- [ ] Error handling: out‑of‑range page, invalid cursor, network failure, and server error states show user‑friendly messages and do not crash the app.
- [ ] State integrity: no duplicate or missing items when navigating forward then backward; cache invalidation works when underlying data changes.
- [ ] Keyboard & screen‑reader: focus moves predictably, announcements include current/total page, disabled states are conveyed.
- [ ] Accessibility contrast: all interactive elements meet WCAG 2.1 AA contrast thresholds.
- [ ] Security: pagination tokens are opaque, rate‑limited, and resistant to enumeration; malicious size/page values are clamped or rejected.
- [ ] Performance: under throttled 3G, page change completes within 2 s; no memory leak after 50 rapid navigations.
- [ ] Cross‑browser: behavior consistent in Chromium, Firefox, and WebKit (tested via Playwright matrix).
- [ ] Production‑edge: validated with skewed data, race‑condition simulation, high DPI/zoom, and common browser extensions.
Run the checklist manually for exploratory verification, then automate the majority of items via your chosen framework.
Closing Takeaways
Effective pagination testing blends rigorous scripted validation with exploratory, persona‑driven discovery. Start by mapping out every pagination variant in your application, then construct a test matrix that covers happy paths, error paths, edge cases, accessibility, and security. Implement the matrix using a framework that offers strong network control and accessibility auditing—Playwright paired with axe‑core is a solid choice. Supplement those tests with manual sessions that focus on timing, visual state, and assistive‑technology interaction.
Remember that production‑only bugs often stem from data variability, user‑induced race conditions, or environmental factors like high‑DPI screens and extensions. Address those by injecting realistic load, simulating latency, and testing with varied device profiles.
Finally, consider leveraging an autonomous QA tool such as SUSATest to surface hidden regressions that scripted tests never think to pursue. The platform’s persona‑driven exploration complements your deterministic suite, turning rare, intermittent failures into actionable, fixable defects.
By following the matrix, checklist, and the combined manual/automated strategy outlined here, you’ll ship pagination that is reliable, inclusive, and resilient—no matter how large the data set grows or how diverse your audience becomes. Happy testing.
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