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

May 18, 2026 · 15 min read · How-To Guides

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

Typical user flows

  1. Add from product card – user clicks the heart icon, sees a confirmation toast, and the badge count increments.
  2. Add from product detail page – similar action but may involve a modal with options (e.g., add to a named list).
  3. Remove from wishlist page – swipe‑left, checkbox bulk‑select, or individual delete button.
  4. Move to cart – “Add to cart” button on the wishlist item that transfers the item and optionally removes it from the wishlist.
  5. Share list – copy link or social share that encodes the current list state.
  6. 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.

CategoryIDPreconditionStepsExpected Outcome
Happy PathHP1User logged in, product page loadedClick wishlist icon → verify toast → check badge count +1Item appears in wishlist DB, UI updates instantly
Happy PathHP2Guest user (no account)Add item → prompted to log in or create account → after login, item persistsItem saved under newly created account
Happy PathHP3User on wishlist pageSelect multiple items → click “Move to cart” → cart updated, wishlist count decrementsItems removed from wishlist, added to cart
Error PathEP1Network throttled to 500ms latency, 5% lossAdd item → wait for toastToast shows error after timeout, UI reverts badge, no DB write
Error PathEP2Item already in wishlistClick wishlist icon againDuplicate prevented; UI may show “already saved” toast, no extra DB row
Error PathEP3Invalid item ID (e.g., removed from catalog)Attempt to add via direct API callAPI returns 404, UI shows generic error, wishlist unchanged
Edge CaseEC1User opens same product in two tabs, adds from both within 200msObserve final stateOnly one entry persists; no race‑condition duplicate
Edge CaseEC2User adds item, then immediately logs out before network resolvesLogin again on same sessionItem persists if saved to server; otherwise cleared if only client‑side
Edge CaseEC3Wishlist exceeds defined limit (e.g., 200 items)Keep adding until limit reachedFurther adds blocked with appropriate UI message
AccessibilityA1Screen reader enabledNavigate to wishlist button via Tab → activateButton announces “Add to wishlist, button”, state changes announced
AccessibilityA2High contrast modeVerify icons and toast have sufficient contrast ratio (≥4.5:1)All visual elements meet WCAG AA
AccessibilityA3Keyboard onlyUse Enter/Space to trigger wishlist actionsSame outcome as mouse click
Security/PrivacySP1Malicious 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/PrivacySP2Unauthenticated endpoint callSend POST to /wishlist/add without cookie/tokenServer responds 401/403, no state change
Security/PrivacySP3Data leakage via URL sharingShare wishlist link that includes raw item IDs in query stringLink 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

  1. Browser matrix – Chrome, Firefox, Safari, Edge (latest two versions).
  2. Device emulation – toggle touch vs. mouse, varying viewport widths (320px, 768px, 1440px).
  3. Network conditions – use Chrome DevTools throttling (Slow 3G, offline) or tools like tc/netem to simulate latency and packet loss.
  4. Data seeding – pre‑populate a test catalog with known item IDs, prices, and stock statuses.
  5. Logging – enable request/response capture (e.g., via mitmproxy or browser HAR export) to verify API calls.

Step‑by‑step test execution

  1. Login / session establishment – ensure you start from a clean state (clear cookies, localStorage, IndexedDB).
  2. Navigate to a product – verify that the wishlist icon is present and correctly styled.
  3. Add item – click the icon, watch for toast, confirm badge increment, and inspect network tab for the POST /wishlist/add call (payload should contain itemId and optionally userId).
  4. Validate persistence – refresh the page, revisit the wishlist page, and confirm the item appears.
  5. Remove item – use the delete action, verify toast, badge decrement, and DELETE /wishlist/remove request.
  6. 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.
  7. Limit test – keep adding items until the UI blocks further adds; attempt a direct API call beyond the limit to ensure server‑side enforcement.
  8. Accessibility audit – run axe‑core manually or via browser extension, note any violations on the wishlist button, toast, and list items.
  9. 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.
  10. 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

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?

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

ToolPrimary UseStrengthsTypical Config
Jest/VitestUnit & componentFast, built‑in mocking, snapshot testingjest.config.js with testEnvironment: jsdom
PlaywrightE2E cross‑browserAuto‑wait, tracing, multiple contextsplaywright.config.js with projects: [{ name: 'chromium' }, { name: 'firefox' }, { name: 'webkit' }]
CypressE2E (Chrome‑centric)Rich DSL, time‑travel debuggingcypress.config.js with baseUrl
Percy/ChromaticVisual regressionBaseline management, CI‑gateAdd @percy/playwright or @storybook/addon-storyshots
Lighthouse CIPerformance & accessibilityScores, audits, thresholdslhci.conf.js with collect: { url: [...] }
MitmproxyNetwork interception & mockingScriptable request/response manipulationLaunch with --mode upstream:http://localhost:3000

In a typical CI pipeline (GitHub Actions, GitLab CI, or Jenkins), you would:

  1. Install dependencies (npm ci).
  2. Run unit tests (npm test).
  3. Start the app in a test environment (npm run start:test).
  4. Execute Playwright suite (npx playwright test).
  5. Upload traces and videos as artifacts.
  6. Run Percy snapshot check (npx percy exec -- playwright test).
  7. 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

  1. Ingestion – You provide SUSA with the URL of your staging or production site (or an APK for mobile hybrids).
  2. 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.
  3. 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).
  4. Learning – Visited states and dead ends are stored; subsequent runs avoid repeating fruitless paths and focus on under‑explored areas, gradually expanding coverage.
  5. 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 ClassTypical Script CoverageWhat SUSA Observed in a Real Run
Race‑condition duplicate addsScripts 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 trapScripts 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 leakageScripts 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 actionsScripts 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 overflowScripts 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

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.

✅ ItemHow to Verify
Happy‑path add/remove works in all supported browsersRun Playwright suite with projects: [chromium, firefox, webkit].
Network error handling shows user‑friendly toast and does not corrupt stateUse page.route to return 500/404; assert badge unchanged and error toast appears.
Duplicate add attempts are idempotentSend two rapid add requests (via console or API) and verify only one DB row.
Wishlist limit enforced client‑ and server‑sideAttempt 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 listRun axe-core on the wishlist page; zero violations of severity ≥ moderate.
No reflected XSS in wishlist‑related inputsInject into any text field (e.g., note, name) and confirm it is escaped in the response.
State persists across logout/login and across tabsAdd item, log out, log back in (same or different account), verify item appears only for the original account.
Performance stays under threshold for bulk actionsUse Lighthouse CI or Playwright’s metrics to ensure main‑thread work < 50 ms for adding 50 items.
No undocumented endpoints expose wishlist dataRun 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 defectsExecute 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:

  1. Specification‑driven checks that validate the happy path and defined error cases (unit, component, and scripted E2E).
  2. 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).
  3. 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