How to Automate Pagination Testing (Step-by-Step)

How to Automate Pagination Testing (Step-by-Step)

June 28, 2026 · 15 min read · How-To Guides

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:

FactorManual 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.

FrameworkLanguageStrengths for PaginationWeaknessesTypical Setup Time
PlaywrightTypeScript/JavaScript, Python, .NET, JavaAuto‑waits, built‑in network interception, easy page‑object creation, supports Chromium/Firefox/WebKitNewer ecosystem, fewer community plugins than Selenium10‑15 min (npm init + install)
Selenium WebDriverJava, C#, Python, Ruby, JavaScriptMature, extensive language bindings, Grid for parallel executionRequires explicit waits, more boilerplate, flaky if not tuned20‑30 min (driver binaries + bindings)
CypressJavaScript/TypeScriptFast test runner, time‑travel debugging, automatic waitingLimited cross‑browser support (Chrome‑family only), no native mobile testing12‑18 min (npm install)
TestCafeJavaScript/TypeScriptNo WebDriver needed, runs on any browser that supports HTML5Smaller community, less flexibility for custom drivers10‑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 execution25‑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.

VariableValuesDescription
Page size10, 25, 50, 100Number of items per page (often configurable via UI or API)
Total items0, 7, 23, 101, 1000Edge cases: empty, partial last page, exact multiples, large data set
Sort orderAscending price, Descending rating, DefaultEnsures paging respects sorting
Filter stateNo filter, Category=Electronics, Price<$50Verifies that pagination preserves filters
User roleGuest, Registered, AdminChecks for role‑based visibility of items
Network conditionOnline, 3G throttled, OfflineTests loading spinners, error handling, retry logic
DeviceDesktop Chrome, Mobile Safari, Tablet FirefoxConfirms 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

AttributeExampleReason
data-testiddata-testid="pagination-next"Explicitly added for testing; immune to styling changes
aria-labelaria-label="Go to next page"Leverages accessibility labels, doubles as a11y check
role + namerole="button" name="Next"Works with Playwright’s built‑in locators
class (only if stable).pager__nextUse 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

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:

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

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:

MetricDescriptionTarget
Test pass rate% of pagination matrix passes per build≥ 98%
Flaky testsNumber of tests requiring retries0
Average navigation latencyMean time from click to new items visible< 800 ms
Empty‑page handling% of tests confirming correct empty state UI100%
Accessibility violationsWCAG A/AA issues on pager controls0

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

  1. Upload the APK or provide the web URL to SUSA.
  2. Select the pagination focus area (or let the AI infer it from UI patterns like “Next”, “Previous”, page numbers).
  3. SUSA’s agents navigate the app using a variety of personas (curious, power‑user, accessibility‑focused). They automatically:
  1. After exploration, SUSA exports Appium (Android) or Playwright (Web) test skeletons that include:
  1. You can then refine the generated tests, add data‑setup hooks, and integrate them into your CI pipeline.

Benefits

Limitations to Consider

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”.

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