← 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.');
})();