How to Test Favorites on Web (Complete Guide)
Favorites (sometimes called bookmarks, stars, hearts, or saved items) are a core interaction pattern in modern web applications. Users rely on them to quickly return to content they care about, to bui
Why Testing Favorites Matters
Favorites (sometimes called bookmarks, stars, hearts, or saved items) are a core interaction pattern in modern web applications. Users rely on them to quickly return to content they care about, to build personal collections, or to signal intent for later conversion. When the favorite mechanism fails, the impact is immediate and measurable:
- Conversion loss – an e‑commerce site where the “Add to wishlist” button silently fails sees a drop in repeat visits and abandoned carts.
- Trust erosion – users who notice that their saved items disappear after a page reload begin to doubt the reliability of the whole platform‑ Support overhead – each missing favorite generates a ticket, increasing load on customer‑service teams.
From a technical standpoint, favorites touch several layers of the stack: UI event handling, client‑side storage, API contracts, and sometimes backend synchronization. A defect in any layer can surface as a silent data loss, a misleading UI state, or a security exposure. Because the feature is often implemented with a mix of frameworks, libraries, and custom code, regression risk is high. Testing favorites therefore provides a low‑cost, high‑return safety net that protects both user experience and business metrics.
Core Concepts: What Is a Favorite Feature?
Understanding the internal mechanics of favorites helps you design tests that hit the right seams.
Data Model Basics
At its simplest, a favorite is a tuple (userId, itemId, timestamp) stored somewhere. The timestamp can be used for sorting recency or for conflict resolution when syncing across devices. Some applications enrich the record with metadata such as a folder/category, a note, or a priority flag.
UI Patterns
Common visual affordances include:
| Pattern | Typical Icon | Interaction | Typical Use |
|---|---|---|---|
| Heart | ❤️ / ♡ | Tap/click to toggle filled/outlined | Social media, media streaming |
| Star | ★ / ☆ | Tap/click to toggle filled/outlined | Product listings, email flagging |
| Bookmark | 🔖 / 📌 | Tap/click to add/remove | News, documentation sites |
| Add to List | “+” or “Add” button | Opens a modal to select a list | Wishlist, playlist, reading list |
The UI may also show a badge count, a tooltip, or an animation to reinforce state change.
Persistence Mechanisms
| Mechanism | Scope | Lifetime | Sync Ability | Typical Use |
|---|---|---|---|---|
| localStorage | Origin‑specific | Until cleared manually | No (unless custom sync) | Simple SPA favorites |
| sessionStorage | Origin‑specific | Tab session | No | Temporary UI state |
| IndexedDB | Origin‑specific | Persistent, larger storage | No (custom sync) | Complex objects, offline‑first |
| Cookie | Origin‑specific | Expires or session | Sent with each request (can be synced) | Lightweight server‑side sync |
| Backend DB (e.g., PostgreSQL, Mongo) | Server‑side | Persistent | Yes (real‑time) | Multi‑device, collaborative apps |
| Service Worker Cache | Origin‑specific | Until cleared | No (but can be used for offline fallback) | Progressive web apps |
Choosing a persistence method influences test coverage: you must verify that data survives page reloads, browser restarts, and, when applicable, device changes.
Sync and Offline Considerations
If the application supports cross‑device sync, the favorite toggle typically fires an optimistic UI update, then sends a PATCH/POST to an endpoint like /api/favorites. The server responds with the updated state, which the client reconciles. Offline‑first apps may queue the request in IndexedDB and replay it when connectivity returns. Tests must therefore cover:
- Optimistic UI vs. eventual consistency
- Handling of 409 conflicts (duplicate favorite)
- Retry logic and exponential back‑off
- Correct UI rollback on network failure
Test Matrix for Favorites
A structured matrix ensures you hit happy paths, error paths, edge cases, accessibility, and security angles. Below is a consolidated table you can copy into a test‑management tool or a spreadsheet.
| Test ID | Category | Description | Preconditions | Steps | Expected Result | Priority |
|---|---|---|---|---|---|---|
| FV‑001 | Happy Path | Add a favorite via heart icon | User logged in, item displayed | 1. Click heart icon (outlined) 2. Verify icon fills 3. Reload page | Icon remains filled, favorite persisted in storage/backend | P1 |
| FV‑002 | Happy Path | Remove a favorite | User logged in, item already favorited | 1. Click heart icon (filled) 2. Verify icon outlines 3. Reload page | Icon remains outlined, favorite removed | P1 |
| FV‑003 | Happy Path | Bulk select and favorite | List view with checkboxes | 1. Select multiple items 2. Click “Favorite selected” button 3. Verify badge counts update | All selected items show filled heart, backend receives batch create | P2 |
| FV‑004 | Error Path | Network loss during toggle | User online, mock network throttling to offline | 1. Toggle favorite 2. Observe UI state 3. Restore network | UI shows optimistic state, request queued, on restore UI updates to server state | P1 |
| FV‑005 | Error Path | Server returns 500 on toggle | User online, intercept endpoint with 500 | 1. Toggle favorite 2. Verify error handling | UI reverts to previous state, error toast shown, retry option available | P1 |
| FV‑006 | Edge Case | Duplicate favorite submission | User already favorited item, rapid double‑click | 1. Double‑click heart quickly 2. Verify only one create request sent | No duplicate entry in backend, UI stays filled | P2 |
| FV‑007 | Edge Case | Storage quota exceeded | localStorage near limit (e.g., 4.9MB of 5MB) | 1. Add many favorites until quota hit 2. Attempt another add | Add fails gracefully, user notified, no data corruption | P2 |
| FV‑008 | Edge Case | Private/incognito mode | Browser in incognito, no extensions | 1. Favorite an item 2. Close incognito window 3. Reopen incognito | Favorite not persisted (expected for sessionStorage) or persisted per mechanism | P2 |
| FV‑009 | Accessibility | Keyboard navigation | Page loaded, focusable elements | 1. Tab to heart icon 2. Press Enter/Space 3. Verify activation | Icon toggles, ARIA state updates, screen reader announces change | P1 |
| FV‑010 | Accessibility | Screen reader label | Page loaded with screen reader (NVDA/JAWS) | 1. Navigate to favorite button 2. Listen to announcement | Button announces “Add to favorites” or “Remove from favorites” based on state | P1 |
| FV‑011 | Security | CSRF protection | Authenticated user, CSRF token required | 1. Attempt to toggle favorite via forged request missing token | Request rejected (403), UI does not change | P1 |
| FV‑012 | Privacy | GDPR right to be forgotten | User requests data deletion | 1. Trigger delete‑account flow 2. Verify favorites table cleared | No favorite records remain for that user in backend or client storage | P1 |
| FV‑013 | Performance | Large list rendering | User has 10 000 favorites | 1. Open favorites page 2. Measure time to first paint | Page loads within 2 s, virtual scrolling used if needed | P2 |
| FV‑014 | Localization | RTL language | UI set to Arabic (rtl) | 1. Observe favorite icon placement | Icon mirrors correctly, padding respects direction | P2 |
| FV‑015 | Theme | Dark mode toggle | System prefers dark, site supports theme switch | 1. Toggle to dark 2. Verify icon contrast meets WCAG AA | Icons retain sufficient contrast, no color‑blind confusion | P2 |
*Priorities*: P1 = blocker for release, P2 = important but can defer if time‑boxed.
Comparison of Manual vs. Automated Tooling
| Aspect | Manual Testing | Automated (Code‑Based) | Autonomous Persona‑Driven (SUSA) |
|---|---|---|---|
| Setup time | Low (just a browser) | Medium (test framework, selectors) | Low‑medium (install agent, point at URL) |
| Coverage depth | High for exploratory, low for repeatability | High for repeatable scripts, low for unforeseen flows | Medium‑high: explores many paths, learns over runs |
| Maintenance | None (ad‑hoc) | High (selector updates, flaky test fixes) | Low (agent adapts to UI changes) |
| Speed per run | Minutes to hours (depends on tester) | Seconds to minutes (parallelizable) | Minutes (depends on exploration budget) |
| Ability to catch UX friction | Excellent (human perception) | Limited (needs explicit assertions) | Good (personas simulate varied behavior) |
| Ideal use case | Early‑stage, ad‑hoc bug hunts | Regression suites, CI gating | Continuous learning, pre‑release exploratory, regression seed generation |
Manual Testing Approach
Even when automation is in place, a disciplined manual session catches nuances that scripts miss. Follow this step‑by‑step routine for each release candidate.
4.1 Setup and Environment
- Browser matrix – Test the latest stable versions of Chrome, Firefox, Safari, and Edge on both Windows and macOS. Include a mobile viewport via device emulation (iOS Safari, Android Chrome).
- User accounts – Prepare at least three test accounts: a fresh account with no favorites, an account with a moderate set (≈50 items), and a power‑user account with >5 000 favorites.
- Network conditioning – Use Chrome DevTools throttling (Slow 3G, offline) or a tool like
tcto simulate latency and packet loss. - Storage inspection – Keep the Application tab open to monitor localStorage/IndexedDB changes in real time.
- Logging – Enable verbose network logs and console errors; capture them with a screen recorder or a tool like
BrowserStack Automatefor later review.
4.2 Step‑by‑Step Test Execution
- Login – Verify that the authentication flow does not interfere with favorite endpoints (e.g., no stray OPTIONS calls).
- Navigate to a content list – Choose a page where favorites are exposed (product grid, article feed, video carousel).
- Baseline check – Confirm that the UI state of each itemId‑filled” or “Id‑outlined”.
- Happy‑path toggle – Perform a single click on a few items, observe immediate UI change, then reload the page to verify persistence.
- Bulk operations – If the UI offers “Select all” or “Select multiple”, test adding and removing favorites in batches. Verify that backend receives a single batch request (or a series, depending on design) and that UI updates atomically.
- Error injection – Using DevTools, toggle the network to offline, attempt a favorite toggle, then restore connectivity. Ensure the UI does not get stuck in an intermediate state and that a retry mechanism (if present) triggers.
- Accessibility spot‑check – Navigate using only the keyboard (Tab, Shift+Tab, Enter/Space). Verify focus order, visible focus ring, and that screen readers announce state changes (use NVDA or VoiceOver).
- Theme and locale switch – Change the OS or browser language to a right‑to‑left language (e.g., Arabic) and toggle dark mode. Confirm that the favorite icon mirrors correctly and contrast ratios stay ≥4.5:1.
- Clean‑up – Log out, clear site data, and repeat the steps to guarantee no cross‑account leakage.
4.3 Exploratory Testing Tips
- Rapid double‑click – Try to trigger the toggle twice within 100 ms to surface race conditions.
- Context‑menu tricks – Right‑click the favorite icon; some browsers expose “Open link in new tab” which may bypass the toggle handler.
- Extension interference – Enable popular ad‑blockers or privacy extensions (uBlock Origin, Privacy Badger) and see if they block the favorite request inadvertently.
- Page‑visibility API – Hide the tab, toggle a favorite, then return; verify that the optimistic update didn’t get lost.
- Service worker interception – If the app uses a SW, temporarily unregister it and confirm that favorites still work via direct network calls.
4.4 Logging and Bug Reporting
When a defect is found, capture:
- Browser version, OS, device model.
- Exact steps with timestamps.
- Screenshot or short video (≤30 s) showing UI before/after.
- Network request/response payload (copy as cURL).
- Console error stack trace.
- Storage snapshot (localStorage/IndexedDB export).
Upload this package to your bug tracker with the test‑matrix ID (e.g., FV‑004) to enable quick triage.
Automated Testing Strategies
Automation gives you confidence that regressions are caught early and that the favorite flow works across configurations. Below are patterns you can adopt, ranging from unit tests to full‑blown persona‑driven exploration with SUSA.
5.1 Unit and Integration Tests for Favorite Logic
Isolate the pure JavaScript module that handles toggle state, storage writes, and API calls. Example using Jest:
// favoriteService.js
export class FavoriteService {
constructor(storageApi, apiClient) {
this.storage = storageApi;
this.api = apiClient;
}
async toggle(itemId) {
const currentlyFav = await this.storage.get(itemId);
if (currentlyFav) {
await this.storage.remove(itemId);
await this.api.delete(`/favorites/${itemId}`);
} else {
await this.storage.set(itemId, { added: Date.now() });
await this.api.post('/favorites', { itemId });
}
return !currentlyFav;
}
}
// favoriteService.test.js
import { FavoriteService } from './favoriteService';
import { mockStorage, mockApi } from './mocks';
test('toggle adds favorite when not present', async () => {
const storage = mockStorage(); // implements get/set/remove
const api = mockApi(); // implements post/delete
const fav = new FavoriteService(storage, api);
storage.get.mockResolvedValue(null); // not favorited yet
await fav.toggle('item-123');
expect(storage.set).toHaveBeenCalledWith('item-123', expect.objectContaining({ added: expect.any(Number) }));
expect(api.post).toHaveBeenCalledWith('/favorites', { itemId: 'item-123' });
});
*Why this matters*: Unit tests guarantee that the state machine logic is correct regardless of UI rendering bugs. They run in milliseconds and can be part of every commit.
5.2 End‑to‑End Tests with Playwright
Playwright offers cross‑browser, auto‑waiting, and traceability. Below is a robust test suite covering the matrix entries FV‑001, FV‑002, FV‑004, and FV‑009.
// tests/favorite.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Favorite functionality', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://example-shop.com/products');
await page.fill('#email', 'tester@example.com');
await page.fill('#password', 'SecurePass!123');
await page.click('#login-button');
await page.waitForURL('**/products**');
});
test('add and remove favorite persists after reload', async ({ page }) => {
const heart = page.locator('.product-card').first().locator('button[aria-label="Add to favorites"]');
await expect(heart).toHaveAttribute('aria-pressed', 'false');
await heart.click();
await expect(heart).toHaveAttribute('aria-pressed', 'true');
await page.reload();
await expect(heart).toHaveAttribute('aria-pressed', 'true');
await heart.click();
await expect(heart).toHaveAttribute('aria-pressed', 'false');
await page.reload();
await expect(heart).toHaveAttribute('aria-pressed', 'false');
});
test('handles offline toggle gracefully', async ({ page }) => {
await page.context().route('**/favorites/**', route => route.abort()); // simulate net loss
const heart = page.locator('.product-card').first().locator('button[aria-label="Add to favorites"]');
await heart.click();
await expect(heart).toHaveAttribute('aria-pressed', 'true'); // optimistic UI
await page.context().unroute('**/favorites/**'); // restore
// Wait for retry (if implemented) or for a toast indicating failure
const toast = page.locator('.toast');
await expect(toast).toContainText('Failed to save favorite', { timeout: 5000 });
// UI should revert
await expect(heart).toHaveAttribute('aria-pressed', 'false');
});
test('keyboard accessibility', async ({ page }) => {
const heart = page.locator('.product-card').first().locator('button[aria-label="Add to favorites"]');
await heart.focus();
await expect(heart).toBeFocused();
await page.keyboard.press('Enter');
await expect(heart).toHaveAttribute('aria-pressed', 'true');
// Screen‑reader announcement can be asserted via aria-live region if present
const live = page.locator('[aria-live="polite"]');
await expect(live).toContainText('Added to favorites');
});
});
*Key points*:
test.beforeEachensures a clean login state.locatorstrings rely on stable ARIA labels (aria-label="Add to favorites").- Network abort simulates offline condition (FV‑004).
- Focus and
aria-pressedchecks cover accessibility (FV‑009).
5.3 Visual Regression for UI State
Tools like Percy or Chromatic can snapshot the favorite button in both states. A simple configuration:
// .chromatic.json
{
"storybook": false,
"url": "https://example-shop.com/products",
"viewport": [
{ "width": 1280, "height": 800 },
{ "width": 375, "height": 667 }
],
"selectors": [".product-card button[aria-label='Add to favorites']"]
}
Run Chromatic on each PR; any shift in icon color, size, or surrounding layout triggers a review.
5.4 Performance and Load Testing for Bulk Favorites
When a user imports a large collection (e.g., migrating from another service), the favorite endpoint may be hammered. Use k6 to simulate bursts:
// k6/favorite_load.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 }, // ramp up to 50 VUs
{ duration: '5m', target: 50 }, // sustain
{ duration: '2m', target: 0 }, // ramp down
],
};
const API_TOKEN = __ENV.API_TOKEN;
const ITEM_IDS = Array.from({ length: 1000 }, (_, i) => `item-${i + 10000}`);
export default function () {
const id = ITEM_IDS[Math.floor(Math.random() * ITEM_IDS.length)];
const payload = JSON.stringify({ itemId: id });
const params = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_TOKEN}`,
},
};
const res = http.post('https://api.example.com/favorites', payload, params);
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(0.5);
}
Run with k6 run favorite_load.js. Monitor backend latency and error rates; adjust throttling or batching as needed.
5.5 Using SUSA for Autonomous Persona‑Driven Exploration
SUSA can be pointed at a staging URL and let’s say a QA environment to surface favorite bugs that scripted tests never consider. Example CLI usage:
# Install the agent (once)
pip install susatest-agent
# Run a 30‑minute exploratory session with a mix of personas
susatest explore \
--url https://staging.example-shop.com \
--apk ./android-app.apk \ # optional if you also have a native companion
--personas curious impatient elderly accessibility \
--duration 30m \
--output ./susa-report.json
The agent will:
- Navigate to product pages, attempt to favorite items via taps, long‑presses, and context menus.
- Simulate an impatient user who rapidly double‑clicks the heart, exposing race conditions (FV‑006).
- Emulate an elderly user with increased touch target size requirements, revealing cases where the hit‑area is too small (accessibility).
- Trigger network interruptions at random intervals to test offline queueing (FV‑004).
- After the run, SUSA generates regression scripts in both Appium (Android) and Playwright (Web) that you can add to your CI pipeline.
Because SUSA builds a memory of explored screens and dead ends, subsequent runs become smarter, gradually covering edge cases like storage quota exhaustion or localization flips that manual testers might overlook.
5.6 CI Integration
Add the Playwright suite to your CI (GitHub Actions example):
name: Web Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- run: npx playwright test
For SUSA, you can add a nightly workflow:
name: SUSA Exploration
on:
schedule:
- cron: '0 2 * * *' # 02:00 UTC daily
jobs:
explore:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install SUSA agent
run: pip install susatest-agent
- run: susatest explore --url https://staging.example-shop.com --personas curious impatient --duration 15m --output ./susa-report.json
- name: Upload report
uses: actions/upload-artifact@v3
with:
name: susa-report
path: susa-report.json
Edge Cases That Appear Only in Production
Even the most exhaustive test matrix can miss issues that only manifest under real‑world traffic, browser quirks, or infrastructure constraints. Below are production‑only failure modes you should monitor and, where possible, reproduce in staging.
6.1 Network Flakiness and Offline Behavior
- Partial‑response bodies – flaky CDN may return a truncated JSON payload; the client may parse it incorrectly and mark a favorite as added when the server did not persist it.
- DNS rebinding attacks – a malicious site could resolve to your internal API after the page loads, causing unauthorized favorite toggles. Mitigate with strict
SameSitecookies andAccess-Control-Allow-Origin.
Reproduction: Use a tool like toxiproxy to inject random latency and packet loss, or configure a local proxy to drop 1 % of response bytes.
6.2 Race Conditions with Concurrent Updates
If a user opens the same product in two tabs and toggles the favorite simultaneously, you can end up with:
- Lost update (second toggle overwrites first).
- Duplicate entry (both toggles send a create).
Solution: Server‑side use of optimistic locking with a version column or a unique constraint on (userId, itemId). The client should send the current version or rely on the server to idempotently handle the request.
Test: Open two Chrome instances, log in as same user, navigate to same product, and rapidly click the heart in both windows. Verify final state matches expectation (only one favorite record).
6.3 Third‑Party Cookie/Storage Restrictions
Safari’s Intelligent Tracking Prevention (ITP) and Chrome’s upcoming “Privacy Sandbox” may purge localStorage after 7 days of no interaction, or partition storage per top‑level site. If your favorite data lives solely in localStorage, users may see their collection disappear after a week of inactivity.
Mitigation: Store favorites in IndexedDB (which is not subject to ITP’s timer) or sync to backend on every change.
6.4 Browser Extension Interference
Ad blockers sometimes mistakenly treat requests to /favorites as tracking and block them. Likewise, password managers may inject extra form fields that alter DOM hierarchy and break selector‑based tests.
Check: Run your test suite with popular extensions enabled (uBlock Origin, Privacy Badger, LastPass). If a block occurs, adjust the endpoint naming or ensure the request is not flagged by common filter lists (e.g., avoid URLs containing “track” or “ad”).
6.5 Localization and RTL Layout
When the UI switches to a right‑to‑left language, the favorite icon may appear on the opposite side of the item card, causing mis‑aligned tap targets or overflow. Also, some languages have longer strings (“إضافة إلى المفضلات”) that can push the icon out of view.
Test: Set the browser locale to ar-SA and verify that:
- The icon’s margin/padding respects
direction: rtl. - The container does not clip the icon.
- Touch target remains at least 48 dp (per Material guidelines).
6.6 Dark Mode and Theme Changes
CSS custom properties may not update correctly when the user toggles dark mode via the OS while the page is already open. A favorite icon that relies on fixed colors could lose contrast.
Approach: Listen to the matchMedia('(prefers-color-scheme: dark)') change event and recompute styles, or use CSS variables that automatically follow the system setting. Validate with the DevTools “Rendering” panel → “Emulate CSS media feature prefers‑color‑scheme”.
6.7 Session Restore After Browser Crash
If the browser crashes while a favorite toggle is in flight (optimistic UI shown, request pending), the next session should either:
- Show the item as unfavorited if the request never reached the server, or
- Show as favorited if the request succeeded but the UI never got the acknowledgment.
Test: Use chrome://restart or kill the browser process via task manager while the network is throttled, then relaunch and check the persisted state.
Accessibility and Inclusive Testing
Favorites must be usable by people with diverse abilities. Beyond the basic keyboard and screen‑reader checks, consider the following dimensions.
7.1 Keyboard Navigation
- All favorite controls must be reachable via
Tab. - Activating via
EnterorSpacemust toggle state. - Avoid using
onkeydownhandlers that prevent default scrolling inadvertently.
7.2 Screen Reader Announcements
- The button should have an
aria-labelthat changes with state:"Add to favorites"vs"Remove from favorites". - If you use a purely icon‑based button, supplement with
aria-labelandaria-pressed="true|false". - Live regions (
aria-live="polite") can announce success or error messages without moving focus.
7.3 Color Contrast and Focus Visible
- Use a contrast ratio of at least 4.5:1 for the icon against its background (WCAG AA).
- Provide a visible focus outline (minimum 2 px solid, offset 2 px) that does not rely solely on color change.
7.4 ARIA Roles and Properties
If you implement a custom “favorite list” panel, treat it as a region with aria-labelledby pointing to a heading. Each list item can be a button with aria-pressed.
Example markup:
<section aria-labelledby="fav-heading" role="region">
<h2 id="fav-heading">My Favorites</h2>
<button
class="fav-item"
aria-pressed="true"
aria-label="Remove ‘Product X’ from favorites"
>
<svg class="icon" aria-hidden="true">…</svg>
Product X
</button>
</section>
7.5 Testing with Persona Profiles (SUSA Mention)
SUSA includes built‑in persona models that simulate varying motor abilities, vision impairments, and cognitive load. For example, the “elderly” persona enlarges tap targets and introduces a delayed reaction time, while the “accessibility” persona enables high‑contrast mode and screen‑reader navigation.
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