← back to Secrets Manager
dw-scroll-debug3.js
313 lines
/**
* 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.');
})();