← back to Secrets Manager
auto-save: 2026-07-22T13:14:23 (4 files) — dw-scroll-debug.js dw-scroll-debug2.js dw-scroll-debug3.js dw-scroll-verify.js
367cf90c4d074e5e7faf3c137451b8b12b2fd8cf · 2026-07-22 13:14:23 -0700 · Steve Abrams
Files touched
A dw-scroll-debug.jsA dw-scroll-debug2.jsA dw-scroll-debug3.jsA dw-scroll-verify.js
Diff
commit 367cf90c4d074e5e7faf3c137451b8b12b2fd8cf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 22 13:14:23 2026 -0700
auto-save: 2026-07-22T13:14:23 (4 files) — dw-scroll-debug.js dw-scroll-debug2.js dw-scroll-debug3.js dw-scroll-verify.js
---
dw-scroll-debug.js | 480 ++++++++++++++++++++++++++++++++++++++++++++++++
dw-scroll-debug2.js | 513 ++++++++++++++++++++++++++++++++++++++++++++++++++++
dw-scroll-debug3.js | 312 ++++++++++++++++++++++++++++++++
dw-scroll-verify.js | 201 ++++++++++++++++++++
4 files changed, 1506 insertions(+)
diff --git a/dw-scroll-debug.js b/dw-scroll-debug.js
new file mode 100644
index 0000000..abd2685
--- /dev/null
+++ b/dw-scroll-debug.js
@@ -0,0 +1,480 @@
+/**
+ * dw-scroll-debug.js
+ * Headless Playwright investigation of the infinite-scroll glitch on
+ * https://www.designerwallcoverings.com/collections/decoglass-wallpaper?page=6
+ *
+ * Instruments:
+ * - dw-card-hover.js run() fire count (via page.evaluate injection)
+ * - .dw-hover-overlay duplicate detection per card
+ * - MutationObserver callback count
+ * - Performance long-tasks (PerformanceObserver)
+ * - Boost network requests (XHR/fetch) for infinite-scroll API calls
+ * - Card count growth across scroll steps
+ * - Console errors
+ */
+
+const { chromium } = require('playwright');
+const path = require('path');
+
+const TARGET_URL = 'https://www.designerwallcoverings.com/collections/decoglass-wallpaper?page=6';
+const SCROLL_STEPS = 12;
+const SCROLL_PAUSE_MS = 2500; // wait between scrolls for Boost to load
+const VIEWPORT = { width: 1440, height: 900 };
+
+(async () => {
+ const browser = await chromium.launch({
+ headless: true,
+ args: [
+ '--no-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-blink-features=AutomationControlled',
+ ],
+ });
+
+ const context = await browser.newContext({
+ viewport: VIEWPORT,
+ userAgent:
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
+ '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
+ extraHTTPHeaders: {
+ 'Accept-Language': 'en-US,en;q=0.9',
+ },
+ });
+
+ const page = await context.newPage();
+
+ // ── Capture console messages ──────────────────────────────────────────────
+ const consoleErrors = [];
+ const consoleWarnings = [];
+ page.on('console', (msg) => {
+ if (msg.type() === 'error') consoleErrors.push(msg.text());
+ if (msg.type() === 'warning') consoleWarnings.push(msg.text());
+ });
+
+ // ── Capture network activity ──────────────────────────────────────────────
+ const boostRequests = [];
+ page.on('request', (req) => {
+ const url = req.url();
+ if (url.includes('boost') || url.includes('filter') || url.includes('search') || url.includes('product')) {
+ boostRequests.push({ url: url.substring(0, 150), method: req.method(), type: req.resourceType() });
+ }
+ });
+
+ const networkErrors = [];
+ page.on('requestfailed', (req) => {
+ networkErrors.push({ url: req.url().substring(0, 120), err: req.failure()?.errorText });
+ });
+
+ let httpStatus = null;
+ page.on('response', (resp) => {
+ if (resp.url() === TARGET_URL || resp.url().startsWith(TARGET_URL.split('?')[0])) {
+ httpStatus = resp.status();
+ }
+ });
+
+ console.log('=== DW Infinite-Scroll Debug ===');
+ console.log(`Target: ${TARGET_URL}`);
+ console.log(`Viewport: ${VIEWPORT.width}x${VIEWPORT.height}`);
+ console.log(`Scroll steps: ${SCROLL_STEPS}, pause: ${SCROLL_PAUSE_MS}ms each\n`);
+
+ // ── Navigate ──────────────────────────────────────────────────────────────
+ console.log('[1] Navigating to page...');
+ try {
+ const resp = await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+ httpStatus = resp?.status();
+ console.log(` HTTP status: ${httpStatus}`);
+ if (httpStatus === 429) {
+ console.log(' [RATE-LIMITED 429] Waiting 15s before retry...');
+ await page.waitForTimeout(15000);
+ await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+ }
+ } catch (e) {
+ console.log(` Navigation error: ${e.message}`);
+ }
+
+ // ── Wait for Boost product cards ──────────────────────────────────────────
+ console.log('[2] Waiting for .boost-sd__product-item cards...');
+ let initialCardCount = 0;
+ try {
+ await page.waitForSelector('.boost-sd__product-item', { timeout: 20000 });
+ initialCardCount = await page.locator('.boost-sd__product-item').count();
+ console.log(` Initial card count: ${initialCardCount}`);
+ } catch (e) {
+ console.log(` Could not find .boost-sd__product-item: ${e.message}`);
+ // Check for alternate selectors
+ const alt1 = await page.locator('.product-list-item').count();
+ const alt2 = await page.locator('[data-product-id]').count();
+ console.log(` .product-list-item: ${alt1}, [data-product-id]: ${alt2}`);
+ const html = await page.content();
+ const snippet = html.substring(0, 3000);
+ console.log(` Page HTML snippet: ${snippet}`);
+ }
+
+ // ── Wait for dw-card-hover.js to initialize ───────────────────────────────
+ console.log('[3] Waiting for dw-card-hover.js to initialize...');
+ await page.waitForTimeout(2000);
+ const dwHoverActive = await page.evaluate(() => !!window.__dwCardHover);
+ console.log(` window.__dwCardHover present: ${dwHoverActive}`);
+
+ // ── Instrument: patch run() to count calls ────────────────────────────────
+ console.log('[4] Patching run() counter and MutationObserver counter...');
+ const instrumentResult = await page.evaluate(() => {
+ // We cannot directly wrap the private run() function inside the IIFE.
+ // Instead, we instrument the MutationObserver callback indirectly:
+ // 1. Count how many times .dw-hover-overlay nodes are INSERTED (net new)
+ // which is a proxy for run() doing work on new cards.
+ // 2. Count MutationObserver callbacks firing on document.body.
+
+ window.__dwRunCount = 0;
+ window.__dwOverlayInsertions = 0;
+ window.__mutObsCallbackCount = 0;
+ window.__longTaskCount = 0;
+ window.__longTaskTotalMs = 0;
+
+ // Count overlay insertions via a second MutationObserver
+ const overlayWatcher = new MutationObserver((muts) => {
+ for (const m of muts) {
+ for (const node of m.addedNodes) {
+ if (node.nodeType === 1) {
+ if (node.classList && node.classList.contains('dw-hover-overlay')) {
+ window.__dwOverlayInsertions++;
+ }
+ // Also count overlays inside added subtrees
+ if (node.querySelectorAll) {
+ window.__dwOverlayInsertions += node.querySelectorAll('.dw-hover-overlay').length;
+ }
+ }
+ }
+ }
+ });
+ overlayWatcher.observe(document.body, { childList: true, subtree: true });
+
+ // Measure long tasks
+ try {
+ const po = new PerformanceObserver((list) => {
+ for (const entry of list.getEntries()) {
+ window.__longTaskCount++;
+ window.__longTaskTotalMs += entry.duration;
+ }
+ });
+ po.observe({ entryTypes: ['longtask'] });
+ } catch (e) { /* longtask not supported */ }
+
+ // Count body-level MutationObserver firings indirectly:
+ // Wrap MutationObserver constructor to intercept calls on document.body
+ // This needs to be done BEFORE dw-card-hover.js runs — too late now.
+ // Instead, we count body childList mutations ourselves.
+ const bodyMutWatcher = new MutationObserver((muts) => {
+ window.__mutObsCallbackCount++;
+ });
+ bodyMutWatcher.observe(document.body, { childList: true, subtree: true });
+
+ return { patched: true, overlayWatcher: 'installed', bodyMutWatcher: 'installed' };
+ });
+ console.log(` Instrumentation: ${JSON.stringify(instrumentResult)}`);
+
+ // ── Baseline overlay counts ────────────────────────────────────────────────
+ console.log('[5] Baseline card and overlay counts...');
+ const baseline = await page.evaluate(() => {
+ const cards = document.querySelectorAll('.boost-sd__product-item, .product-list-item');
+ let withOverlay = 0, withMultipleOverlays = 0;
+ for (const card of cards) {
+ const overlays = card.querySelectorAll('.dw-hover-overlay');
+ // Also check siblings (v5 places overlay as sibling, not inside card)
+ const parent = card.parentElement;
+ const siblingOverlays = parent ? parent.querySelectorAll(':scope > .dw-hover-overlay') : [];
+ const total = overlays.length + siblingOverlays.length;
+ if (total >= 1) withOverlay++;
+ if (total > 1) withMultipleOverlays++;
+ }
+ // Count all .dw-hover-overlay in the document
+ const allOverlays = document.querySelectorAll('.dw-hover-overlay').length;
+ return {
+ cards: cards.length,
+ withOverlay,
+ withMultipleOverlays,
+ allOverlays,
+ overlaysInDom: allOverlays
+ };
+ });
+ console.log(` Cards: ${baseline.cards}, with overlay: ${baseline.withOverlay}, duplicates (>1): ${baseline.withMultipleOverlays}`);
+ console.log(` Total .dw-hover-overlay in DOM: ${baseline.allOverlays}`);
+
+ // ── Scroll steps ──────────────────────────────────────────────────────────
+ console.log(`[6] Scrolling ${SCROLL_STEPS} times to trigger infinite-scroll...`);
+ const snapshots = [];
+
+ for (let step = 0; step < SCROLL_STEPS; step++) {
+ // Scroll to bottom
+ await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
+ await page.waitForTimeout(SCROLL_PAUSE_MS);
+
+ // Capture state
+ const snap = await page.evaluate((stepNum) => {
+ const cards = document.querySelectorAll('.boost-sd__product-item, .product-list-item');
+ let withOverlay = 0, withMultipleOverlays = 0, noOverlay = 0;
+ const dupCards = [];
+
+ for (const card of cards) {
+ // v5 places overlay as a sibling AFTER the image column, so check
+ // both inside the card and as an immediate sibling
+ const insideOverlays = card.querySelectorAll('.dw-hover-overlay').length;
+ // Traverse parent to find sibling overlays adjacent to this card
+ let sibCount = 0;
+ let sib = card.nextSibling;
+ while (sib) {
+ if (sib.nodeType === 1 && sib.classList && sib.classList.contains('dw-hover-overlay')) {
+ sibCount++;
+ }
+ sib = sib.nodeType === 1 ? null : sib.nextSibling; // stop at next element node
+ break; // only check first next sibling
+ }
+ // Also check inside card's image wrapper area
+ const total = insideOverlays + sibCount;
+ if (total >= 1) withOverlay++;
+ else noOverlay++;
+ if (total > 1 || insideOverlays > 1) {
+ withMultipleOverlays++;
+ dupCards.push(card.getAttribute('data-product-id') || card.className.substring(0, 40));
+ }
+ }
+
+ const allOverlays = document.querySelectorAll('.dw-hover-overlay').length;
+
+ return {
+ step: stepNum,
+ cardCount: cards.length,
+ withOverlay,
+ noOverlay,
+ withMultipleOverlays,
+ dupCards: dupCards.slice(0, 5),
+ allOverlays,
+ scrollY: window.scrollY,
+ docHeight: document.body.scrollHeight,
+ mutObsCallbacks: window.__mutObsCallbackCount,
+ overlayInsertions: window.__dwOverlayInsertions,
+ longTaskCount: window.__longTaskCount,
+ longTaskTotalMs: Math.round(window.__longTaskTotalMs),
+ };
+ }, step + 1);
+
+ console.log(
+ ` Step ${step + 1}: cards=${snap.cardCount} overlays=${snap.allOverlays} ` +
+ `dups=${snap.withMultipleOverlays} noOverlay=${snap.noOverlay} ` +
+ `mutCBs=${snap.mutObsCallbacks} overlayInserts=${snap.overlayInsertions} ` +
+ `longTasks=${snap.longTaskCount}(${snap.longTaskTotalMs}ms) ` +
+ `scrollY=${snap.scrollY}/${snap.docHeight}`
+ );
+ if (snap.dupCards.length > 0) {
+ console.log(` DUPLICATE OVERLAY CARDS: ${snap.dupCards.join(', ')}`);
+ }
+ snapshots.push(snap);
+
+ // Check if we stopped getting new cards (stall detection)
+ if (step > 2 && snap.cardCount === snapshots[step - 1].cardCount && snap.scrollY > 0) {
+ console.log(` [STALL DETECTED] Card count unchanged at ${snap.cardCount} for 2 steps`);
+ }
+ }
+
+ // ── Final analysis ────────────────────────────────────────────────────────
+ console.log('\n[7] Final DOM analysis...');
+
+ const finalAnalysis = await page.evaluate(() => {
+ const cards = document.querySelectorAll('.boost-sd__product-item, .product-list-item');
+ const allOverlays = document.querySelectorAll('.dw-hover-overlay');
+
+ // Check for overlays outside of card contexts (orphaned)
+ let orphanedOverlays = 0;
+ for (const ov of allOverlays) {
+ const inCard = ov.closest('.boost-sd__product-item, .product-list-item');
+ // v5: overlay is sibling, walk up to see if it's near a card
+ if (!inCard) {
+ // Check previous sibling chain for a card
+ let prev = ov.previousElementSibling;
+ let foundCard = false;
+ while (prev && !foundCard) {
+ if (prev.matches('.boost-sd__product-item, .product-list-item')) foundCard = true;
+ // Check inside prev for an image layout wrapper
+ if (prev.matches('.boost-sd__product-item-grid-view-layout-image')) foundCard = true;
+ prev = prev.previousElementSibling;
+ }
+ if (!foundCard) orphanedOverlays++;
+ }
+ }
+
+ // Count per-card overlays using the v5 sibling placement logic
+ const cardOverlayCounts = {};
+ for (const card of cards) {
+ const pid = card.getAttribute('data-product-id') ||
+ card.getAttribute('data-id') ||
+ Array.from(card.classList).find(c => c.includes('boost')) || 'unknown';
+
+ // Inside the card
+ let count = card.querySelectorAll('.dw-hover-overlay').length;
+
+ // v5 places overlay after the image-col sibling; the overlay is a child
+ // of the card's parent (the grid item row/wrapper), not inside the card itself.
+ // Let's check by scanning ALL overlays and seeing how many follow THIS card
+ // in the DOM parent.
+ const parent = card.parentElement;
+ if (parent) {
+ const children = Array.from(parent.children);
+ const cardIdx = children.indexOf(card);
+ // Count consecutive .dw-hover-overlay siblings right after card
+ for (let i = cardIdx + 1; i < children.length; i++) {
+ if (children[i].classList.contains('dw-hover-overlay')) count++;
+ else break;
+ }
+ }
+
+ cardOverlayCounts[pid] = count;
+ }
+
+ const duplicatedCards = Object.entries(cardOverlayCounts).filter(([, c]) => c > 1);
+ const zeroOverlayCards = Object.entries(cardOverlayCounts).filter(([, c]) => c === 0);
+
+ // Check if Boost infinite scroll sentinel/loader exists
+ const loaders = document.querySelectorAll(
+ '[class*="boost-sd__load-more"], [class*="boost-sd__infinite"], [class*="load-more"], .boost-sd__button-load-more'
+ );
+
+ // Check for any CSS that might block scroll
+ const bodyOverflow = window.getComputedStyle(document.body).overflow;
+ const htmlOverflow = window.getComputedStyle(document.documentElement).overflow;
+
+ return {
+ finalCardCount: cards.length,
+ finalOverlayCount: allOverlays.length,
+ orphanedOverlays,
+ duplicatedCards: duplicatedCards.slice(0, 10).map(([id, c]) => `${id}:${c}`),
+ duplicatedCardCount: duplicatedCards.length,
+ zeroOverlayCardCount: zeroOverlayCards.length,
+ boostLoaderElements: loaders.length,
+ boostLoaderClasses: Array.from(loaders).map(el => el.className).slice(0, 5),
+ bodyOverflow,
+ htmlOverflow,
+ mutObsCallbacks: window.__mutObsCallbackCount,
+ overlayInsertions: window.__dwOverlayInsertions,
+ longTaskCount: window.__longTaskCount,
+ longTaskTotalMs: Math.round(window.__longTaskTotalMs),
+ dwHoverActive: window.__dwCardHover,
+ };
+ });
+
+ console.log('\n=== FINAL ANALYSIS ===');
+ console.log(`Cards in DOM: ${finalAnalysis.finalCardCount}`);
+ console.log(`Total .dw-hover-overlay in DOM: ${finalAnalysis.finalOverlayCount}`);
+ console.log(`Orphaned overlays (not near any card): ${finalAnalysis.orphanedOverlays}`);
+ console.log(`Cards with DUPLICATE overlays: ${finalAnalysis.duplicatedCardCount}`);
+ if (finalAnalysis.duplicatedCards.length > 0) {
+ console.log(` Duplicate samples: ${finalAnalysis.duplicatedCards.join(', ')}`);
+ }
+ console.log(`Cards with ZERO overlays: ${finalAnalysis.zeroOverlayCardCount}`);
+ console.log(`Boost loader/sentinel elements: ${finalAnalysis.boostLoaderElements}`);
+ if (finalAnalysis.boostLoaderClasses.length > 0) {
+ console.log(` Loader classes: ${finalAnalysis.boostLoaderClasses.join(', ')}`);
+ }
+ console.log(`Body overflow: ${finalAnalysis.bodyOverflow}, HTML overflow: ${finalAnalysis.htmlOverflow}`);
+ console.log(`\nMutationObserver body callbacks: ${finalAnalysis.mutObsCallbacks}`);
+ console.log(`run() proxy (overlay insertions): ${finalAnalysis.overlayInsertions}`);
+ console.log(`Long tasks: ${finalAnalysis.longTaskCount} totaling ${finalAnalysis.longTaskTotalMs}ms`);
+ console.log(`window.__dwCardHover: ${finalAnalysis.dwHoverActive}`);
+
+ // ── Scroll growth analysis ────────────────────────────────────────────────
+ console.log('\n=== SCROLL GROWTH ANALYSIS ===');
+ let prevCards = baseline.cards;
+ let stalls = 0;
+ for (const snap of snapshots) {
+ const delta = snap.cardCount - prevCards;
+ const status = delta > 0 ? `+${delta} new cards` : (delta === 0 ? 'STALL - no new cards' : `SHRINK by ${Math.abs(delta)}`);
+ const ratio = snap.allOverlays > 0 ? (snap.allOverlays / snap.cardCount).toFixed(2) : '0.00';
+ console.log(` Step ${snap.step}: ${snap.cardCount} cards (${status}), ${snap.allOverlays} overlays, ratio=${ratio}, longTasks=${snap.longTaskCount}(${snap.longTaskTotalMs}ms)`);
+ if (delta === 0) stalls++;
+ prevCards = snap.cardCount;
+ }
+
+ // ── MutationObserver thrash analysis ─────────────────────────────────────
+ console.log('\n=== MUTATION OBSERVER ANALYSIS ===');
+ const finalSnap = snapshots[snapshots.length - 1];
+ console.log(`Total body MutationObserver callbacks fired: ${finalSnap.mutObsCallbacks}`);
+ console.log(`Total overlay insertions by run(): ${finalSnap.overlayInsertions}`);
+ if (finalSnap.mutObsCallbacks > 0 && finalAnalysis.finalCardCount > 0) {
+ const callsPerCard = (finalSnap.mutObsCallbacks / finalAnalysis.finalCardCount).toFixed(1);
+ console.log(`Average MutObs callbacks per card: ${callsPerCard}`);
+ // Each run() call processes ALL cards and inserts overlays for new ones.
+ // If run() fires N times and inserts M overlays, N > M suggests many no-op re-scans.
+ const efficiency = finalSnap.overlayInsertions > 0
+ ? (finalSnap.mutObsCallbacks / finalSnap.overlayInsertions).toFixed(1)
+ : 'N/A';
+ console.log(`Callback:overlay-insertion ratio: ${efficiency}:1 (higher = more wasted re-scans)`);
+ }
+
+ // ── SELF-REINFORCING LOOP DETECTION ──────────────────────────────────────
+ console.log('\n=== SELF-REINFORCING LOOP DETECTION ===');
+ console.log('Key question: Does run() inserting .dw-hover-overlay nodes trigger MORE observer callbacks?');
+ console.log('(If yes, each overlay insertion fires the observer, which calls run() again,');
+ console.log(' which already skips labeled cards — but still does a full querySelectorAll scan)');
+ console.log(`Cards at baseline: ${baseline.cards}`);
+ console.log(`Overlays inserted by run(): ${finalSnap.overlayInsertions}`);
+ console.log(`Observer callbacks fired: ${finalSnap.mutObsCallbacks}`);
+ if (finalSnap.overlayInsertions > 0 && finalSnap.mutObsCallbacks > finalSnap.overlayInsertions * 1.5) {
+ console.log('FINDING: Observer callbacks >> overlay insertions — observer is firing for NON-run()-insertions too');
+ console.log(' This means Boost DOM mutations are also triggering the observer, causing repeated full-grid scans');
+ }
+
+ // ── Console errors ────────────────────────────────────────────────────────
+ if (consoleErrors.length > 0) {
+ console.log(`\n=== CONSOLE ERRORS (${consoleErrors.length}) ===`);
+ consoleErrors.slice(0, 10).forEach((e, i) => console.log(` [${i + 1}] ${e}`));
+ }
+ if (consoleWarnings.length > 0) {
+ console.log(`\n=== CONSOLE WARNINGS (${consoleWarnings.length}) ===`);
+ consoleWarnings.slice(0, 5).forEach((e, i) => console.log(` [${i + 1}] ${e}`));
+ }
+ if (networkErrors.length > 0) {
+ console.log(`\n=== NETWORK ERRORS (${networkErrors.length}) ===`);
+ networkErrors.slice(0, 5).forEach((e, i) => console.log(` [${i + 1}] ${e.url} — ${e.err}`));
+ }
+
+ // ── Boost API requests ────────────────────────────────────────────────────
+ const boostApiReqs = boostRequests.filter(r => r.url.includes('boost') && r.type === 'fetch' || r.url.includes('search') || r.url.includes('filter'));
+ if (boostApiReqs.length > 0) {
+ console.log(`\n=== BOOST API REQUESTS (${boostApiReqs.length}) ===`);
+ boostApiReqs.slice(0, 10).forEach((r, i) => console.log(` [${i + 1}] ${r.method} ${r.url}`));
+ }
+
+ // ── Final verdict ─────────────────────────────────────────────────────────
+ console.log('\n=== ROOT CAUSE VERDICT ===');
+ const overlapRatio = finalAnalysis.finalOverlayCount / Math.max(finalAnalysis.finalCardCount, 1);
+ const hasOverlayDuplicates = finalAnalysis.duplicatedCardCount > 0;
+ const hasMutThrash = finalSnap.mutObsCallbacks > 200;
+ const hasLongTasks = finalAnalysis.longTaskCount > 5;
+ const cardGrowthStopped = stalls >= 3;
+
+ if (hasOverlayDuplicates) {
+ console.log(`CONFIRMED: ${finalAnalysis.duplicatedCardCount} cards have DUPLICATE .dw-hover-overlay elements`);
+ console.log(' Root cause: run() is inserting overlays faster than its duplicate-check catches');
+ console.log(' OR: labelCard() sibling-insertion logic places overlay outside the card,');
+ console.log(' so card.querySelector(".dw-hover-overlay") returns null on next run() call');
+ console.log(' (the guard `if (card.querySelector(".dw-hover-overlay")) return;` misses sibling overlays)');
+ }
+ if (hasMutThrash) {
+ console.log(`CONFIRMED: MutationObserver fired ${finalSnap.mutObsCallbacks} times during scrolling`);
+ console.log(' Each fire schedules a full document.querySelectorAll() scan with 80ms debounce.');
+ console.log(' With Boost appending dozens of nodes per infinite-scroll batch, this fires continuously.');
+ }
+ if (hasLongTasks) {
+ console.log(`CONFIRMED: ${finalAnalysis.longTaskCount} long tasks (>${finalAnalysis.longTaskTotalMs}ms total)`);
+ console.log(' Long tasks block scroll-event processing, causing visible jank.');
+ }
+ if (cardGrowthStopped) {
+ console.log(`CONFIRMED: Infinite scroll STALLED — card count stopped growing for ${stalls} consecutive steps`);
+ }
+ if (!hasOverlayDuplicates && !hasMutThrash && !cardGrowthStopped) {
+ console.log('No dramatic failures found — glitch may be intermittent or require more scroll steps');
+ console.log(`Overlay/card ratio: ${overlapRatio.toFixed(2)} (should be ~1.0)`);
+ }
+
+ await browser.close();
+ console.log('\nDone.');
+})();
diff --git a/dw-scroll-debug2.js b/dw-scroll-debug2.js
new file mode 100644
index 0000000..909a957
--- /dev/null
+++ b/dw-scroll-debug2.js
@@ -0,0 +1,513 @@
+/**
+ * dw-scroll-debug2.js — Deep dive:
+ * 1. Why does scrollY freeze at 1564/2464 — is body overflow:hidden blocking scroll?
+ * 2. What is the Boost pagination mode (infinite vs load-more button)?
+ * 3. Does clicking the load-more button work?
+ * 4. What is Boost loading — does it load more products, or is page=6 end-of-collection?
+ * 5. Detailed self-reinforcing loop test: inject a fake card, watch observer thrash.
+ */
+
+const { chromium } = require('playwright');
+
+const TARGET_URL = 'https://www.designerwallcoverings.com/collections/decoglass-wallpaper?page=6';
+const VIEWPORT = { width: 1440, height: 900 };
+
+(async () => {
+ const browser = await chromium.launch({
+ headless: true,
+ args: ['--no-sandbox', '--disable-dev-shm-usage'],
+ });
+ const context = await browser.newContext({
+ viewport: VIEWPORT,
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
+ });
+ const page = await context.newPage();
+
+ const consoleErrors = [];
+ page.on('console', msg => { if (msg.type() === 'error') consoleErrors.push(msg.text()); });
+
+ // Capture all Boost API filter/search responses
+ const boostApiResponses = [];
+ page.on('response', async (resp) => {
+ const url = resp.url();
+ if (url.includes('bc-sf-filter') || url.includes('boostcommerce') && url.includes('filter')) {
+ try {
+ const status = resp.status();
+ let body = '';
+ try { body = await resp.text(); } catch(e) { body = '[could not read]'; }
+ boostApiResponses.push({ url: url.substring(0, 200), status, bodySnippet: body.substring(0, 500) });
+ } catch(e) {}
+ }
+ });
+
+ console.log('=== DW Scroll Debug — Round 2 ===');
+ console.log(`URL: ${TARGET_URL}`);
+
+ await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+ await page.waitForTimeout(3000); // let Boost fully render
+
+ // ── 1. Investigate scroll container ──────────────────────────────────────
+ console.log('\n[1] Scroll container investigation...');
+ const scrollInfo = await page.evaluate(() => {
+ // Find what element is actually scrollable
+ function isScrollable(el) {
+ const style = window.getComputedStyle(el);
+ const overflow = style.overflow + style.overflowY + style.overflowX;
+ return /auto|scroll/.test(overflow) && (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth);
+ }
+
+ const scrollableEls = [];
+ function walk(el, depth) {
+ if (depth > 8) return;
+ if (isScrollable(el)) {
+ scrollableEls.push({
+ tag: el.tagName,
+ id: el.id,
+ className: (el.className || '').substring(0, 60),
+ scrollHeight: el.scrollHeight,
+ clientHeight: el.clientHeight,
+ scrollTop: el.scrollTop,
+ overflow: window.getComputedStyle(el).overflow,
+ overflowY: window.getComputedStyle(el).overflowY,
+ });
+ }
+ for (const child of el.children) walk(child, depth + 1);
+ }
+ walk(document.documentElement, 0);
+
+ return {
+ bodyScrollHeight: document.body.scrollHeight,
+ bodyClientHeight: document.body.clientHeight,
+ bodyOverflow: window.getComputedStyle(document.body).overflow,
+ bodyOverflowY: window.getComputedStyle(document.body).overflowY,
+ htmlOverflow: window.getComputedStyle(document.documentElement).overflow,
+ htmlOverflowY: window.getComputedStyle(document.documentElement).overflowY,
+ windowScrollY: window.scrollY,
+ windowInnerHeight: window.innerHeight,
+ documentScrollHeight: document.documentElement.scrollHeight,
+ scrollableEls: scrollableEls.slice(0, 10),
+ };
+ });
+
+ console.log(` body: scrollHeight=${scrollInfo.bodyScrollHeight}, clientHeight=${scrollInfo.bodyClientHeight}`);
+ console.log(` body overflow: "${scrollInfo.bodyOverflow}", overflowY: "${scrollInfo.bodyOverflowY}"`);
+ console.log(` html overflow: "${scrollInfo.htmlOverflow}", overflowY: "${scrollInfo.htmlOverflowY}"`);
+ console.log(` document.documentElement.scrollHeight: ${scrollInfo.documentScrollHeight}`);
+ console.log(` window.innerHeight: ${scrollInfo.windowInnerHeight}, scrollY: ${scrollInfo.windowScrollY}`);
+ if (scrollInfo.scrollableEls.length > 0) {
+ console.log(' Scrollable elements found:');
+ scrollInfo.scrollableEls.forEach(el => {
+ console.log(` <${el.tag}> id="${el.id}" class="${el.className}"`);
+ console.log(` scrollHeight=${el.scrollHeight} clientHeight=${el.clientHeight} scrollTop=${el.scrollTop}`);
+ console.log(` overflow: "${el.overflow}" overflowY: "${el.overflowY}"`);
+ });
+ }
+
+ // ── 2. Boost pagination mode detection ───────────────────────────────────
+ console.log('\n[2] Boost pagination mode...');
+ const paginationInfo = await page.evaluate(() => {
+ // Look for load-more button
+ const loadMoreBtn = document.querySelector(
+ '.boost-sd__pagination-button--load-more, [class*="load-more"], [data-type="load-more"]'
+ );
+ // Look for infinite scroll sentinel
+ const infiniteSentinel = document.querySelector(
+ '[class*="infinite"], [class*="sentinel"], [class*="waypoint"]'
+ );
+ // Look for numbered pagination
+ const pageNumbers = document.querySelectorAll('.boost-sd__pagination-item, .boost-sd__pagination-number');
+ // Check Boost config
+ const boostConfig = window.boost_pf_config || window.boostConfig || window.boostSdConfig;
+
+ // Get the load-more button HTML
+ const loadMoreHTML = loadMoreBtn ? loadMoreBtn.outerHTML.substring(0, 300) : null;
+ const loadMoreVisible = loadMoreBtn ? window.getComputedStyle(loadMoreBtn).display !== 'none' : false;
+ const loadMoreRect = loadMoreBtn ? loadMoreBtn.getBoundingClientRect() : null;
+
+ return {
+ hasLoadMoreBtn: !!loadMoreBtn,
+ loadMoreBtnText: loadMoreBtn ? (loadMoreBtn.textContent || '').trim() : null,
+ loadMoreHTML,
+ loadMoreVisible,
+ loadMoreRect: loadMoreRect ? { top: loadMoreRect.top, bottom: loadMoreRect.bottom, height: loadMoreRect.height } : null,
+ hasInfiniteSentinel: !!infiniteSentinel,
+ pageNumberCount: pageNumbers.length,
+ boostConfigKeys: boostConfig ? Object.keys(boostConfig).slice(0, 20) : null,
+ // Try to read Boost's current pagination type from any global
+ boostPaginationType: (window.BOOST_SD || {}).paginationType || 'unknown',
+ };
+ });
+
+ console.log(` Has load-more button: ${paginationInfo.hasLoadMoreBtn}`);
+ if (paginationInfo.hasLoadMoreBtn) {
+ console.log(` Button text: "${paginationInfo.loadMoreBtnText}"`);
+ console.log(` Button visible: ${paginationInfo.loadMoreVisible}`);
+ console.log(` Button position: ${JSON.stringify(paginationInfo.loadMoreRect)}`);
+ console.log(` Button HTML: ${paginationInfo.loadMoreHTML}`);
+ }
+ console.log(` Has infinite-scroll sentinel: ${paginationInfo.hasInfiniteSentinel}`);
+ console.log(` Page number elements: ${paginationInfo.pageNumberCount}`);
+ console.log(` Boost pagination type: ${paginationInfo.boostPaginationType}`);
+ console.log(` Boost config keys: ${JSON.stringify(paginationInfo.boostConfigKeys)}`);
+
+ // ── 3. Try scrolling within the actual scrollable container ──────────────
+ console.log('\n[3] Attempting scroll on the correct scrollable container...');
+ // The body has "overflow: hidden auto" which means overflow-y: auto on body
+ // but maybe the main element or a wrapper is the scroll container
+ // Try scrolling the <main> element or the html element
+
+ const scrollAttempts = await page.evaluate(async () => {
+ const results = [];
+
+ // Try window.scrollTo first
+ const before = { scrollY: window.scrollY, docH: document.documentElement.scrollHeight };
+
+ window.scrollTo(0, 99999);
+ await new Promise(r => setTimeout(r, 100));
+ results.push({ method: 'window.scrollTo(0,99999)', scrollY: window.scrollY, docH: document.documentElement.scrollHeight });
+
+ // Try scrolling body
+ document.body.scrollTop = 99999;
+ await new Promise(r => setTimeout(r, 100));
+ results.push({ method: 'document.body.scrollTop=99999', bodyScrollTop: document.body.scrollTop });
+
+ // Try scrolling html
+ document.documentElement.scrollTop = 99999;
+ await new Promise(r => setTimeout(r, 100));
+ results.push({ method: 'document.documentElement.scrollTop=99999', scrollTop: document.documentElement.scrollTop });
+
+ // Find and scroll the actual overflowing container
+ function findScrollContainer() {
+ const candidates = document.querySelectorAll('main, #main, .main, [role="main"], #shopify-section-main, .boost-sd, .boost-sd__container');
+ for (const el of candidates) {
+ const style = window.getComputedStyle(el);
+ if (/auto|scroll/.test(style.overflowY) || /auto|scroll/.test(style.overflow)) {
+ return el;
+ }
+ }
+ return null;
+ }
+ const scrollEl = findScrollContainer();
+ if (scrollEl) {
+ scrollEl.scrollTop = 99999;
+ await new Promise(r => setTimeout(r, 100));
+ results.push({ method: `${scrollEl.tagName}#${scrollEl.id || ''}.${(scrollEl.className||'').substring(0,30)}.scrollTop=99999`, scrollTop: scrollEl.scrollTop, scrollHeight: scrollEl.scrollHeight });
+ }
+
+ return { before, results };
+ });
+
+ console.log(` Before: scrollY=${scrollAttempts.before.scrollY}, docH=${scrollAttempts.before.docH}`);
+ scrollAttempts.results.forEach(r => console.log(` ${r.method}: scrollY=${r.scrollY || r.bodyScrollTop || r.scrollTop || '?'}`));
+
+ // ── 4. Use page.evaluate to fire native scroll events ────────────────────
+ console.log('\n[4] Firing native scroll events + checking Boost load...');
+ const cardsBefore = await page.locator('.boost-sd__product-item').count();
+
+ // Try using Playwright's keyboard PageDown
+ await page.keyboard.press('End');
+ await page.waitForTimeout(2000);
+
+ const afterEnd = await page.evaluate(() => ({
+ scrollY: window.scrollY,
+ cardCount: document.querySelectorAll('.boost-sd__product-item').count || document.querySelectorAll('.boost-sd__product-item').length,
+ bodyScrollTop: document.body.scrollTop,
+ htmlScrollTop: document.documentElement.scrollTop,
+ }));
+ console.log(` After End key: scrollY=${afterEnd.scrollY}, bodyScrollTop=${afterEnd.bodyScrollTop}, htmlScrollTop=${afterEnd.htmlScrollTop}`);
+ const cardsAfterEnd = await page.locator('.boost-sd__product-item').count();
+ console.log(` Cards: ${cardsBefore} → ${cardsAfterEnd}`);
+
+ // Try clicking the load-more button directly
+ console.log('\n[5] Clicking load-more button directly...');
+ const loadMoreSel = '.boost-sd__pagination-button--load-more, [class*="load-more"]';
+ const loadMoreExists = await page.locator(loadMoreSel).count();
+ if (loadMoreExists > 0) {
+ console.log(` Found ${loadMoreExists} load-more button(s), clicking...`);
+ try {
+ await page.locator(loadMoreSel).first().scrollIntoViewIfNeeded();
+ await page.waitForTimeout(500);
+ await page.locator(loadMoreSel).first().click();
+ await page.waitForTimeout(3000);
+ const cardsAfterClick = await page.locator('.boost-sd__product-item').count();
+ console.log(` Cards after click: ${cardsBefore} → ${cardsAfterClick}`);
+
+ // Now count run() effect: new overlays
+ const overlayCount = await page.locator('.dw-hover-overlay').count();
+ console.log(` Overlays after click: ${overlayCount}`);
+
+ // Check for duplicates on new cards
+ const dupCheck = await page.evaluate(() => {
+ const cards = document.querySelectorAll('.boost-sd__product-item');
+ let dups = 0;
+ for (const card of cards) {
+ const insideOvs = card.querySelectorAll('.dw-hover-overlay').length;
+ // Check sibling overlays (v5 placement)
+ let sibOvs = 0;
+ const parent = card.parentElement;
+ if (parent) {
+ const children = Array.from(parent.children);
+ const idx = children.indexOf(card);
+ for (let i = idx + 1; i < children.length; i++) {
+ if (children[i].classList && children[i].classList.contains('dw-hover-overlay')) sibOvs++;
+ else break;
+ }
+ }
+ if (insideOvs + sibOvs > 1) dups++;
+ }
+ return { dups, totalCards: cards.length, totalOverlays: document.querySelectorAll('.dw-hover-overlay').length };
+ });
+ console.log(` After load-more click: ${dupCheck.totalCards} cards, ${dupCheck.totalOverlays} overlays, ${dupCheck.dups} with duplicates`);
+
+ // ── Now watch observer thrash during Boost load ──────────────────────
+ console.log('\n[6] Measuring observer thrash during load-more sequence...');
+ await page.evaluate(() => {
+ window.__mutCbSince = 0;
+ window.__runSince = 0;
+ const watcher = new MutationObserver((muts) => {
+ window.__mutCbSince++;
+ // Count how many nodes were added
+ for (const m of muts) window.__runSince += m.addedNodes.length;
+ });
+ watcher.observe(document.body, { childList: true, subtree: true });
+ window.__mutWatcher2 = watcher;
+ });
+
+ // Click load-more again if available
+ const loadMoreExists2 = await page.locator(loadMoreSel).count();
+ if (loadMoreExists2 > 0) {
+ const cardsBefore2 = await page.locator('.boost-sd__product-item').count();
+ await page.locator(loadMoreSel).first().scrollIntoViewIfNeeded();
+ await page.locator(loadMoreSel).first().click();
+ // Watch mutations fire in real-time for 5 seconds
+ for (let t = 0; t < 5; t++) {
+ await page.waitForTimeout(1000);
+ const snap = await page.evaluate(() => ({
+ mutCbs: window.__mutCbSince,
+ addedNodes: window.__runSince,
+ cards: document.querySelectorAll('.boost-sd__product-item').length,
+ overlays: document.querySelectorAll('.dw-hover-overlay').length,
+ }));
+ console.log(` t+${t+1}s: mutCbs=${snap.mutCbs} addedNodes=${snap.addedNodes} cards=${snap.cards} overlays=${snap.overlays}`);
+ }
+ const cardsAfter2 = await page.locator('.boost-sd__product-item').count();
+ const overlays2 = await page.locator('.dw-hover-overlay').count();
+ console.log(` Load #2: cards ${cardsBefore2} → ${cardsAfter2}, overlays: ${overlays2}`);
+
+ // Final duplicate check
+ const dupCheck2 = await page.evaluate(() => {
+ const cards = document.querySelectorAll('.boost-sd__product-item');
+ let dups = 0;
+ const dupList = [];
+ for (const card of cards) {
+ const insideOvs = card.querySelectorAll('.dw-hover-overlay').length;
+ let sibOvs = 0;
+ const parent = card.parentElement;
+ if (parent) {
+ const children = Array.from(parent.children);
+ const idx = children.indexOf(card);
+ for (let i = idx + 1; i < children.length; i++) {
+ if (children[i].classList && children[i].classList.contains('dw-hover-overlay')) sibOvs++;
+ else break;
+ }
+ }
+ if (insideOvs + sibOvs > 1) {
+ dups++;
+ dupList.push(`inside=${insideOvs} sib=${sibOvs}`);
+ }
+ }
+ return {
+ dups,
+ dupList: dupList.slice(0, 5),
+ totalCards: cards.length,
+ totalOverlays: document.querySelectorAll('.dw-hover-overlay').length,
+ };
+ });
+ console.log(` FINAL: ${dupCheck2.totalCards} cards, ${dupCheck2.totalOverlays} overlays, ${dupCheck2.dups} DUPLICATES`);
+ if (dupCheck2.dupList.length) console.log(` Dup breakdown: ${dupCheck2.dupList.join(', ')}`);
+ }
+ } catch (e) {
+ console.log(` Error clicking load-more: ${e.message}`);
+ }
+ } else {
+ console.log(' No load-more button found — checking for infinite scroll trigger element...');
+ const pageInfo = await page.evaluate(() => {
+ // Get the full pagination HTML
+ const pagination = document.querySelector('[class*="boost-sd__pagination"], [class*="pagination"]');
+ return {
+ paginationHTML: pagination ? pagination.outerHTML.substring(0, 800) : null,
+ paginationCount: document.querySelectorAll('[class*="boost-sd__pagination"]').length,
+ };
+ });
+ console.log(` Pagination elements: ${pageInfo.paginationCount}`);
+ if (pageInfo.paginationHTML) console.log(` Pagination HTML: ${pageInfo.paginationHTML}`);
+ }
+
+ // ── 7. Self-reinforcing loop test: inject fake card, watch cascade ────────
+ console.log('\n[7] Self-reinforcing loop test...');
+ const loopTest = await page.evaluate(() => {
+ // Reset counters
+ window.__loopMutCbs = 0;
+ window.__loopOverlayInserts = 0;
+
+ const loopWatcher = new MutationObserver((muts) => {
+ window.__loopMutCbs++;
+ for (const m of muts) {
+ for (const n of m.addedNodes) {
+ if (n.nodeType === 1 && n.classList && n.classList.contains('dw-hover-overlay')) {
+ window.__loopOverlayInserts++;
+ }
+ }
+ }
+ });
+ loopWatcher.observe(document.body, { childList: true, subtree: true });
+
+ // Find the product grid
+ const grid = document.querySelector('.boost-sd__product-list, .boost-sd__product-grid, [class*="product-grid"], [class*="product-list"]');
+ if (!grid) return { error: 'No grid found', gridCandidates: document.querySelectorAll('[class*="boost"]').length };
+
+ // Clone an existing card to simulate Boost appending
+ const existingCard = document.querySelector('.boost-sd__product-item');
+ if (!existingCard) return { error: 'No existing card found' };
+
+ const clonedCard = existingCard.cloneNode(true);
+ // Remove existing overlays from clone (simulate fresh card from Boost)
+ for (const ov of clonedCard.querySelectorAll('.dw-hover-overlay')) ov.remove();
+ // Remove __dwProcessed markers if any
+ clonedCard.removeAttribute('data-dw-hover');
+
+ const beforeCbs = window.__loopMutCbs;
+ const beforeInserts = window.__loopOverlayInserts;
+
+ // Append the fake card
+ grid.appendChild(clonedCard);
+
+ return new Promise(resolve => {
+ setTimeout(() => {
+ const afterCbs = window.__loopMutCbs - beforeCbs;
+ const afterInserts = window.__loopOverlayInserts - beforeInserts;
+ const allOverlays = document.querySelectorAll('.dw-hover-overlay').length;
+ const allCards = document.querySelectorAll('.boost-sd__product-item').length;
+
+ // Check if the cloned card got an overlay (even though it was cloned from one)
+ const cloneOverlays = clonedCard.querySelectorAll('.dw-hover-overlay').length;
+ let cloneSibOverlays = 0;
+ let sib = clonedCard.nextSibling;
+ while (sib) {
+ if (sib.nodeType === 1 && sib.classList && sib.classList.contains('dw-hover-overlay')) cloneSibOverlays++;
+ if (sib.nodeType === 1) break;
+ sib = sib.nextSibling;
+ }
+
+ resolve({
+ mutCbsFired: afterCbs,
+ overlayInserts: afterInserts,
+ totalCards: allCards,
+ totalOverlays: allOverlays,
+ cloneInsideOverlays: cloneOverlays,
+ cloneSiblingOverlays: cloneSibOverlays,
+ ratio: afterCbs > 0 ? (allCards / afterCbs).toFixed(2) : 'N/A',
+ });
+ }, 500); // wait 500ms for debounce (observer has 80ms debounce + run() time)
+ });
+ });
+ console.log(` After injecting 1 fake card into grid:`);
+ console.log(` MutObs callbacks fired: ${loopTest.mutCbsFired}`);
+ console.log(` Overlay insertions: ${loopTest.overlayInserts}`);
+ console.log(` Clone got inside overlay: ${loopTest.cloneInsideOverlays}`);
+ console.log(` Clone got sibling overlay: ${loopTest.cloneSiblingOverlays}`);
+ console.log(` Total cards: ${loopTest.totalCards}, total overlays: ${loopTest.totalOverlays}`);
+ if (loopTest.error) console.log(` Error: ${loopTest.error}`);
+
+ // ── 8. Check the guard condition in labelCard ─────────────────────────────
+ console.log('\n[8] Testing labelCard guard against v5 sibling placement...');
+ const guardTest = await page.evaluate(() => {
+ // v5 places the overlay as a SIBLING of the image-col, not inside the card
+ // The guard is: if (card.querySelector('.dw-hover-overlay')) return;
+ // If the overlay is placed OUTSIDE the card, this guard will FAIL to detect
+ // an existing overlay → run() will try to insert another one.
+
+ // Find a card and check where its overlay actually lives
+ const card = document.querySelector('.boost-sd__product-item');
+ if (!card) return { error: 'no card' };
+
+ const insideOverlays = card.querySelectorAll('.dw-hover-overlay').length;
+ const imgCol = card.querySelector('.boost-sd__product-item-grid-view-layout-image, .product-list-item-thumbnail');
+ const imgColOverlays = imgCol ? imgCol.querySelectorAll('.dw-hover-overlay').length : 'no imgCol';
+
+ // Check parent context
+ const parent = card.parentElement;
+ const parentTag = parent ? parent.tagName : 'none';
+ const parentClass = parent ? (parent.className || '').substring(0, 80) : '';
+ const parentChildren = parent ? Array.from(parent.children).map(c => c.tagName + (c.className ? '.' + (c.className||'').split(' ')[0] : '')).join(', ') : '';
+
+ // Where IS the overlay?
+ const allOverlays = document.querySelectorAll('.dw-hover-overlay');
+ let overlayParentClass = '';
+ if (allOverlays.length > 0) {
+ const ov = allOverlays[0];
+ overlayParentClass = (ov.parentElement?.className || '').substring(0, 80);
+ }
+
+ // Critical: does card.querySelector('.dw-hover-overlay') find the overlay?
+ const guardWorks = insideOverlays > 0;
+
+ return {
+ insideOverlays,
+ imgColOverlays,
+ parentTag,
+ parentClass,
+ parentChildrenSummary: parentChildren.substring(0, 200),
+ overlayParentClass,
+ guardWorks,
+ conclusion: guardWorks
+ ? 'GUARD WORKS: overlay is inside card, duplicates prevented'
+ : 'GUARD BROKEN: overlay is NOT inside card — run() cannot detect existing overlay!',
+ };
+ });
+
+ console.log(` Card inside overlays: ${guardTest.insideOverlays}`);
+ console.log(` imgCol inside overlays: ${guardTest.imgColOverlays}`);
+ console.log(` Overlay parent class: "${guardTest.overlayParentClass}"`);
+ console.log(` Card parent: <${guardTest.parentTag}> class="${guardTest.parentClass}"`);
+ console.log(` Parent children: ${guardTest.parentChildrenSummary}`);
+ console.log(` Guard works: ${guardTest.guardWorks}`);
+ console.log(` CONCLUSION: ${guardTest.conclusion}`);
+
+ // ── Boost API responses ───────────────────────────────────────────────────
+ console.log('\n[9] Boost API responses captured:');
+ boostApiResponses.forEach((r, i) => {
+ console.log(` [${i+1}] ${r.status} ${r.url}`);
+ console.log(` Response snippet: ${r.bodySnippet.substring(0, 300)}`);
+ });
+
+ if (consoleErrors.length > 0) {
+ console.log('\n[10] Console errors:');
+ consoleErrors.slice(0, 5).forEach((e, i) => console.log(` [${i+1}] ${e}`));
+ }
+
+ // ── 10. Check what the Boost filter page=6 actually returns ──────────────
+ console.log('\n[11] Checking collection size / page count...');
+ const collectionInfo = await page.evaluate(() => {
+ // Boost often exposes total count in DOM
+ const totalEl = document.querySelector('[class*="boost-sd__total"], [class*="product-count"], [data-total-products]');
+ const paginationInfo = document.querySelector('[class*="boost-sd__pagination"]');
+ const boostData = window.boostSdConfig || window.boost_pf_config || {};
+
+ return {
+ totalEl: totalEl ? totalEl.textContent.trim() : null,
+ paginationHTML: paginationInfo ? paginationInfo.outerHTML.substring(0, 600) : null,
+ boostDataKeys: Object.keys(boostData).slice(0, 20),
+ currentPage: boostData.page || 'unknown',
+ totalProducts: boostData.totalProducts || 'unknown',
+ };
+ });
+ console.log(` Total element text: ${collectionInfo.totalEl}`);
+ console.log(` Current page: ${collectionInfo.currentPage}`);
+ console.log(` Total products: ${collectionInfo.totalProducts}`);
+ console.log(` Boost data keys: ${JSON.stringify(collectionInfo.boostDataKeys)}`);
+ if (collectionInfo.paginationHTML) console.log(` Pagination HTML:\n ${collectionInfo.paginationHTML}`);
+
+ await browser.close();
+ console.log('\nDone.');
+})();
diff --git a/dw-scroll-debug3.js b/dw-scroll-debug3.js
new file mode 100644
index 0000000..707e8bc
--- /dev/null
+++ b/dw-scroll-debug3.js
@@ -0,0 +1,312 @@
+/**
+ * dw-scroll-debug3.js — Final targeted test:
+ *
+ * Key question from debug2:
+ * - body.scrollHeight == body.clientHeight == 2464 → the page is NOT taller than viewport
+ * because the Boost container uses INTERNAL scrolling within a fixed-height wrapper.
+ * - `body overflow: "hidden auto"` means the BODY itself is the scroll container.
+ * - window.scrollTo(0,99999) → scrollY=1564 (not 2464 - innerHeight = 1564) — so window IS the scroller,
+ * but scrollHeight is only 2464, meaning max scroll = 2464 - 900 = 1564. We're already at bottom.
+ *
+ * BUT the Boost button says "Load Previous Page" (not Next Page) and it's loading pages 5, 4...
+ * Boost is in INFINITE SCROLL mode that loads OLDER pages going UP, not down!
+ * The scroll trigger fires when user scrolls to the TOP (not bottom).
+ *
+ * Let me confirm: does scrolling to TOP trigger the infinite-scroll load?
+ * And measure the self-reinforcing observer loop during a real Boost append.
+ */
+
+const { chromium } = require('playwright');
+
+const TARGET_URL = 'https://www.designerwallcoverings.com/collections/decoglass-wallpaper?page=6';
+const VIEWPORT = { width: 1440, height: 900 };
+
+(async () => {
+ const browser = await chromium.launch({
+ headless: true,
+ args: ['--no-sandbox', '--disable-dev-shm-usage'],
+ });
+ const context = await browser.newContext({
+ viewport: VIEWPORT,
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
+ });
+ const page = await context.newPage();
+
+ const boostApiCalls = [];
+ page.on('request', req => {
+ if (req.url().includes('bc-sf-filter')) boostApiCalls.push(req.url().substring(0, 200));
+ });
+
+ console.log('=== DW Scroll Debug — Round 3: Infinite Scroll Direction + Observer Thrash ===\n');
+
+ await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+ await page.waitForSelector('.boost-sd__product-item', { timeout: 20000 });
+ await page.waitForTimeout(2000);
+
+ const initial = await page.evaluate(() => ({
+ cards: document.querySelectorAll('.boost-sd__product-item').length,
+ overlays: document.querySelectorAll('.dw-hover-overlay').length,
+ scrollY: window.scrollY,
+ docH: document.documentElement.scrollHeight,
+ winH: window.innerHeight,
+ maxScroll: document.documentElement.scrollHeight - window.innerHeight,
+ }));
+ console.log(`Initial state: ${initial.cards} cards, ${initial.overlays} overlays`);
+ console.log(`ScrollY: ${initial.scrollY}, docH: ${initial.docH}, winH: ${initial.winH}, maxScroll: ${initial.maxScroll}`);
+
+ // ── Set up detailed mutation observer BEFORE any interaction ──────────────
+ await page.evaluate(() => {
+ window.__phase = 'idle';
+ window.__phaseMutCbs = {};
+ window.__phaseAddedNodes = {};
+ window.__phaseOverlayInserts = {};
+
+ const tracker = new MutationObserver((muts) => {
+ const phase = window.__phase;
+ if (!window.__phaseMutCbs[phase]) {
+ window.__phaseMutCbs[phase] = 0;
+ window.__phaseAddedNodes[phase] = 0;
+ window.__phaseOverlayInserts[phase] = 0;
+ }
+ window.__phaseMutCbs[phase]++;
+ for (const m of muts) {
+ window.__phaseAddedNodes[phase] += m.addedNodes.length;
+ for (const n of m.addedNodes) {
+ if (n.nodeType === 1) {
+ if (n.classList && n.classList.contains('dw-hover-overlay')) {
+ window.__phaseOverlayInserts[phase]++;
+ }
+ if (n.querySelectorAll) {
+ window.__phaseOverlayInserts[phase] += n.querySelectorAll('.dw-hover-overlay').length;
+ }
+ }
+ }
+ }
+ });
+ tracker.observe(document.body, { childList: true, subtree: true });
+ window.__debugTracker = tracker;
+ });
+
+ // ── Test 1: Scroll to TOP → does Boost load previous pages? ──────────────
+ console.log('\n[TEST 1] Scroll to TOP — does Boost auto-load previous pages?');
+ const boostCallsBefore = boostApiCalls.length;
+ await page.evaluate(() => { window.__phase = 'scroll-to-top'; window.scrollTo(0, 0); });
+ await page.waitForTimeout(3000);
+
+ const afterScrollTop = await page.evaluate(() => ({
+ cards: document.querySelectorAll('.boost-sd__product-item').length,
+ overlays: document.querySelectorAll('.dw-hover-overlay').length,
+ scrollY: window.scrollY,
+ stats: { mutCbs: window.__phaseMutCbs, addedNodes: window.__phaseAddedNodes, overlayInserts: window.__phaseOverlayInserts },
+ }));
+ console.log(` After scroll to top: ${afterScrollTop.cards} cards, ${afterScrollTop.overlays} overlays, scrollY=${afterScrollTop.scrollY}`);
+ console.log(` New Boost API calls: ${boostApiCalls.length - boostCallsBefore}`);
+ console.log(` Observer stats: ${JSON.stringify(afterScrollTop.stats)}`);
+
+ // ── Test 2: Scroll to bottom — does Boost auto-load NEXT pages? ──────────
+ console.log('\n[TEST 2] Scroll to BOTTOM — does Boost auto-load next pages?');
+ await page.evaluate(() => { window.__phase = 'scroll-to-bottom'; window.scrollTo(0, 99999); });
+ await page.waitForTimeout(3000);
+
+ const afterScrollBottom = await page.evaluate(() => ({
+ cards: document.querySelectorAll('.boost-sd__product-item').length,
+ overlays: document.querySelectorAll('.dw-hover-overlay').length,
+ scrollY: window.scrollY,
+ stats: { mutCbs: window.__phaseMutCbs, addedNodes: window.__phaseAddedNodes, overlayInserts: window.__phaseOverlayInserts },
+ }));
+ console.log(` After scroll to bottom: ${afterScrollBottom.cards} cards, ${afterScrollBottom.overlays} overlays, scrollY=${afterScrollBottom.scrollY}`);
+ console.log(` New Boost API calls: ${boostApiCalls.length - boostCallsBefore - (afterScrollTop.cards - initial.cards > 0 ? 1 : 0)}`);
+ console.log(` Observer stats: ${JSON.stringify(afterScrollBottom.stats)}`);
+
+ // ── Test 3: Click load-more, watch mutation cascade closely ───────────────
+ console.log('\n[TEST 3] Click load-more — detailed observer cascade timing...');
+ const cardsBefore3 = await page.locator('.boost-sd__product-item').count();
+ const overlaysBefore3 = await page.locator('.dw-hover-overlay').count();
+ const boostBefore3 = boostApiCalls.length;
+
+ await page.evaluate(() => { window.__phase = 'load-more-click'; });
+ const loadMoreBtn = page.locator('.boost-sd__pagination-button--load-more');
+ await loadMoreBtn.scrollIntoViewIfNeeded();
+ await loadMoreBtn.click();
+
+ // Sample every 200ms for 4 seconds
+ const timeline = [];
+ for (let i = 0; i < 20; i++) {
+ await page.waitForTimeout(200);
+ const snap = await page.evaluate(() => ({
+ cards: document.querySelectorAll('.boost-sd__product-item').length,
+ overlays: document.querySelectorAll('.dw-hover-overlay').length,
+ // Current phase stats
+ mutCbs: window.__phaseMutCbs['load-more-click'] || 0,
+ addedNodes: window.__phaseAddedNodes['load-more-click'] || 0,
+ overlayInserts: window.__phaseOverlayInserts['load-more-click'] || 0,
+ }));
+ timeline.push(snap);
+ }
+
+ console.log(' ms | cards | overlays | mutCbs | addedNodes | ovInserts | ratio(mutCb:ov)');
+ console.log(' ----|-------|----------|--------|------------|-----------|---------------');
+ timeline.forEach((snap, i) => {
+ const ratio = snap.overlayInserts > 0 ? (snap.mutCbs / snap.overlayInserts).toFixed(1) : 'N/A';
+ console.log(` ${((i+1)*200).toString().padStart(4)} | ${snap.cards.toString().padStart(5)} | ${snap.overlays.toString().padStart(8)} | ${snap.mutCbs.toString().padStart(6)} | ${snap.addedNodes.toString().padStart(10)} | ${snap.overlayInserts.toString().padStart(9)} | ${ratio}`);
+ });
+
+ const finalSnap3 = timeline[timeline.length - 1];
+ console.log(`\n SUMMARY: cards ${cardsBefore3}→${finalSnap3.cards} (+${finalSnap3.cards - cardsBefore3})`);
+ console.log(` overlays ${overlaysBefore3}→${finalSnap3.overlays} (+${finalSnap3.overlays - overlaysBefore3})`);
+ console.log(` MutObs callbacks: ${finalSnap3.mutCbs}`);
+ console.log(` Added DOM nodes total: ${finalSnap3.addedNodes}`);
+ console.log(` Overlay insertions by run(): ${finalSnap3.overlayInserts}`);
+ if (finalSnap3.overlayInserts > 0) {
+ const wasted = finalSnap3.mutCbs - finalSnap3.overlayInserts;
+ const wastedPct = ((wasted / finalSnap3.mutCbs) * 100).toFixed(0);
+ console.log(` Wasted run() calls (no new overlays to insert): ${wasted} (${wastedPct}% of callbacks)`);
+ console.log(` Each overlay insertion triggers ${(finalSnap3.mutCbs / finalSnap3.overlayInserts).toFixed(1)}x observer callbacks`);
+ }
+ console.log(` Boost API calls during this: ${boostApiCalls.length - boostBefore3}`);
+
+ // ── Test 4: Duplicate overlay check after load-more ──────────────────────
+ console.log('\n[TEST 4] Duplicate overlay analysis after load-more...');
+ const dupAnalysis = await page.evaluate(() => {
+ const cards = document.querySelectorAll('.boost-sd__product-item');
+ let noOverlay = 0, singleOverlay = 0, multiOverlay = 0;
+ const dupDetails = [];
+
+ for (const card of cards) {
+ // The overlay is placed INSIDE the image-wrapper (not as a sibling of the card)
+ // Per debug2: overlayParentClass includes "boost-sd__product-image-wrapper"
+ const insideCard = card.querySelectorAll('.dw-hover-overlay').length;
+ if (insideCard === 0) noOverlay++;
+ else if (insideCard === 1) singleOverlay++;
+ else {
+ multiOverlay++;
+ dupDetails.push({
+ title: (card.querySelector('.boost-sd__product-title')?.textContent || '').trim().substring(0, 40),
+ count: insideCard,
+ });
+ }
+ }
+
+ // Also check for orphan overlays not inside any card
+ const allOverlays = document.querySelectorAll('.dw-hover-overlay').length;
+ const orphans = allOverlays - singleOverlay - (multiOverlay > 0 ? multiOverlay * 2 : 0); // rough
+
+ return { noOverlay, singleOverlay, multiOverlay, dupDetails: dupDetails.slice(0, 5), totalCards: cards.length, allOverlays };
+ });
+ console.log(` Cards: ${dupAnalysis.totalCards} total, ${dupAnalysis.singleOverlay} clean, ${dupAnalysis.noOverlay} missing overlay, ${dupAnalysis.multiOverlay} DUPLICATED`);
+ console.log(` Total overlays in DOM: ${dupAnalysis.allOverlays}`);
+ if (dupAnalysis.dupDetails.length > 0) {
+ console.log(` Duplicated cards:`);
+ dupAnalysis.dupDetails.forEach(d => console.log(` "${d.title}": ${d.count} overlays`));
+ }
+
+ // ── Test 5: The REAL scroll glitch — infinite scroll direction mismatch ───
+ console.log('\n[TEST 5] Boost pagination analysis — page direction...');
+ const pageAnalysis = await page.evaluate(() => {
+ // Boost is configured for "infinite scroll" but with a page=6 URL:
+ // - "Load Previous Page" button means Boost loads OLDER products (page 5, 4...)
+ // - The "next page" / bottom trigger would load page 7+
+ // - decoglass-wallpaper has 215 total products, 24/page = ~9 pages
+ // - page=6 has 24 products (pages 6 of ~9), so there IS a next page
+
+ const pagination = document.querySelector('.boost-sd__pagination-infinite-scroll-container');
+ const allButtons = pagination ? Array.from(pagination.querySelectorAll('button')).map(b => ({
+ text: b.textContent.trim(),
+ class: b.className,
+ disabled: b.disabled,
+ })) : [];
+
+ // Check for a "load next" or scroll-to-bottom sentinel
+ const scrollSentinel = document.querySelector('[class*="boost-sd__sentinel"], [class*="infinite-scroll-sentinel"]');
+
+ // Check page URL parameters
+ const urlParams = new URLSearchParams(window.location.search);
+
+ return {
+ currentPageParam: urlParams.get('page'),
+ allPaginationButtons: allButtons,
+ hasScrollSentinel: !!scrollSentinel,
+ sentinelClass: scrollSentinel?.className,
+ paginationHTML: pagination?.outerHTML?.substring(0, 1000),
+ hasInfiniteScrollContainer: !!pagination,
+ };
+ });
+ console.log(` Current page param: ${pageAnalysis.currentPageParam}`);
+ console.log(` Pagination buttons: ${JSON.stringify(pageAnalysis.allPaginationButtons)}`);
+ console.log(` Has scroll sentinel: ${pageAnalysis.hasScrollSentinel}, class: ${pageAnalysis.sentinelClass}`);
+ console.log(` Pagination container HTML:\n ${pageAnalysis.paginationHTML}`);
+
+ // ── Test 6: Try triggering bottom-page infinite scroll (Boost next-page) ──
+ console.log('\n[TEST 6] Trying to trigger NEXT-page infinite scroll (scroll to absolute bottom)...');
+ const boostBefore6 = boostApiCalls.length;
+ // Scroll in small increments to ensure scroll events fire
+ await page.evaluate(async () => {
+ const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
+ for (let y = window.scrollY; y <= maxScroll; y += 50) {
+ window.scrollTo(0, y);
+ await new Promise(r => setTimeout(r, 20));
+ }
+ window.scrollTo(0, maxScroll);
+ });
+ await page.waitForTimeout(3000);
+
+ const afterBottom6 = await page.evaluate(() => ({
+ cards: document.querySelectorAll('.boost-sd__product-item').length,
+ overlays: document.querySelectorAll('.dw-hover-overlay').length,
+ scrollY: window.scrollY,
+ docH: document.documentElement.scrollHeight,
+ }));
+ console.log(` After gradual scroll to bottom: ${afterBottom6.cards} cards, ${afterBottom6.overlays} overlays`);
+ console.log(` scrollY=${afterBottom6.scrollY}, docH=${afterBottom6.docH}`);
+ console.log(` New Boost API calls: ${boostApiCalls.length - boostBefore6}`);
+ if (boostApiCalls.length > boostBefore6) {
+ console.log(` New calls: ${boostApiCalls.slice(boostBefore6).join('\n ')}`);
+ }
+
+ // ── Test 7: What's blocking infinite scroll trigger at bottom? ────────────
+ console.log('\n[TEST 7] Checking Boost infinite scroll sentinel/waypoint position...');
+ const sentinelInfo = await page.evaluate(() => {
+ // Boost 2.1.17 uses an IntersectionObserver for its infinite scroll sentinel
+ // Let's find ALL elements with "sentinel", "waypoint", "trigger" in their class
+ const candidates = Array.from(document.querySelectorAll('*')).filter(el => {
+ const cls = (el.className || '').toString();
+ return cls.includes('sentinel') || cls.includes('waypoint') || cls.includes('infinite-trigger') || cls.includes('load-next');
+ });
+
+ // Also look for data attributes
+ const dataEls = Array.from(document.querySelectorAll('[data-infinite], [data-sentinel], [data-load-more]'));
+
+ // Check scroll position relative to viewport
+ const all = [...candidates, ...dataEls].slice(0, 5);
+ return {
+ count: candidates.length + dataEls.length,
+ elements: all.map(el => ({
+ tag: el.tagName,
+ class: (el.className || '').toString().substring(0, 80),
+ rect: el.getBoundingClientRect(),
+ dataAttrs: Object.fromEntries(Array.from(el.attributes).filter(a => a.name.startsWith('data-')).map(a => [a.name, a.value.substring(0, 50)])),
+ })),
+ };
+ });
+ console.log(` Sentinel/waypoint elements found: ${sentinelInfo.count}`);
+ sentinelInfo.elements.forEach((el, i) => {
+ console.log(` [${i+1}] <${el.tag}> class="${el.class}"`);
+ console.log(` rect: ${JSON.stringify(el.rect)}`);
+ console.log(` data: ${JSON.stringify(el.dataAttrs)}`);
+ });
+
+ // ── Summary ───────────────────────────────────────────────────────────────
+ console.log('\n=== COMPREHENSIVE ROOT CAUSE SUMMARY ===');
+
+ const totalBoostCalls = boostApiCalls.length;
+ console.log(`Total Boost API calls during entire session: ${totalBoostCalls}`);
+ console.log('\nBoost API calls breakdown:');
+ boostApiCalls.forEach((url, i) => {
+ const pageMatch = url.match(/page=(\d+)/);
+ const page = pageMatch ? pageMatch[1] : '?';
+ console.log(` [${i+1}] page=${page}: ${url.substring(0, 100)}`);
+ });
+
+ await browser.close();
+ console.log('\nDone.');
+})();
diff --git a/dw-scroll-verify.js b/dw-scroll-verify.js
new file mode 100644
index 0000000..4cecbe7
--- /dev/null
+++ b/dw-scroll-verify.js
@@ -0,0 +1,201 @@
+/**
+ * dw-scroll-verify.js — Verify the v6 fix works correctly.
+ * Injects the fixed dw-card-hover.js logic and verifies:
+ * 1. No duplicate overlays after load-more
+ * 2. Observer fires only on product-node mutations, not overlay insertions
+ * 3. Infinite scroll loads subsequent pages correctly
+ */
+
+const { chromium } = require('playwright');
+const fs = require('fs');
+const path = require('path');
+
+const TARGET_URL = 'https://www.designerwallcoverings.com/collections/decoglass-wallpaper?page=6';
+const FIXED_JS_PATH = '/Users/macstudio3/Projects/Designer-Wallcoverings/shopify/theme-LIVE-591/assets/dw-card-hover.js';
+const VIEWPORT = { width: 1440, height: 900 };
+
+(async () => {
+ const fixedJs = fs.readFileSync(FIXED_JS_PATH, 'utf8');
+
+ const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] });
+ const context = await browser.newContext({
+ viewport: VIEWPORT,
+ userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
+ });
+
+ // Intercept and replace the live dw-card-hover.js with the fixed version
+ await context.route('**/assets/dw-card-hover.js*', async (route) => {
+ route.fulfill({ status: 200, contentType: 'application/javascript', body: fixedJs });
+ });
+
+ const page = await context.newPage();
+ const boostApiCalls = [];
+ page.on('request', req => { if (req.url().includes('bc-sf-filter')) boostApiCalls.push(req.url()); });
+
+ console.log('=== DW Scroll Verify — v6 Fix Validation ===\n');
+ console.log('Injecting fixed dw-card-hover.js via route interception...\n');
+
+ await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+ await page.waitForSelector('.boost-sd__product-item', { timeout: 20000 });
+ await page.waitForTimeout(2000);
+
+ const initial = await page.evaluate(() => ({
+ cards: document.querySelectorAll('.boost-sd__product-item').length,
+ overlays: document.querySelectorAll('.dw-hover-overlay').length,
+ dwHoverActive: window.__dwCardHover,
+ processedCards: document.querySelectorAll('[data-dw-hover]').length,
+ }));
+ console.log(`Initial: ${initial.cards} cards, ${initial.overlays} overlays, processed=${initial.processedCards}, dwHoverActive=${initial.dwHoverActive}`);
+
+ // Set up mutation tracking
+ await page.evaluate(() => {
+ window.__v6ObsCallbacks = 0;
+ window.__v6OverlayTriggeredCallbacks = 0;
+ window.__v6ProductTriggeredCallbacks = 0;
+ const tracker = new MutationObserver((muts) => {
+ window.__v6ObsCallbacks++;
+ let hasOverlay = false, hasProduct = false;
+ for (const m of muts) {
+ for (const n of m.addedNodes) {
+ if (n.nodeType !== 1) continue;
+ if (n.classList && n.classList.contains('dw-hover-overlay')) hasOverlay = true;
+ if (n.classList && (n.classList.contains('boost-sd__product-item') || n.classList.contains('product-list-item'))) hasProduct = true;
+ }
+ }
+ if (hasOverlay) window.__v6OverlayTriggeredCallbacks++;
+ if (hasProduct) window.__v6ProductTriggeredCallbacks++;
+ });
+ tracker.observe(document.body, { childList: true, subtree: true });
+ });
+
+ // Click load-more 3 times and verify
+ let totalLoadMoreClicks = 0;
+ for (let round = 0; round < 3; round++) {
+ const btnSel = '.boost-sd__pagination-button--load-more';
+ const btnExists = await page.locator(btnSel).count();
+ if (!btnExists) { console.log(` Round ${round+1}: no load-more button, stopping`); break; }
+
+ const cardsBefore = await page.locator('.boost-sd__product-item').count();
+ const cbsBefore = await page.evaluate(() => window.__v6ObsCallbacks);
+
+ await page.locator(btnSel).first().scrollIntoViewIfNeeded();
+ await page.locator(btnSel).first().click();
+ await page.waitForTimeout(2500);
+ totalLoadMoreClicks++;
+
+ const snap = await page.evaluate(() => {
+ const cards = document.querySelectorAll('.boost-sd__product-item');
+ let dups = 0, missing = 0, processedCount = 0;
+ for (const card of cards) {
+ const insideOvs = card.querySelectorAll('.dw-hover-overlay').length;
+ const isProcessed = card.dataset.dwHover === '1';
+ if (isProcessed) processedCount++;
+ if (insideOvs > 1) dups++;
+ if (insideOvs === 0 && !isProcessed) missing++;
+ }
+ return {
+ cards: cards.length,
+ overlays: document.querySelectorAll('.dw-hover-overlay').length,
+ dups,
+ missing,
+ processedCount,
+ obsCbs: window.__v6ObsCallbacks,
+ overlayTriggered: window.__v6OverlayTriggeredCallbacks,
+ productTriggered: window.__v6ProductTriggeredCallbacks,
+ };
+ });
+
+ const newCbs = snap.obsCbs - cbsBefore;
+ console.log(`Round ${round+1}: cards=${cardsBefore}→${snap.cards} (+${snap.cards - cardsBefore}), overlays=${snap.overlays}, dups=${snap.dups}, missing=${snap.missing}`);
+ console.log(` Observer: +${newCbs} callbacks this round (overlay-triggered=${snap.overlayTriggered}, product-triggered=${snap.productTriggered})`);
+ console.log(` data-dw-hover processed: ${snap.processedCount}/${snap.cards}`);
+
+ if (snap.dups > 0) console.log(` !!! FAIL: ${snap.dups} cards with duplicate overlays`);
+ else console.log(` PASS: no duplicate overlays`);
+ }
+
+ // Test: inject fake card and verify observer does NOT fire run() for overlay mutations
+ console.log('\n[Self-loop test] Inject card, verify observer fires selectively...');
+ const loopTest = await page.evaluate(() => {
+ window.__v6ObsCallbacks = 0;
+ window.__v6OverlayTriggeredCallbacks = 0;
+
+ const grid = document.querySelector('.boost-sd__product-list');
+ const existingCard = document.querySelector('.boost-sd__product-item');
+ if (!grid || !existingCard) return { error: 'no grid/card' };
+
+ const clone = existingCard.cloneNode(true);
+ // Remove processed flag and overlays to simulate fresh card
+ delete clone.dataset.dwHover;
+ for (const ov of clone.querySelectorAll('.dw-hover-overlay')) ov.remove();
+
+ grid.appendChild(clone);
+
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve({
+ obsCbsAfter: window.__v6ObsCallbacks,
+ overlayTriggered: window.__v6OverlayTriggeredCallbacks,
+ totalCards: document.querySelectorAll('.boost-sd__product-item').length,
+ totalOverlays: document.querySelectorAll('.dw-hover-overlay').length,
+ cloneProcessed: clone.dataset.dwHover === '1',
+ cloneInsideOvs: clone.querySelectorAll('.dw-hover-overlay').length,
+ });
+ }, 600);
+ });
+ });
+
+ if (loopTest.error) {
+ console.log(` Error: ${loopTest.error}`);
+ } else {
+ console.log(` After inject: cards=${loopTest.totalCards}, overlays=${loopTest.totalOverlays}`);
+ console.log(` Observer callbacks: ${loopTest.obsCbsAfter} (overlay-triggered: ${loopTest.overlayTriggered})`);
+ console.log(` Clone processed (data-dw-hover=1): ${loopTest.cloneProcessed}`);
+ console.log(` Clone inside overlays: ${loopTest.cloneInsideOvs}`);
+
+ if (loopTest.overlayTriggered === 0) {
+ console.log(' PASS: overlay insertions did NOT trigger observer callbacks (self-loop eliminated)');
+ } else {
+ console.log(` WARN: ${loopTest.overlayTriggered} overlay-triggered callbacks (self-loop may still exist)`);
+ }
+ if (loopTest.cloneProcessed) {
+ console.log(' PASS: clone card was labeled and marked as processed');
+ } else {
+ console.log(' FAIL: clone card was NOT processed by run()');
+ }
+ }
+
+ // Final scroll test: does infinite scroll to bottom still trigger page 7/8?
+ console.log('\n[Scroll test] Verify bottom-scroll still triggers Boost next-page load...');
+ const boostBefore = boostApiCalls.length;
+ await page.evaluate(async () => {
+ const max = document.documentElement.scrollHeight - window.innerHeight;
+ for (let y = window.scrollY; y <= max; y += 100) {
+ window.scrollTo(0, y);
+ await new Promise(r => setTimeout(r, 15));
+ }
+ });
+ await page.waitForTimeout(3000);
+ const newBoostCalls = boostApiCalls.slice(boostBefore);
+ console.log(` New Boost API calls after scrolling to bottom: ${newBoostCalls.length}`);
+ newBoostCalls.forEach((url, i) => {
+ const m = url.match(/page=(\d+)/);
+ console.log(` [${i+1}] page=${m?.[1]||'?'}: ${url.substring(0, 100)}`);
+ });
+
+ const finalState = await page.evaluate(() => ({
+ cards: document.querySelectorAll('.boost-sd__product-item').length,
+ overlays: document.querySelectorAll('.dw-hover-overlay').length,
+ dups: Array.from(document.querySelectorAll('.boost-sd__product-item')).filter(c => c.querySelectorAll('.dw-hover-overlay').length > 1).length,
+ }));
+ console.log(`\nFinal state: ${finalState.cards} cards, ${finalState.overlays} overlays, ${finalState.dups} with duplicate overlays`);
+
+ console.log('\n=== VERIFICATION SUMMARY ===');
+ console.log(`Guard fix (data-dw-hover attribute): ${loopTest.cloneProcessed ? 'WORKING' : 'NEEDS CHECK'}`);
+ console.log(`Self-loop elimination: ${loopTest.overlayTriggered === 0 ? 'CONFIRMED — overlay mutations no longer trigger observer' : 'PARTIAL — some overlay triggers remain'}`);
+ console.log(`Infinite scroll integration: ${newBoostCalls.length > 0 ? `WORKING — ${newBoostCalls.length} new page loads triggered` : 'NO NEW LOADS (may be at end of collection or scroll not reaching sentinel)'}`);
+ console.log(`Duplicate overlays at end: ${finalState.dups === 0 ? 'NONE — clean' : finalState.dups + ' DUPLICATES — issue persists'}`);
+
+ await browser.close();
+ console.log('\nDone.');
+})();
← 2b76a66 chore: refactor — rename GOOGLE→GOOGLE_YOUTUBE for clarity (
·
back to Secrets Manager
·
auto-save: 2026-07-22T14:44:58 (2 files) — registry.json rou 4604d18 →