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
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:
- 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.
- 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.
- 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.
- 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.
- Accessibility gaps – Missing ARIA labels, keyboard traps, or insufficient contrast prevent users relying on assistive tech from interacting with filters.
- 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 Dimension | Happy Path | Error Path | Edge Case | Accessibility | Security/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
- Isolate the feature – Deploy a version where only the filter/sort component is enabled, or use feature flags to disable unrelated UI.
- Clear caches – Disable service workers, clear localStorage/sessionStorage, and set a hard refresh (Ctrl+Shift+R) to avoid stale state.
- Instrument the network – Open DevTools → Network, enable “Preserve log”, and filter XHR/fetch calls to monitor request/response payloads.
- 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
- Verify that selecting a single filter option updates the URL query string (or internal state) and that the result set reflects the expected subset.
- Confirm that toggling a sort column changes the visual order and that the sort indicator (arrow) points correctly.
- Ensure that combining multiple filters (e.g., category = “Electronics” AND price < 100) yields the intersection of the individual results.
- Check that pagination controls adapt correctly when the filtered set size changes (first page shows correct items, total pages update).
#### Error Path Tests
- Enter invalid values (e.g., letters in a numeric field, out‑of‑range dates) and assert that the UI shows an inline validation message and does not submit a malformed request.
- Simulate a 400/500 response from the backend by mocking the API; ensure the UI displays an error toast and retains the filter state so the user can correct input without losing selections.
- Test rapid successive changes (type‑ahead) to confirm debounce logic prevents request flooding and that the final request reflects the last input.
#### Edge Cases
- Empty results – Apply filters that guarantee zero matches; verify that a helpful empty‑state message appears and that sort controls remain disabled or hidden appropriately.
- Universal selection – Select all options in a multi‑select filter; ensure the request does not exceed server limits and that performance stays within acceptable thresholds.
- Locale‑specific formats – Switch the browser language to a locale that uses different date or number formats; confirm the UI translates inputs correctly before sending them to the server.
- Large data sets – Load a page with >10 000 rows, apply a filter that reduces the set to a few rows, and measure layout shift and time to interactive.
- Deep linking – Paste a URL that contains predefined filter/sort parameters; the page should initialize with those values and display the correct data set without additional user action.
#### Accessibility Checks
- Navigate the filter panel using only Tab/Shift+Tab; each control must receive a visible focus indicator.
- Activate checkboxes, radios, and selects via Space/Enter; ensure the associated label is announced by screen readers.
- Validate that sort buttons have
aria-labeloraria-describedbyexplaining the current sort direction and that the state changes when toggled. - Run an automated audit (axe‑core) and manually inspect any violations related to contrast, missing roles, or keyboard traps.
#### Security and Privacy Checks
- Attempt to inject SQL‑like strings (
' OR 1=1--) into text filters; observe whether the request is sanitized or rejected, and confirm that no error messages leak database schema. - For date range filters, try to submit a future date far beyond the allowed range; the backend should clamp or reject the input, not return unintended records.
- Verify that applying a filter does not inadvertently remove pagination limits, causing the server to return the entire table (a potential data‑exposure vector).
- Check that any personal data exposed in the URL (e.g., user‑id inside a query param) is not logged by third‑party analytics tools inadvertently.
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:
- State‑drift bugs where the URL does not update after a filter change when using the keyboard only (missed by scripts that rely on mouse clicks).
- Accessibility traps where a modal dialog opened by a filter button traps focus for screen‑reader users but not for mouse users.
- Security edge cases where a specific combination of special characters in a date filter triggers a 500 error only when sent via a programmed XHR (the “adversarial” persona tries varied payloads).
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:
- Input speed – “impatient” types rapidly, “elderly” types slowly with deliberate pauses.
- Error tolerance – “novice” retries after a failure, “adversarial” deliberately sends malformed data.
- Navigation style – “power user” relies on keyboard shortcuts, “accessibility” user prefers screen‑reader navigation and avoids mouse.
- Goal orientation – “curious” explores every toggle, “task‑focused” goes straight to a known flow (e.g., checkout).
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:
- The widget traps focus when opened via keyboard, preventing escape to the main page (found by the “accessibility” persona).
- Rapidly changing the month dropdown causes the widget to re‑render incorrectly, showing overlapping calendars (found by the “impatient” persona).
- Submitting a date far beyond the supported range triggers a backend validation error that returns a 500 with a stack trace (found by the “adversarial” persona).
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.
| Category | Item | ✅/❌ |
|---|---|---|
| Functional | Single 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 Handling | Invalid 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 Cases | Empty 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 | ||
| Accessibility | All 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/Privacy | Input 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 | ||
| Performance | Filter action does not increase TTI beyond threshold (e.g., 250 ms) | |
| Large data set (>10 k rows) filters within acceptable time (<2 s) | ||
| Regression | Automated 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 | ||
| Observability | Network 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:
- Unit tests for pure transformation logic, guaranteeing algorithmic correctness.
- Integration tests (Cypress/Playwright) that assert UI changes, network calls, and state persistence across interactions.
- Visual and performance checks to catch layout shifts and slowdowns introduced by new filter logic.
- Accessibility audits (axe‑core, manual keyboard navigation) to ensure inclusivity.
- Security and privacy probes (input fuzzing, error‑message inspection) to protect against injection and data leakage.
- 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