How to Test Wishlists on Web (Complete Guide)
Wishlists sit at the intersection of product discovery and conversion. When a user adds an item to a wishlist they signal intent, and the feature often feeds downstream processes such as saved‑for‑lat
Why Wishlist Testing Matters
Wishlists sit at the intersection of product discovery and conversion. When a user adds an item to a wishlist they signal intent, and the feature often feeds downstream processes such as saved‑for‑later emails, price‑drop alerts, or social sharing. A broken wishlist can therefore erode trust, abandon carts, and skew analytics that drive inventory and marketing decisions.
In production, wishlist failures tend to cluster around a few patterns: silent failures where the UI shows success but no request is sent, race conditions that lose items under concurrent adds, and state‑desync bugs where the wishlist count displayed in the header diverges from the persisted list. These issues are hard to catch with simple happy‑path checks because they depend on timing, network throttling, or specific user interactions (e.g., adding the same item twice from different tabs).
Testing wishlists thoroughly therefore protects revenue, preserves data integrity, and ensures that the feature behaves predictably across the varied ways real users interact with it.
Wishlist Feature Anatomy
Understanding the moving parts helps you design tests that hit the right seams.
Core components
- UI layer – buttons, icons, modals, toast notifications, and the wishlist page itself.
- State management – client‑side store (Redux, MobX, Vuex, or React context) that holds the list of item IDs.
- API contract – endpoints such as
POST /wishlist/add,DELETE /wishlist/remove,GET /wishlist, and possiblyPATCH /wishlist/notify. - Persistence layer – server‑side database or cache that stores the wishlist per user/session.
- Side‑effects – analytics events, email triggers, or webhook calls that fire on add/remove.
Typical user flows
- Add from product card – user clicks the heart icon, sees a confirmation toast, and the badge count increments.
- Add from product detail page – similar action but may involve a modal with options (e.g., add to a named list).
- Remove from wishlist page – swipe‑left, checkbox bulk‑select, or individual delete button.
- Move to cart – “Add to cart” button on the wishlist item that transfers the item and optionally removes it from the wishlist.
- Share list – copy link or social share that encodes the current list state.
- Sync across devices – user logs in on a second browser and expects the same wishlist to appear.
Each flow touches a different combination of UI, state, API, and persistence, making a matrix approach essential.
Test Matrix for Wishlists
Below is a comprehensive matrix that groups scenarios by intent, outlines preconditions, steps, and expected outcomes. Use it as a checklist when writing manual test cases or parametrizing automated suites.
| Category | ID | Precondition | Steps | Expected Outcome |
|---|---|---|---|---|
| Happy Path | HP1 | User logged in, product page loaded | Click wishlist icon → verify toast → check badge count +1 | Item appears in wishlist DB, UI updates instantly |
| Happy Path | HP2 | Guest user (no account) | Add item → prompted to log in or create account → after login, item persists | Item saved under newly created account |
| Happy Path | HP3 | User on wishlist page | Select multiple items → click “Move to cart” → cart updated, wishlist count decrements | Items removed from wishlist, added to cart |
| Error Path | EP1 | Network throttled to 500ms latency, 5% loss | Add item → wait for toast | Toast shows error after timeout, UI reverts badge, no DB write |
| Error Path | EP2 | Item already in wishlist | Click wishlist icon again | Duplicate prevented; UI may show “already saved” toast, no extra DB row |
| Error Path | EP3 | Invalid item ID (e.g., removed from catalog) | Attempt to add via direct API call | API returns 404, UI shows generic error, wishlist unchanged |
| Edge Case | EC1 | User opens same product in two tabs, adds from both within 200ms | Observe final state | Only one entry persists; no race‑condition duplicate |
| Edge Case | EC2 | User adds item, then immediately logs out before network resolves | Login again on same session | Item persists if saved to server; otherwise cleared if only client‑side |
| Edge Case | EC3 | Wishlist exceeds defined limit (e.g., 200 items) | Keep adding until limit reached | Further adds blocked with appropriate UI message |
| Accessibility | A1 | Screen reader enabled | Navigate to wishlist button via Tab → activate | Button announces “Add to wishlist, button”, state changes announced |
| Accessibility | A2 | High contrast mode | Verify icons and toast have sufficient contrast ratio (≥4.5:1) | All visual elements meet WCAG AA |
| Accessibility | A3 | Keyboard only | Use Enter/Space to trigger wishlist actions | Same outcome as mouse click |
| Security/Privacy | SP1 | Malicious user attempts to add item with XSS payload in custom note field (if supported) | Submit note containing | Payload sanitized, not executed, stored as plain text |
| Security/Privacy | SP2 | Unauthenticated endpoint call | Send POST to /wishlist/add without cookie/token | Server responds 401/403, no state change |
| Security/Privacy | SP3 | Data leakage via URL sharing | Share wishlist link that includes raw item IDs in query string | Link does not expose sensitive user data; only non‑PII identifiers |
The table above can be copied into a test‑management tool and expanded with additional rows for locale‑specific behavior, payment‑method restrictions, or A/B test flags.
Manual Testing Approach
Even with strong automation, a hands‑on pass catches nuances that scripts may overlook, especially around timing, visual feedback, and cross‑browser quirks.
Setup and environment
- Browser matrix – Chrome, Firefox, Safari, Edge (latest two versions).
- Device emulation – toggle touch vs. mouse, varying viewport widths (320px, 768px, 1440px).
- Network conditions – use Chrome DevTools throttling (Slow 3G, offline) or tools like
tc/netemto simulate latency and packet loss. - Data seeding – pre‑populate a test catalog with known item IDs, prices, and stock statuses.
- Logging – enable request/response capture (e.g., via
mitmproxyor browser HAR export) to verify API calls.
Step‑by‑step test execution
- Login / session establishment – ensure you start from a clean state (clear cookies, localStorage, IndexedDB).
- Navigate to a product – verify that the wishlist icon is present and correctly styled.
- Add item – click the icon, watch for toast, confirm badge increment, and inspect network tab for the
POST /wishlist/addcall (payload should containitemIdand optionallyuserId). - Validate persistence – refresh the page, revisit the wishlist page, and confirm the item appears.
- Remove item – use the delete action, verify toast, badge decrement, and
DELETE /wishlist/removerequest. - Cross‑tab test – open the same product in a second tab, add from both tabs within a short window, then check that only one entry exists.
- Limit test – keep adding items until the UI blocks further adds; attempt a direct API call beyond the limit to ensure server‑side enforcement.
- Accessibility audit – run axe‑core manually or via browser extension, note any violations on the wishlist button, toast, and list items.
- Security probe – using the browser console, attempt to inject a script into any free‑text field (e.g., note) and observe whether it gets escaped.
- Logout / login again – after adding items, log out, then log back in with a different account; confirm that the wishlist is isolated per account.
During each step, annotate observations: timing of UI updates, any flicker, whether error states are recoverable, and if the page remains usable when JavaScript is disabled (progressive enhancement fallback).
Exploratory tips
- Interrupt flows – navigate away mid‑request, then return to see if the wishlist recovers.
- Alter system clock – test expiration‑based features (e.g., price‑drop alerts) by shifting the clock forward.
- Simulate race conditions – use two browser windows to fire add/remove requests almost simultaneously via the console (
fetchcalls). - Check offline behavior – go offline, add items, then go online and verify sync.
These manual checks often surface issues like stale optimistic UI updates, missing rollback on failure, or ambiguous error messages that only appear under specific timing windows.
Automated Testing Strategies
Automation provides repeatability and scalability. The goal is to cover the matrix above with a combination of unit, component, and end‑to‑end (E2E) tests, augmented by visual regression and performance checks.
Unit and component tests
Isolate the wishlist reducer, action creators, and UI components.
// wishlistReducer.test.js
import { addItem, removeItem } from './wishlistActions';
import wishlistReducer from './wishlistReducer';
test('addItem adds unique ID', () => {
const state = wishlistReducer(undefined, { type: '@@init' });
const newState = wishlistReducer(state, addItem({ itemId: 42 }));
expect(newState.items).toContainEqual({ itemId: 42, addedAt: expect.any(Number) });
});
test('addItem prevents duplicate', () => {
const state = { items: [{ itemId: 42 }] };
const newState = wishlistReducer(state, addItem({ itemId: 42 }));
expect(newState.items).toHaveLength(1);
});
Run these with Jest or Vitest on every commit; they guard against regressions in state logic.
End‑to‑end tests with Playwright
Playwright offers cross‑browser control, network mocking, and tracing. Below is a parameterized test that walks through the happy path, error path, and limit scenario.
// wishlist.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Wishlist flows', () => {
test.use({ viewport: { width: 1280, height: 800 } });
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.fill('#email', 'tester@example.com');
await page.fill('#password', 'SecurePass!123');
await page.click('button[type="submit"]');
await page.waitForURL('/products');
});
test('happy path add and remove', async ({ page }) => {
await page.goto('/product/42');
await page.click('[data-testid="wishlist-button"]');
await expect(page.locator('[data-testid="toast"]')).toContainText('Saved');
await expect(page.locator('[data-testid="wishlist-badge"]')).toHaveText('1');
await page.goto('/wishlist');
await expect(page.locator(`[data-item-id="42"]`)).toBeVisible();
await page.locator(`[data-item-id="42"] [data-testid="remove-button"]`).click();
await expect(page.locator('[data-testid="wishlist-badge"]')).toHaveText('0');
});
test('network failure shows error and reverts UI', async ({ page }) => {
await page.route('**/wishlist/add', async route => {
// Simulate a 500 error after a delay
await new Promise(r => setTimeout(r, 500));
await route.fulfill({ status: 500, body: JSON.stringify({ error: 'server' }) });
});
await page.goto('/product/13');
await page.click('[data-testid="wishlist-button"]');
const toast = page.locator('[data-testid="toast-error"]');
await expect(toast).toContainText('Could not save');
// Badge should not have changed
await expect(page.locator('[data-testid="wishlist-badge"]')).toHaveText('0');
});
test('limit enforcement', async ({ page }) => {
const limit = 200;
for (let i = 1; i <= limit; i++) {
await page.goto(`/product/${i}`);
await page.click('[data-testid="wishlist-button"]');
}
// Attempt one more – should be blocked
await page.goto(`/product/${limit + 1}`);
await page.click('[data-testid="wishlist-button"]');
const toast = page.locator('[data-testid="toast-info"]');
await expect(toast).toContainText('Wishlist full');
// Verify badge still shows limit
await expect(page.locator('[data-testid="wishlist-badge"]')).toHaveText(limit.toString());
});
});
Why Playwright?
- Auto‑waits reduce flakiness.
- Built‑in tracing (
test.use({ trace: 'retain-on-failure' })) lets you inspect DOM and network after a failure. - Easy to spin up multiple contexts to simulate concurrent tabs.
Visual regression and performance
Use a tool like Percy or Chromatic to capture screenshots of the wishlist page under different states (empty, single item, maxed out). Pair with Lighthouse CI to ensure that adding/removing items does not cause layout shifts or long tasks.
# Run Lighthouse CI as part of CI pipeline
lhci autorun --collect.url=https://staging.example.com/wishlist
Data‑driven and parameterized tests
Leverage test fixtures to run the same flow against multiple locales, currencies, or feature flags.
// wishlistData.test.js
const testData = [
{ locale: 'en-US', currency: 'USD', expectedBadge: '1' },
{ locale: 'fr-FR', currency: 'EUR', expectedBadge: '1' },
{ locale: 'ja-JP', currency: 'JPY', expectedBadge: '1' },
];
testData.forEach(({ locale, currency, expectedBadge }) => {
test(`add item in ${locale}`, async ({ page }) => {
await page.setLocale(locale);
await page.goto('/product/10');
await page.click('[data-testid="wishlist-button"]');
await expect(page.locator('[data-testid="wishlist-badge"]')).toHaveText(expectedBadge);
});
});
This approach surfaces bugs that only appear when number formatting, right‑to‑left layouts, or specific promo banners are present.
Tooling and Infrastructure
A robust test ecosystem needs the right runners, mocking strategies, and observability hooks.
Test runners and CI integration
| Tool | Primary Use | Strengths | Typical Config |
|---|---|---|---|
| Jest/Vitest | Unit & component | Fast, built‑in mocking, snapshot testing | jest.config.js with testEnvironment: jsdom |
| Playwright | E2E cross‑browser | Auto‑wait, tracing, multiple contexts | playwright.config.js with projects: [{ name: 'chromium' }, { name: 'firefox' }, { name: 'webkit' }] |
| Cypress | E2E (Chrome‑centric) | Rich DSL, time‑travel debugging | cypress.config.js with baseUrl |
| Percy/Chromatic | Visual regression | Baseline management, CI‑gate | Add @percy/playwright or @storybook/addon-storyshots |
| Lighthouse CI | Performance & accessibility | Scores, audits, thresholds | lhci.conf.js with collect: { url: [...] } |
| Mitmproxy | Network interception & mocking | Scriptable request/response manipulation | Launch with --mode upstream:http://localhost:3000 |
In a typical CI pipeline (GitHub Actions, GitLab CI, or Jenkins), you would:
- Install dependencies (
npm ci). - Run unit tests (
npm test). - Start the app in a test environment (
npm run start:test). - Execute Playwright suite (
npx playwright test). - Upload traces and videos as artifacts.
- Run Percy snapshot check (
npx percy exec -- playwright test). - Publish Lighthouse results (
lhci autorun).
Mocking APIs and service workers
Instead of hitting a real backend, you can intercept requests with Playwright’s route handling or with MSW (Mock Service Worker) for unit tests.
// msw handlers for wishlist
import { rest } from 'msw';
export const handlers = [
rest.post('/wishlist/add', (req, res, ctx) => {
const { itemId } = req.body;
return res(ctx.status(200), ctx.json({ success: true, itemId }));
}),
rest.delete('/wishlist/remove', (req, res, ctx) => {
const { itemId } = req.body;
return res(ctx.status(200), ctx.json({ success: true, itemId }));
}),
];
In Playwright:
await page.route('**/wishlist/**', async route => {
const url = new URL(route.request().url());
if (url.pathname.endsWith('/add')) {
await route.fulfill({ status: 200, body: JSON.stringify({ success: true }) });
} else {
await route.continue();
}
});
Mocking lets you simulate latency, error codes, and payload variations without standing up a full API stack.
Logging and observability
Capture console errors, network failures, and custom events during test runs. Playwright’s page.on('console', msg => ...) and page.on('pageerror', err => ...) feed into your CI logging system. For production monitoring, instrument the wishlist with custom events (e.g., wishlist_added, wishlist_failed) and correlate them with feature flags in your analytics platform.
Autonomous, Persona‑Driven Exploration with SUSA
Even the most exhaustive matrix can miss emergent behavior that only appears when real users—each with distinct habits—interact with the feature. Autonomous testing platforms like SUSA address this gap by exploring the application without pre‑written scripts, guided by configurable user personas.
How it works
- Ingestion – You provide SUSA with the URL of your staging or production site (or an APK for mobile hybrids).
- Persona modeling – Each persona (e.g., “impatient”, “elderly”, “accessibility‑focused”, “adversarial”) defines a probability distribution over actions: tap speed, scroll depth, likelihood to fill optional fields, tolerance for error messages, and use of assistive technology.
- Exploration loop – SUSA starts from the entry point, performs actions according to the selected persona, observes DOM changes, network traffic, and accessibility events, then decides the next step based on a reinforcement‑learning style reward signal (e.g., discovering new URLs, triggering errors, or hitting accessibility violations).
- Learning – Visited states and dead ends are stored; subsequent runs avoid repeating fruitless paths and focus on under‑explored areas, gradually expanding coverage.
- Reporting – After a session, SUSA outputs a structured log: discovered flows, observed crashes/ANRs (if applicable), WCAG violations, security hints (e.g., reflected XSS in URL parameters), and UX friction points (e.g., buttons that require >2 seconds to respond).
Because the exploration is not bound to a predetermined script, it can stumble onto edge cases that a developer might never think to encode—such as a user who repeatedly opens the wishlist modal, cancels, then reopens it while a network request is pending, or a power user who drags items from the wishlist to the cart via a hidden drag‑and‑drop handler that isn’t exposed in the UI spec.
What it finds that scripts miss
| Issue Class | Typical Script Coverage | What SUSA Observed in a Real Run |
|---|---|---|
| Race‑condition duplicate adds | Scripts add once, wait for response, then assert. | Impatient persona double‑tapped the wishlist button 150 ms apart; the UI showed a single toast but the backend received two POSTs, creating a duplicate entry that only appeared after a page refresh. |
| Accessibility focus trap | Scripts check for ARIA labels but not focus order. | Elderly persona used only Tab navigation; after opening the wishlist modal, focus remained trapped inside the modal despite the close button being reachable, violating WCAG 2.1.1. |
| Hidden API endpoint leakage | Scripts only hit documented endpoints. | Adversarial persona fuzzed query parameters and discovered a /wishlist/export endpoint that returned the entire wishlist in JSON without authentication, exposing PII. |
| Performance degradation under bulk actions | Scripts test single add/remove. | Power‑user persona selected 50 items via checkbox bulk‑select and hit “Move to cart”; the main thread blocked for 3.2 seconds, causing a noticeable UI freeze that would affect low‑end devices. |
| Locale‑specific layout overflow | Scripts run in en‑US only. | Novice persona switched language to Arabic; the wishlist badge overlapped with the cart icon due to missing flex‑wrap handling, a visual bug only visible in RTL layouts. |
These findings illustrate how autonomous exploration surfaces defects that arise from the *interaction* of timing, user behavior, and implementation details—areas where static test matrices often rely on educated guesses rather than empirical observation.
Integrating SUSA into your workflow
- Pre‑release – Run a short SUSA session (e.g., 10 minutes per persona) on your staging build as part of the nightly pipeline. Treat any newly discovered critical or high‑severity finding as a blocker.
- Post‑release – Schedule a weekly exploratory run against production to catch regressions that emerge from feature flags or A/B tests that were not present in staging.
- Feedback loop – Export the list of discovered URLs and error traces into your test‑case management tool; convert reproducible flows into Playwright tests to prevent regression.
While SUSA complements—not replaces—your manual and automated suites, its persona‑driven approach continuously expands the horizon of what you consider “tested”.
Checklist for Release
Before you tag a release as ready for production, run through this concise list. Each item maps back to the matrix or a specific testing technique.
| ✅ Item | How to Verify |
|---|---|
| Happy‑path add/remove works in all supported browsers | Run Playwright suite with projects: [chromium, firefox, webkit]. |
| Network error handling shows user‑friendly toast and does not corrupt state | Use page.route to return 500/404; assert badge unchanged and error toast appears. |
| Duplicate add attempts are idempotent | Send two rapid add requests (via console or API) and verify only one DB row. |
| Wishlist limit enforced client‑ and server‑side | Attempt to add beyond limit via UI and via direct API call; both should be blocked with consistent messaging. |
| Accessibility compliance (WCAG AA) for wishlist button, toast, and list | Run axe-core on the wishlist page; zero violations of severity ≥ moderate. |
| No reflected XSS in wishlist‑related inputs | Inject into any text field (e.g., note, name) and confirm it is escaped in the response. |
| State persists across logout/login and across tabs | Add item, log out, log back in (same or different account), verify item appears only for the original account. |
| Performance stays under threshold for bulk actions | Use Lighthouse CI or Playwright’s metrics to ensure main‑thread work < 50 ms for adding 50 items. |
| No undocumented endpoints expose wishlist data | Run a passive scanner (e.g., OWASP ZAP) against the wishlist URLs; flag any 200 responses that return JSON without auth. |
| Persona‑driven exploratory run finds no new critical defects | Execute a 15‑minute SUSA session with all eight personas; review the report for severity ≥ high. |
If any item fails, create a ticket, reproduce the issue with a failing test (manual or automated), and fix before merging.
Closing Takeaways
Wishlists may appear simple—a button, a toast, a list—but they sit at a fragile crossroads of UI optimism, network reliability, state persistence, and user psychology. A solid testing strategy therefore needs three layers:
- Specification‑driven checks that validate the happy path and defined error cases (unit, component, and scripted E2E).
- Scenario‑based exploration that exercises timing, concurrency, and edge‑condition combinations not captured in pre‑written scripts (manual exploratory sessions, data‑driven parametrized tests, and tools like Playwright’s tracing).
- Persona‑centric autonomy that mimics the real‑world diversity of how people actually interact with the feature, uncovering hidden race conditions, accessibility traps, and leaked endpoints that would otherwise slip into production.
By coupling a thorough test matrix (as shown above) with both scripted automation and an exploratory, persona‑driven platform such as SUSA, you gain confidence that the wishlist will behave correctly for the curious first‑timer, the impatient power‑user, the elderly novice, and even the adversarial tester probing for weaknesses.
Invest the effort now—write the matrix, automate the core flows, schedule regular autonomous runs—and you’ll see fewer wishlist‑related incidents, cleaner analytics, and a smoother path from product discovery to purchase. 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