How to Automate Pagination Testing (Step-by-Step)
How to Automate Pagination Testing (Step-by-Step)
How to Automate Pagination Testing (Step-by-Step)
Pagination is a common UI pattern that lets users navigate large data sets without overwhelming the interface. When pagination breaks—whether due to missing next‑button states, incorrect page numbers, or stale data—users experience frustration and conversion loss. Automating pagination testing catches these regressions early, provides repeatable verification across environments, and frees manual testers to focus on exploratory work. This guide walks you through a complete, production‑ready approach: deciding when automation pays off, picking a framework, designing a reliable test matrix, crafting locators, handling waits, managing data, integrating with CI, and reporting results. Real code snippets in Playwright (TypeScript) and Selenium (Python) illustrate each step, and a short section shows how an autonomous QA platform can bootstrap pagination tests without writing a single line of script.
Why Automate Pagination Testing (Step-by‑Step)
Automating pagination testing delivers measurable returns when the pagination component is exercised frequently, has complex state, or is prone to environment‑specific flakiness. Consider the following decision factors:
| Factor | Manual testing sufficient? | Automation adds value? |
|---|---|---|
| Simple static list with two pages | ✅ Low effort, rare regression | ❌ Overhead outweighs benefit |
| Dynamic data fetched per page, infinite scroll fallback | ❌ Hard to verify all states manually | ✅ Repeated validation of loading, empty, error states |
| Multiple user roles with different page sizes | ❌ Tedious to switch accounts and verify each | ✅ Parameterized tests cover matrix efficiently |
| Pagination coupled with filtering, sorting, or lazy‑loaded images | ❌ Manual checks miss timing issues | ✅ Synchronized waits catch race conditions |
| Frequent UI redesigns of the pager component | ✅ If redesign is infrequent, manual spot‑checks ok | ❌ Constant script maintenance may erode ROI |
If your product meets two or more of the “Automation adds value” checks, invest in automated pagination tests. The payoff appears as reduced escape defects, faster release cycles, and confidence that core navigation works for every persona.
How to Automate Pagination Testing (Step‑by‑Step): Choosing the Right Framework
Selecting a test framework hinges on the technology stack, team skill set, and desired execution speed. Below is a comparison of the most popular choices for web pagination testing.
| Framework | Language | Strengths for Pagination | Weaknesses | Typical Setup Time |
|---|---|---|---|---|
| Playwright | TypeScript/JavaScript, Python, .NET, Java | Auto‑waits, built‑in network interception, easy page‑object creation, supports Chromium/Firefox/WebKit | Newer ecosystem, fewer community plugins than Selenium | 10‑15 min (npm init + install) |
| Selenium WebDriver | Java, C#, Python, Ruby, JavaScript | Mature, extensive language bindings, Grid for parallel execution | Requires explicit waits, more boilerplate, flaky if not tuned | 20‑30 min (driver binaries + bindings) |
| Cypress | JavaScript/TypeScript | Fast test runner, time‑travel debugging, automatic waiting | Limited cross‑browser support (Chrome‑family only), no native mobile testing | 12‑18 min (npm install) |
| TestCafe | JavaScript/TypeScript | No WebDriver needed, runs on any browser that supports HTML5 | Smaller community, less flexibility for custom drivers | 10‑15 min (npm install) |
| Appium (for mobile pagination) | Java, Python, JavaScript, etc. | Tests native and hybrid apps, supports gestures (swipe, scroll) | Requires device/emulator setup, slower execution | 25‑35 min (Android SDK + Appium server) |
If your team already writes TypeScript for front‑end code, Playwright offers the lowest friction and built‑in waiting mechanisms that reduce flaky pagination tests. For organizations heavily invested in Java, Selenium with TestNG or JUnit remains a solid choice. Mobile pagination (e.g., infinite‑scroll lists in Android) is best handled with Appium, though the same principles of locator strategy and data setup apply.
How to Automate Pagination Testing (Step‑by‑Step): Building a Test Matrix
A well‑defined test matrix captures the combinatorial space of pagination scenarios. Start by identifying the variables that affect pagination behavior, then enumerate meaningful combinations. The table below shows a typical matrix for an e‑commerce product listing page.
| Variable | Values | Description |
|---|---|---|
| Page size | 10, 25, 50, 100 | Number of items per page (often configurable via UI or API) |
| Total items | 0, 7, 23, 101, 1000 | Edge cases: empty, partial last page, exact multiples, large data set |
| Sort order | Ascending price, Descending rating, Default | Ensures paging respects sorting |
| Filter state | No filter, Category=Electronics, Price<$50 | Verifies that pagination preserves filters |
| User role | Guest, Registered, Admin | Checks for role‑based visibility of items |
| Network condition | Online, 3G throttled, Offline | Tests loading spinners, error handling, retry logic |
| Device | Desktop Chrome, Mobile Safari, Tablet Firefox | Confirms responsive pager layout |
From this matrix, you can generate test cases programmatically (e.g., using a data‑driven test framework) or manually select a representative subset. A pragmatic approach is to run the full matrix on every nightly build and a smoke subset (page size=25, total items=101, default sort, no filter, guest user, online, desktop) on each pull request.
Generating Combinations in Code (TypeScript Example)
import { test, expect } from '@playwright/test';
const pageSizes = [10, 25, 50, 100];
const totalItemsList = [0, 7, 23, 101, 1000];
const sorts = ['price-asc', 'rating-desc', 'default'];
const filters = ['none', 'electronics', 'under-50'];
const roles = ['guest', 'registered', 'admin'];
test.describe('Pagination matrix', () => {
for (const size of pageSizes) {
for (const total of totalItemsList) {
for (const sort of sorts) {
for (const filt of filters) {
for (const role of roles) {
test(`size=${size} total=${total} sort=${sort} filter=${filt} role=${role}`, async ({ page }) => {
// Setup: login as role, apply filter/sort, set page size via API or UI
await setupPagination(page, role, size, sort, filt, total);
// Verify first page loads correctly
await expect(page.locator('.item')).toHaveCount(Math.min(size, total));
// Navigate to last page if applicable
if (total > size) {
await gotoLastPage(page);
await expect(page.locator('.item')).toHaveCount(total % size || size);
}
});
}
}
}
}
}
});
The helper functions setupPagination, gotoLastPage, and any API calls abstract away setup details, keeping the test readable while still exercising the full matrix.
Locator Strategies for Pagination Controls
Stable locators are the foundation of reliable pagination tests. Avoid brittle selectors like XPath that depend on exact DOM hierarchy; instead, favor attributes that convey intent and are less likely to change during redesigns.
Recommended Attributes
| Attribute | Example | Reason |
|---|---|---|
data-testid | data-testid="pagination-next" | Explicitly added for testing; immune to styling changes |
aria-label | aria-label="Go to next page" | Leverages accessibility labels, doubles as a11y check |
role + name | role="button" name="Next" | Works with Playwright’s built‑in locators |
class (only if stable) | .pager__next | Use only when the class is part of a design system with versioned CSS |
Playwright Example Using data-testid
async function clickNext(page) {
await page.click('[data-testid="pagination-next"]');
}
async function getCurrentPageNumber(page) {
const text = await page.innerText('[data-testid="pagination-current"]');
return parseInt(text, 10);
}
If your application does not expose test IDs, you can request the development team to add them; the cost is negligible compared to the maintenance burden of flaky selectors.
Handling Dynamic Page Numbers
When page numbers are rendered as a list of buttons, locate the container and then filter by text.
async function goToPage(page, targetNumber) {
const pageButtons = page.locator('[data-testid="pagination-page"]');
await pageButtons.filter({ hasText: String(targetNumber) }).first().click();
}
Mobile Pagination (Swipe‑Based)
For mobile apps where pagination is implemented via swipe gestures, use Appium’s touch actions.
from appium import webdriver
from appium.webdriver.common.touch_action import TouchAction
def swipe_to_next_page(driver):
size = driver.get_window_size()
start_x = size['width'] * 0.8
end_x = size['width'] * 0.2
y = size['height'] * 0.5
TouchAction(driver).press(x=start_x, y=y).wait(200).move_to(x=end_x, y=y).release().perform()
Always verify that the swipe resulted in a new set of items by checking for a known element that appears only after the swipe.
Handling Waits, Synchronization, and Flakiness
Pagination often involves asynchronous data loading, which is a common source of flaky tests. The goal is to wait for the *observable* outcome rather than arbitrary timeouts.
Implicit vs Explicit Waits
- Implicit waits (Selenium) apply globally and can mask real issues; avoid them.
- Explicit waits (WebDriverWait, Playwright’s
waitForSelector/waitForResponse) are preferred because they target specific conditions.
Playwright’s Auto‑waiting
Playwright automatically waits for elements to be attached, visible, and stable before performing actions. For network‑dependent pagination, combine auto‑wait with explicit response waiting.
async function navigateToNextPage(page) {
// Wait for the network request that fetches the next page
const [response] = await Promise.all([
page.waitForResponse(resp => resp.url().includes('/api/items') && resp.status() === 200),
page.click('[data-testid="pagination-next"]')
]);
const json = await response.json();
// Optionally assert on the payload
expect(json.items.length).toBeGreaterThan(0);
}
Selenium Example with ExpectedConditions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def go_to_next_page(driver):
next_btn = (By.CSS_SELECTOR, '[data-testid="pagination-next"]')
WebDriverWait(driver, 15).until(EC.element_to_be_clickable(next_btn)).click()
# Wait for the new page's first item to appear
first_item = (By.CSS_SELECTOR, '.item')
WebDriverWait(driver, 15).until(EC.presence_of_element_located(first_item))
Dealing with Stale Element References
When the DOM is replaced after a page change, any previously located element becomes stale. Re‑locate after each navigation.
async function getItemsAfterNavigation(page) {
await page.click('[data-testid="pagination-next"]');
// Re‑locate the item list
return await page.locator('.item').all();
}
Network Throttling and Error Simulation
To test error states (e.g., 500 response, empty payload), intercept and modify requests.
await page.route('**/api/items', async route => {
const resp = await route.fetch();
if (resp.status() === 200) {
// Force an empty page on the second request
const json = await resp.json();
json.items = [];
await route.fulfill({ json });
} else {
await route.continue_();
}
});
By controlling the network behaviorally verifying loading spinners, error messages, and UI state, you eliminate false positives caused by timing variances.
Data Setup and Teardown for Pagination Scenarios
Reliable pagination tests need deterministic data. Depending on your architecture, you can set up data via API calls, database seeds, or UI actions before each test, and clean it up afterward.
API‑Based Setup (Preferred)
If your application exposes an admin or seeding endpoint, use it to create a known number of items.
async function seedItems(count) {
const res = await fetch('https://api.example.com/admin/seed', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ count })
});
if (!res.ok) throw new Error(`Seed failed: ${res.status}`);
}
Call seedItems in a beforeEach hook, then delete after each test.
test.beforeEach(async ({ request }) => {
await request.post('/admin/seed', { data: { count: 150 } });
});
test.afterEach(async ({ request }) => {
await request.post('/admin/clear');
});
Database Seeding (For Backend‑Heavy Tests)
When you have direct DB access, use transaction rollback to isolate tests.
import pytest
import sqlalchemy as sa
@pytest.fixture
def db_session(engine):
connection = engine.connect()
trans = connection.begin()
session = sa.orm.scoped_session(sa.orm.sessionmaker(bind=connection))
yield session
session.remove()
trans.rollback()
connection.close()
In the test, insert the required rows via ORM, then rely on the rollback to cleanse.
UI‑Based Setup (When No API Exists)
If you must create items through the UI, encapsulate the flow in a reusable function and add a short explicit wait for success toast.
async function createItemViaUI(page, itemData) {
await page.click('[data-testid="add-item-button"]');
await page.fill('[data-testid="item-name"]', itemData.name);
await page.fill('[data-testid="item-price"]', String(itemData.price));
await page.click('[data-testid="submit-item"]');
await page.waitForSelector('[data-testid="toast-success"]');
}
Remember to delete items after the test, either via a “delete all” button or an API call, to keep the environment clean.
Writing Maintainable Test Code
Maintainability hinges on separation of concerns, reusable abstractions, and clear naming. Adopt the Page Object Model (POM) or a similar pattern, and keep test steps declarative.
Page Object for Pagination (Playwright)
// pagination.po.ts
export class PaginationPage {
constructor(private page) {}
async goto() {
await this.page.goto('/products');
}
async setPageSize(size) {
await this.page.selectOption('[data-testid="page-size-select"]', String(size));
}
async applyFilter(filter) {
await this.page.selectOption('[data-testid="filter-select"]', filter);
}
async sortBy(option) {
await this.page.selectOption('[data-testid="sort-select"]', option);
}
async goToPage(number) {
await this.page.locator('[data-testid="pagination-page"]')
.filter({ hasText: String(number) })
.first()
.click();
}
async next() {
await this.page.click('[data-testid="pagination-next"]');
}
async prev() {
await this.page.click('[data-testid="pagination-prev"]');
}
async getItemCount() {
return await this.page.locator('.item').count();
}
async isNextDisabled() {
return await this.page.isDisabled('[data-testid="pagination-next"]');
}
async isPrevDisabled() {
return await this.page.isDisabled('[data-testid="pagination-prev"]');
}
}
Test Using the POM
import { test, expect } from '@playwright/test';
import { PaginationPage } from './pagination.po';
test.describe('Pagination behavior', () => {
let pagination: PaginationPage;
test.beforeEach(async ({ page }) => {
pagination = new PaginationPage(page);
await pagination.goto();
await pagination.setPageSize('25');
});
test('navigates to last page correctly', async ({ page }) => {
const total = await page.evaluate(() => window.__TOTAL_ITEMS__); // injected via test setup
const lastPage = Math.ceil(total / 25);
await pagination.goToPage(lastPage);
expect(await pagination.getItemCount()).toBe(total % 25 || 25);
expect(await pagination.isNextDisabled()).toBeTruthy();
});
test('previous button disabled on first page', async ({ page }) => {
expect(await pagination.isPrevDisabled()).toBeTruthy();
await pagination.next();
expect(await pagination.isPrevDisabled()).toBeFalsy();
});
});
Key takeaways:
- Single source of truth: locators live only in the POM.
- Readable tests: each line expresses intent, not mechanics.
- Easy updates: if the UI changes the
data-testidvalue, edit only the POM.
Utility Functions for Common Actions
Extract repetitive logic (login, API seeding, waiting for toast) into helper modules. Keep them pure and side‑effect‑free where possible, returning promises or values that tests can assert on.
Running Pagination Tests in CI/CD
Integrating pagination tests into your pipeline ensures that regressions are caught before they reach production. The steps below apply to most CI systems (GitHub Actions, GitLab CI, Azure Pipelines, Jenkins).
1. Containerize the Test Environment
Use a Docker image that contains Node.js (for Playwright) or Java + Selenium dependencies. Example Dockerfile for Playwright:
FROM mcr.microsoft.com/playwright:v1.45.0-focal
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx playwright install-deps
CMD ["npx", "playwright", "test"]
2. Define the CI Job
# .github/workflows/pagination.yml
name: Pagination Tests
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Install Playwright browsers
run: npx playwright install-deps
- run: npx playwright test --reporter=html
- name: Upload test report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
3. Parallel Execution
Speed up matrix testing by splitting the test file across workers.
npx playwright test --workers=4
If you use a test matrix (see earlier), you can also parametrize the job:
strategy:
matrix:
pageSize: [10, 25, 50]
totalItems: [0, 101, 1000]
Each combination runs in its own container, providing fine‑grained failure isolation.
4. Flake Detection
Enable retry on flaky tests but log the attempts.
npx playwright test --retries 2
In the CI step, capture the retry count and fail the job if any test required more than one retry, indicating instability.
5. Reporting
- HTML report (Playwright) gives a visual trace with screenshots and DOM snapshots.
- JUnit XML (most frameworks) integrates with CI dashboards and trend analysis.
- Custom metrics: record average page‑load time per pagination action and publish as a gauge to Grafana or Datadog.
Example of publishing a JUnit report:
npx playwright test --reporter=junit --output=test-results.xml
Then configure your CI to parse test-results.xml.
Reporting and Metrics
Beyond pass/fail, effective pagination testing surfaces insights about performance, coverage, and user experience.
Test Result Dashboard
Create a simple dashboard that shows:
| Metric | Description | Target |
|---|---|---|
| Test pass rate | % of pagination matrix passes per build | ≥ 98% |
| Flaky tests | Number of tests requiring retries | 0 |
| Average navigation latency | Mean time from click to new items visible | < 800 ms |
| Empty‑page handling | % of tests confirming correct empty state UI | 100% |
| Accessibility violations | WCAG A/AA issues on pager controls | 0 |
Use your CI’s built‑in analytics or a third‑party tool (e.g., Codecov, Allure) to chart these over time.
Allure Integration (Example)
npm i -D allure-playwright
npx playwright test --reporter=allure-playwright
allure generate allure-results --clean -o allure-report
allure open
Allure captures attachments (screenshots, videos) and provides a trend view of test duration.
Performance Budgets
If your pager relies on lazy‑loaded images or infinite scroll, assert that the network payload stays within a budget.
await page.route('**/items*', route => {
const request = request();
// abort if response size > 200KB
route.continue_({ headers: { ...request.headers(), 'x-size-limit': '200000' } });
});
Then in the test, verify that no request exceeded the limit by checking the response headers or using page.waitForResponse and inspecting response.headers()['content-length'].
Leveraging Autonomous Exploration to Bootstrap Pagination Tests
Writing pagination tests from scratch can be time‑consuming, especially when the UI is complex or frequently changing. An autonomous QA platform such as SUSA can accelerate the initial test creation by exploring the application, discovering pagination patterns, and generating starter scripts.
How It Works
- Upload the APK or provide the web URL to SUSA.
- Select the pagination focus area (or let the AI infer it from UI patterns like “Next”, “Previous”, page numbers).
- SUSA’s agents navigate the app using a variety of personas (curious, power‑user, accessibility‑focused). They automatically:
- Detect pagination controls via heuristics and accessibility attributes.
- Vary page size, sort, and filter combinations.
- Capture network requests and responses to understand data shape.
- Record interactions as a trace.
- After exploration, SUSA exports Appium (Android) or Playwright (Web) test skeletons that include:
- Locator suggestions (data‑testid, aria-label, role).
- Placeholder waits and retries.
- Basic assertions (item count, disabled state).
- You can then refine the generated tests, add data‑setup hooks, and integrate them into your CI pipeline.
Benefits
- Reduced bootstrapping time: What might take a day of manual script creation can be done in under an hour.
- Coverage of edge cases: The autonomous agents try unusual sequences (rapid next/prev clicks, network throttling) that a human might overlook.
- Living documentation: Each run updates the explored map, so if the UI changes, the platform highlights new or missing elements, prompting test updates.
Limitations to Consider
- The generated code is a starting point; you must replace placeholder data with real seeding logic and adapt assertions to your business rules.
- For highly customized pagination (e.g., virtualized lists with custom scroll agents), you may need to augment the locators or add custom swipe logic.
- Security and data‑privacy reviews still apply; ensure the platform does not expose sensitive endpoints during exploration.
If you adopt SUSA, treat its output as a draft that you review, refactor into your POM, and augment with the data‑setup and reporting practices described earlier.
Checklist for Reliable Pagination Automation
Use this list before marking a pagination test suite as “ready for CI”.
- [ ] Locator stability: All pagination controls located via
data-testid,aria-label, or stablerole/name. - [ ] Wait strategy: No
sleepor arbitrary timeouts; use explicit waits for network responses or DOM changes. - [ ] Data determinism: Seed known item counts via API or DB; clean up after each test.
- [ ] Matrix coverage: At least one test for each combination of page size, empty/partial/full last page, and disabled navigation states.
- [ ] Error handling: Verify UI shows loading spinner, error message, and retry behavior on failed requests.
- [ ] Accessibility: Pagination controls are reachable via keyboard and have appropriate labels.
- [ ] Performance: Assert that page transitions complete within your defined latency budget.
- [ ] Flake mitigation: No more than one retry allowed in CI; investigate any test that needs a retry.
- [ ] CI integration: Tests run on every PR, with parallelism and artifact collection (HTML/JUnit reports).
- [ ] Maintainability: Page Object Model used; helpers for login, seeding, and toast verification are centralized.
- [ ] Reporting: Pass/fail, latency, and accessibility metrics are published to a dashboard or trend system.
Final Takeaways
Automating pagination testing transforms a fragile, manual check into a repeatable safety net that guards against regressions in data navigation, UI responsiveness, and accessibility. Start by deciding whether automation delivers ROI based on your product’s complexity and release cadence. Choose a framework that matches your stack—Playwright for modern web apps with built‑in waiting, Selenium for legacy Java environments, or Appium for mobile lists. Design a test matrix that captures the variables influencing pagination (page size, total items, sort, filter, role, network, device) and implement it using data‑driven techniques.
Invest in solid locators (data-testid or aria-label) and rely on explicit waits tied to network responses or DOM changes, never on static sleeps. Keep your test code maintainable with a Page Object Model, centralized helper functions, and clear, intention‑revealing names. Integrate the suite into your CI pipeline with containerized environments, parallel execution, flake‑detection retries, and comprehensive reporting (HTML, JUnit, Allure). Finally, consider using an autonomous QA platform like SUSA to jump‑start test creation, but always treat its output as a draft that you refine with your own locators, data setup, and assertions.
By following this guide, you’ll have a robust, maintainable pagination automation suite that catches defects early, provides fast feedback to developers, and ensures a smooth browsing experience for every user—no matter how large the data set or how varied the interaction patterns. 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