← 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)