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