How to Test Filters And Sorting on Web (Complete Guide)

Filters and sorting are core interaction points in most web applications. They let users narrow large data sets, find relevant items, and impose a meaningful order on results. When these mechanisms fa

March 12, 2026 · 14 min read · How-To Guides

Introduction

Filters and sorting are core interaction points in most web applications. They let users narrow large data sets, find relevant items, and impose a meaningful order on results. When these mechanisms fail, users experience frustration, abandonment, or incorrect data interpretation, which directly impacts conversion and trust. Testing them thoroughly is therefore not optional; it is a baseline quality gate for any feature that presents lists, tables, or grids.

Why Filters and Sorting Matter

A filter component typically consists of input controls (checkboxes, radios, selects, date pickers, text fields) that modify a query sent to a backend or a client‑side state machine. Sorting usually toggles between ascending/descending order on one or more columns, sometimes with multi‑level logic. Both affect the same downstream pipeline: request generation, data transformation, and rendering. A defect in any part of that pipeline can surface as missing rows, duplicate entries, wrong totals, or UI glitches that are hard to trace back to the source.

From a business perspective, filters and sorting often sit on the critical path for e‑commerce product listings, admin dashboards, analytics views, and content feeds. If a user cannot apply a price range filter or sort by newest first, they may leave the site. In regulated domains, incorrect sorting can lead to compliance violations (e.g., displaying outdated legal notices). Hence, the cost of a bug in these controls is disproportionately high compared to the effort required to test them.

Common Failure Modes in Production

Observations from production incidents reveal recurring patterns:

  1. State drift – The filter UI updates but the underlying query parameters are not refreshed, causing stale results after a navigation or a browser back/forward action.
  2. Incorrect debounce/throttle – Rapid changes (e.g., typing in a search box) trigger too many requests, overwhelming network traffic or race conditions that leave the UI in an inconsistent state.
  3. Server‑side mismatches – The API expects filter values in a specific format (e.g., ISO dates) while the UI sends a locale‑specific string, resulting in 400 errors or empty payloads.
  4. Sorting instability – When two records share the same sort key, the order is non‑deterministic, leading to flaky UI tests and user‑perceived “jumping” items.
  5. Accessibility gaps – Missing ARIA labels, keyboard traps, or insufficient contrast prevent users relying on assistive tech from interacting with filters.
  6. Security oversights – Unvalidated filter inputs are passed directly to SQL or NoSQL queries, opening injection vectors; excessive data exposure occurs when a filter inadvertently disables pagination limits.

Understanding these patterns helps prioritize test cases and choose the right verification techniques.

Test Matrix for Filters and Sorting

Below is a comprehensive matrix that separates test dimensions (what to verify) from test types (how to verify). Each cell indicates a recommended technique; “✓” means the technique is applicable, “–” means it is not.

Test DimensionHappy PathError PathEdge CaseAccessibilitySecurity/Privacy
UI Interaction
Network Request Validation
State Persistence
Sorting Stability
Performance Impact
ARIA / Keyboard Support
Input Sanitization
Data Leakage Check
Regression Script Generation

The matrix shows that most dimensions benefit from multiple techniques. For example, verifying network request validation is useful for happy paths, error paths, and edge cases, while security checks focus on error paths and privacy concerns.

Manual Testing Approach

Manual exploration remains valuable for discovering subtle UX issues that automated scripts may overlook. The following step‑by‑out procedure can be adapted to any web app.

#### Preparing the Environment

  1. Isolate the feature – Deploy a version where only the filter/sort component is enabled, or use feature flags to disable unrelated UI.
  2. Clear caches – Disable service workers, clear localStorage/sessionStorage, and set a hard refresh (Ctrl+Shift+R) to avoid stale state.
  3. Instrument the network – Open DevTools → Network, enable “Preserve log”, and filter XHR/fetch calls to monitor request/response payloads.
  4. Set up accessibility tools – Install axe‑core browser extension or use the built‑in Chrome Accessibility pane to audit ARIA attributes and contrast ratios.

#### Happy Path Tests

#### Error Path Tests

#### Edge Cases

#### Accessibility Checks

#### Security and Privacy Checks

Automated Testing Approaches

Automation provides repeatability and speed, especially for regression suites. The following layers complement each other.

#### Unit Tests for Filter/Sort Logic

If the application isolates filter building and sorting into pure functions (common in Redux, Vuex, or React hooks), unit tests can assert correctness without a browser. Example using Jest:


// filterUtils.js
export const applyFilters = (items, filters) => {
  return items.filter(item => {
    return Object.entries(filters).every(([key, value]) => {
      if (value === null) return true; // not selected
      return String(item[key]).toLowerCase().includes(value.toLowerCase());
    });
  });
};

// filterUtils.test.js
import { applyFilters } from './filterUtils';

test('applies multiple text filters correctly', () => {
  const data = [
    { name: 'Apple Watch', category: 'Electronics', price: 399 },
    { name: 'Leather Bag', category: 'Fashion', price: 120 },
    { name: 'Coffee Mug', category: 'Home', price: 15 }
  ];
  const filters = { category: 'electronics', price: '' };
  const result = applyFilters(data, filters);
  expect(result).toHaveLength(1);
  expect(result[0].name).toBe('Apple Watch');
});

These tests run in milliseconds and guard against regressions in the core algorithm.

#### Integration Tests with Cypress/Playwright

End‑to‑end (E2E) tests drive the actual browser, asserting UI changes, network calls, and state persistence. Below is a Cypress example that checks a price‑range filter and a multi‑column sort.


// cypress/integrations/filter_sort_spec.js
describe('Product listing filter & sort', () => {
  beforeEach(() => {
    cy.visit('/products');
    // intercept the API call to control response
    cy.intercept('GET', '/api/products*', { fixture: 'products.json' }).as('getProducts');
  });

  it('applies price range filter and updates results', () => {
    cy.get('#price-min').type('50');
    cy.get('#price-max').type('200{enter}');
    cy.wait('@getProducts').its('request.query').should('deep.include', {
      min_price: 50,
      max_price: 200
    });
    cy.get('.product-card').should('have.length', 8); // based on fixture
    cy.get('.product-card').first().should('contain', '$75');
  });

  it('toggles sort direction and maintains UI indicator', () => {
    cy.get('#sort-select').select('price_asc');
    cy.wait('@getProducts').its('request.query').should('include', { sort: 'price_asc' });
    cy.get('.product-price').first().then($el => {
      const firstPrice = parseFloat($el.text().replace('$', ''));
      cy.get('.product-price').last().then($last => {
        const lastPrice = parseFloat($last.text().replace('$', ''));
        expect(firstPrice).to.be.lte(lastPrice);
      });
    });
    // toggle descending
    cy.get('#sort-select').select('price_desc');
    cy.wait('@getProducts').its('request.query').should('include', { sort: 'price_desc' });
    cy.get('.product-price').first().then($first => {
      const firstPrice = parseFloat($first.text().replace('$', ''));
      cy.get('.product-price').last().then($last => {
        const lastPrice = parseFloat($last.text().replace('$', ''));
        expect(firstPrice).to.be.gte(lastPrice);
      });
    });
  });
});

Playwright offers a similar API with built‑in auto‑waiting and tracing.

#### Visual Regression for UI

Filter panels often involve dynamic showing/hiding of sections, tooltip placement, or responsive layout shifts. Tools like Percy or Chromatic can capture screenshots before and after a filter change and highlight unintended visual differences.

#### Performance Testing

Use Lighthouse CI or WebPageTest to measure the impact of a filter operation on First Contentful Paint (FCP) and Time to Interactive (TTI). A regression that adds a heavy client‑side computation after each filter change will surface as increased TTI.

#### Using SUSA for Autonomous Exploration

SUSA can be pointed at the staging URL of the product listing page. It will autonomously interact with filter controls using its built‑in personas (e.g., “impatient” user who rapidly changes values, “elderly” user who relies on keyboard navigation, “adversarial” user who attempts malformed inputs). Because SUSA explores without pre‑written scripts, it often discovers:

Each run stores explored screens and dead ends, so subsequent executions become smarter, reducing flakiness and expanding coverage over time.

Tooling and Code Examples

This section consolidates concrete snippets that you can copy into your repository.

1. Jest unit test for a sorting utility


// sortUtils.js
export const stableSort = (items, key, asc = true) => {
  const direction = asc ? 1 : -1;
  return [...items].sort((a, b) => {
    if (a[key] < b[key]) return -1 * direction;
    if (a[key] > b[key]) return 1 * direction;
    // preserve original order for equal keys
    return items.indexOf(a) - items.indexOf(b);
  });
};

// sortUtils.test.js
import { stableSort } from './sortUtils';

test('maintains stable order on duplicate keys', () => {
  const data = [
    { id: 1, score: 10, name: 'alpha' },
    { id: 2, score: 10, name: 'beta' },
    { id: 3, score: 5,  name: 'gamma' }
  ];
  const sorted = stableSort(data, 'score', true);
  expect(sorted.map(i => i.id)).toEqual([3, 1, 2]); // 3 first, then original 1,2 order
});

2. Cypress test for clearing all filters


it('clears all filters and resets UI', () => {
  cy.get('#category-select').select('electronics');
  cy.get('#brand-input').type('sony{enter}');
  cy.get('#apply-filters').click();
  cy.wait('@getProducts');
  // verify filters are reflected in UI
  cy.get('#category-select').should('have.value', 'electronics');
  cy.get('#brand-input').should('have.value', 'sony');

  // click clear button
  cy.get('#clear-filters').click();
  // ensure inputs return to default state
  cy.get('#category-select').should('have.value', '');
  cy.get('#brand-input').should('have.value', '');
  // request should be made without filter params
  cy.wait('@getProducts').its('request.query').should('not.have.property', 'category')
    .and('not.have.property', 'brand');
});

3. Playwright test for keyboard navigation of a filter panel


test('filter panel is operable via keyboard only', async ({ page }) => {
  await page.goto('/search');
  // open filter panel with Tab + Enter
  await page.focus('#filter-toggle');
  await page.press('#filter-toggle', 'Enter');
  await expect(page.locator('#filter-panel')).toBeVisible();

  // navigate to first checkbox
  await page.press('#filter-panel', 'Tab');
  await expect(page.locator('#filter-panel')).toHaveFocus();
  await page.press('#filter-panel', 'Space');
  await expect(page.locator('#filter-panel input[type="checkbox"]')).toBeChecked();

  // close panel with Escape
  await page.press('#filter-panel', 'Escape');
  await expect(page.locator('#filter-panel')).toBeHidden();
});

4. Axe‑core accessibility audit in a Cypress plugin


// cypress/support/commands.js
import { injectAxe, checkA11y } from 'axe-core';

Cypress.Commands.add('checkA11y', (context, options) => {
  injectAxe();
  checkA11y(context, options);
});

// usage in a spec
it('passes accessibility audit on filter panel', () => {
  cy.visit('/products');
  cy.openFilterPanel(); // custom command that opens the panel
  cy.checkA11y('#filter-panel'); // runs axe on the panel subtree
});

5. Lighthouse CI configuration for performance regression detection


// lighthouserc.json
{
  "ci": {
    "collect": {
      "url": ["https://staging.example.com/products"],
      "settings": {
        "preset": "desktop",
        "chromeFlags": "--headless"
      }
    },
    "assert": {
      "preset": "lighthouse:recommended",
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "interactive": ["warn", { "maxNumericValue": 2500 }]
      }
    },
    "upload": {
      "target": "temporary-public-storage"
    }
  }
}

Run with lhci autorun after each build to catch slowdowns introduced by new filter logic.

Autonomous Persona‑Driven Exploration

Scripted tests excel at verifying known scenarios, but they rarely stumble upon combinations that a real user might try out of curiosity, frustration, or necessity. Autonomous agents like SUSA address this gap by simulating distinct user personalities, each with a defined behavior model.

How Personas Work

SUSA defines a set of personas, each encoded as a policy that influences:

When the agent encounters a filter component, it applies the persona’s policy to generate interaction sequences. For example, the “adversarial” persona might try to inject Unicode control characters, SQL keywords, or extremely long strings into a numeric filter, while the “elderly” persona will verify that all controls are reachable via Tab and that focus rings are visible.

What Scripts Miss

Consider a filter that uses a third‑party date‑picker widget. A scripted test may select a date via the widget’s API, but it will not notice:

These issues often remain invisible to unit or integration tests because they rely on specific timing, input modality, or edge‑case data that scripted tests do not generate.

Example Bug Found Only by Persona

During a SUSA run on a staging e‑commerce site, the “power user” persona attempted to sort a product list by a custom attribute (popularity) using a keyboard shortcut (Shift+S). The UI responded by updating the sort indicator but the underlying request continued to send the previous sort key (price). The mismatch caused the list to appear sorted by popularity while the backend actually returned price‑sorted data, leading to a confusing user experience. The bug was traced to a missing event listener on the shortcut handler; a conventional Cypress test that clicked the sort button never exercised the keyboard path, so the defect stayed hidden until the autonomous exploration surfaced it.

Checklist for Filter and Sort Testing

Use this concise list before marking a feature as ready for release.

CategoryItem✅/❌
FunctionalSingle filter changes results correctly
Multiple filters combine via AND logic
Clear all filters resets UI and returns to default state
Sort toggles between asc/desc and updates indicator
Sort is stable on equal keys
Error HandlingInvalid input shows inline validation, no request sent
Backend 400/500 shows user‑friendly message, filter state retained
Rapid input debounces correctly, final request reflects last value
Edge CasesEmpty result set displays helpful message
Selecting all options does not exceed server limits
Locale‑specific formats are translated before sending
Deep link with filter/sort params initializes correctly
AccessibilityAll controls reachable via Tab, visible focus indicator
Labels associated with inputs, announced by screen readers
Sort button conveys current state via aria-label or aria-describedby
No focus traps when opening dialogs from filter panel
Security/PrivacyInput sanitized – no SQL/NoSQL injection vectors
Error messages do not leak stack traces or DB schema
Pagination limits remain enforced after filtering
No sensitive data leaked in URL or headers inadvertently
PerformanceFilter action does not increase TTI beyond threshold (e.g., 250 ms)
Large data set (>10 k rows) filters within acceptable time (<2 s)
RegressionAutomated unit tests cover filter builder and sorter pure functions
E2E suite includes at least one happy‑path, one error‑path, and one accessibility scenario per filter/sort component
Visual regression baseline updated after intentional UI changes
ObservabilityNetwork logs show correct query params for each filter/sort change
Console free of warnings/errors after interacting with filters

Mark each item as ✅ when verified, ❌ when a defect is found, and investigate before proceeding to the next release candidate.

Closing Takeaways

Filters and sorting are deceptively simple UI elements that sit at the intersection of client‑side state, network communication, and server‑side logic. Their failure modes are varied—state drift, invalid inputs, accessibility gaps, security flaws, and performance regressions—all of which can erode user trust and business outcomes.

A disciplined testing strategy combines:

  1. Unit tests for pure transformation logic, guaranteeing algorithmic correctness.
  2. Integration tests (Cypress/Playwright) that assert UI changes, network calls, and state persistence across interactions.
  3. Visual and performance checks to catch layout shifts and slowdowns introduced by new filter logic.
  4. Accessibility audits (axe‑core, manual keyboard navigation) to ensure inclusivity.
  5. Security and privacy probes (input fuzzing, error‑message inspection) to protect against injection and data leakage.
  6. Autonomous, persona‑driven exploration (e.g., SUSA) that discovers issues hidden from scripted tests because they depend on specific input modalities, timing, or atypical user goals.

By applying the matrix, checklist, and tooling outlined above, you can move from ad‑hoc spot checks to a repeatable, confidence‑building verification pipeline. The result is a filter and sorting experience that works reliably for every user, every device, and every edge case—exactly what modern web applications demand.

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