← Back to main article

A detailed look at QA-009 - how a 4-session feature became 10 sessions through real-device testing, and the bugs that only appeared on a decade-old iPad.

Focused work time: ~4 hours (morning of Jan 8, 2026) Issues discovered: 7 total (4 compatibility issues + 3 critical bugs) Final output: 2,793 lines added across 20 files, 1,735 lines of documentation


# The Original Issue

Search result images displayed as white boxes on iOS 9 Safari.

# What Real-Device Testing Discovered

4 compatibility issues that weren't in the original plan:

  1. iOS 9 Safari blocks some external image URLs (CORS and security policies)
  2. WebP format not supported on iOS 9 (added in Safari 14)
  3. await for image caching added 4-5 seconds to response time
  4. JPEG quality at 85 was too low for high-DPI displays

# How the Sessions Evolved

Session Task What Happened
1 Model & Service Planned
2 API Integration Planned
3 Frontend Updates Learned: JS renders cards, not templates
4 Cleanup & Docs Planned
5 Performance Fix Response time 4-5s -> <1s with threading
6 Image Quality Bumped JPEG 85->92 for high-DPI
7 Production Config Gunicorn threads, monitoring
8 Progressive Loading Added spinners + polling for UX
9 Load More Fix 3 critical bugs discovered
10 Performance Opts 75% fewer DOM queries

Sessions 1-4 were the original plan. Sessions 5-10 emerged from testing on the actual iPad.


# The Three Critical Bugs (Session 9)

These bugs only appeared when testing pagination with cached images on the real iPad. Unit tests and desktop browser testing missed them entirely.

# Bug #1: escapeSelector() was insufficient

// BROKEN - Only escaped " and \
function escapeSelector(str) {
    return str.replace(/["\\]/g, '\\$&');
}

// FIXED - Escape ALL CSS special characters
function escapeSelector(str) {
    return str.replace(/([!"#$%&'()*+,.\:;<=>?@\[\\\]^`{|}~])/g, '\\$1');
}

URLs contain colons, dots, slashes, and hash symbols. document.querySelector('[data-url="https://example.com/path"]') failed silently because the URL characters weren't escaped.

# Bug #2: Spinner logic wouldn't replace external URL images

The code checked if the image source started with / (local path) before replacing with a spinner. External URLs don't start with /, so they were never replaced.

// BROKEN
if (img.src.startsWith('/')) {
    img.src = '/static/spinner.gif';
}

// FIXED - Check for any valid src, not just local paths
if (img.src && img.src !== '') {
    img.dataset.originalSrc = img.src;
    img.src = '/static/spinner.gif';
}

# Bug #3: Pagination rendered ALL results every time

The "Load More" button was supposed to append new results. Instead, it re-rendered the entire result set, causing duplicates.

// BROKEN - Replaced all content
resultsContainer.innerHTML = renderAllResults(allResults);

// FIXED - Append only new results
const newResultsHtml = renderResults(newResults);
resultsContainer.insertAdjacentHTML('beforeend', newResultsHtml);

# Why Desktop Testing Missed These

  1. Desktop browsers handle CSS selectors more leniently - Chrome's querySelector didn't fail on unescaped URLs
  2. External images loaded fine on desktop - No iOS 9 security policies blocking them
  3. Pagination wasn't tested with slow image loading - Desktop cached images instantly, masking the race condition

# The Takeaway

Complex features WILL grow beyond the plan. The implementation phase/session structure keeps each chunk manageable even when the total expands - you can always ask Claude to add a new Session with a specific objective to a Phase. Four sessions became ten, but each session was still focused and completable.


# Commit Reference

6dc3bfa - feat: Add progressive image caching for search results (QA-009)