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