/ tests / capture / dom-crop-analyzer-evaluate.test.js
dom-crop-analyzer-evaluate.test.js
   1  /**
   2   * Coverage tests for dom-crop-analyzer.js using DOM-aware page.evaluate mock
   3   *
   4   * The key insight: page.evaluate(fn, args) normally sends fn to the browser.
   5   * To get V8 coverage on lines 23-242, we must actually CALL fn in Node.js
   6   * context with a fake document/window injected into globalThis.
   7   *
   8   * This mirrors the pattern from tests/browser-notifications.test.js.
   9   */
  10  
  11  import { test, describe, mock } from 'node:test';
  12  import assert from 'node:assert/strict';
  13  import { analyzeCropBoundaries } from '../../src/utils/dom-crop-analyzer.js';
  14  
  15  // ---------------------------------------------------------------------------
  16  // Fake DOM helpers
  17  // ---------------------------------------------------------------------------
  18  
  19  /**
  20   * Build a fake document with configurable nav and main content elements.
  21   */
  22  function buildFakeDocument({
  23    navElements = [],
  24    mainElement = null,
  25    querySelectorAllResults = {},
  26    querySelectorResults = {},
  27  } = {}) {
  28    return {
  29      querySelectorAll: selector => {
  30        if (selector in querySelectorAllResults) return querySelectorAllResults[selector];
  31        // Check nav elements
  32        const navSelectors = [
  33          'nav',
  34          'header',
  35          '[role="navigation"]',
  36          '[role="banner"]',
  37          '.navbar',
  38          '.header',
  39          '.nav',
  40          '.navigation',
  41          '#header',
  42          '#navbar',
  43          '#navigation',
  44        ];
  45        if (navSelectors.includes(selector)) return navElements;
  46        return [];
  47      },
  48      querySelector: selector => {
  49        if (selector in querySelectorResults) return querySelectorResults[selector];
  50        // Check main content selectors
  51        const mainSelectors = [
  52          'main',
  53          '[role="main"]',
  54          '.container',
  55          '.content',
  56          '.main-content',
  57          '#main',
  58          '#content',
  59          '.wrapper',
  60          '.page-wrapper',
  61        ];
  62        if (mainSelectors.includes(selector) && mainElement) return mainElement;
  63        return null;
  64      },
  65      body: { querySelectorAll: () => [] },
  66      documentElement: { scrollHeight: 1200, clientHeight: 800 },
  67    };
  68  }
  69  
  70  /**
  71   * Build a fake window object.
  72   */
  73  function buildFakeWindow({
  74    innerHeight = 1080,
  75    innerWidth = 1920,
  76    computedStyles = new Map(),
  77  } = {}) {
  78    return {
  79      innerHeight,
  80      innerWidth,
  81      getComputedStyle: el => {
  82        if (computedStyles.has(el)) return computedStyles.get(el);
  83        return (
  84          el._style || {
  85            display: 'block',
  86            visibility: 'visible',
  87            opacity: '1',
  88            position: 'static',
  89          }
  90        );
  91      },
  92    };
  93  }
  94  
  95  /**
  96   * Create a DOM-aware page mock that actually calls the evaluate callback
  97   * with fake document/window injected into globalThis.
  98   */
  99  function createDOMPage({ fakeDoc, fakeWin }) {
 100    return {
 101      evaluate: mock.fn(async (fn, args) => {
 102        const prevDoc = globalThis.document;
 103        const prevWin = globalThis.window;
 104        globalThis.document = fakeDoc;
 105        globalThis.window = fakeWin;
 106        try {
 107          return args !== undefined ? fn(args) : fn();
 108        } finally {
 109          if (prevDoc === undefined) delete globalThis.document;
 110          else globalThis.document = prevDoc;
 111          if (prevWin === undefined) delete globalThis.window;
 112          else globalThis.window = prevWin;
 113        }
 114      }),
 115      viewportSize: () => ({ width: fakeWin.innerWidth, height: fakeWin.innerHeight }),
 116    };
 117  }
 118  
 119  // ---------------------------------------------------------------------------
 120  // Tests: No navigation, no main content (baseline)
 121  // ---------------------------------------------------------------------------
 122  
 123  describe('analyzeCropBoundaries with DOM-aware evaluate mock', () => {
 124    test('returns zero crop when no DOM elements detected', async () => {
 125      const fakeDoc = buildFakeDocument();
 126      const fakeWin = buildFakeWindow();
 127      const page = createDOMPage({ fakeDoc, fakeWin });
 128  
 129      const result = await analyzeCropBoundaries(page, 'desktop_above');
 130  
 131      assert.strictEqual(result.topCrop, 0);
 132      assert.strictEqual(result.leftCrop, 0);
 133      assert.strictEqual(result.rightCrop, 0);
 134      assert.strictEqual(result.metadata.hadNav, false);
 135    });
 136  
 137    test('page.evaluate is actually called (not just mocked)', async () => {
 138      const fakeDoc = buildFakeDocument();
 139      const fakeWin = buildFakeWindow();
 140      const page = createDOMPage({ fakeDoc, fakeWin });
 141  
 142      await analyzeCropBoundaries(page, 'desktop_above');
 143  
 144      assert.strictEqual(page.evaluate.mock.callCount(), 1);
 145    });
 146  });
 147  
 148  // ---------------------------------------------------------------------------
 149  // Tests: Nav detection and cropping logic (lines 25-166)
 150  // ---------------------------------------------------------------------------
 151  
 152  describe('Nav detection via real evaluate callback', () => {
 153    test('detects nav element in upper half and crops when sticky, no CTAs', async () => {
 154      const navEl = {
 155        getBoundingClientRect: () => ({
 156          top: 0,
 157          bottom: 80,
 158          height: 80,
 159          width: 1920,
 160          left: 0,
 161          right: 1920,
 162        }),
 163        querySelector: () => null, // no CTAs or trust signals
 164        _style: { position: 'fixed', display: 'block', visibility: 'visible', opacity: '1' },
 165      };
 166  
 167      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 168      const fakeWin = buildFakeWindow({
 169        innerHeight: 1080,
 170        innerWidth: 1920,
 171        computedStyles: new Map([[navEl, { position: 'fixed', display: 'block' }]]),
 172      });
 173      const page = createDOMPage({ fakeDoc, fakeWin });
 174  
 175      const result = await analyzeCropBoundaries(page, 'desktop_above');
 176  
 177      assert.strictEqual(result.topCrop, 80);
 178      assert.strictEqual(result.metadata.navCropped, true);
 179      assert.match(result.metadata.navReasoning, /Cropped.*sticky nav/);
 180    });
 181  
 182    test('preserves nav when it contains a visible CTA button', async () => {
 183      // CTA button with visible dimensions
 184      const ctaEl = {
 185        getBoundingClientRect: () => ({
 186          top: 10,
 187          bottom: 50,
 188          height: 40,
 189          width: 120,
 190          left: 1700,
 191          right: 1820,
 192        }),
 193        _style: { display: 'block', visibility: 'visible', opacity: '1' },
 194      };
 195  
 196      const navEl = {
 197        getBoundingClientRect: () => ({
 198          top: 0,
 199          bottom: 80,
 200          height: 80,
 201          width: 1920,
 202          left: 0,
 203          right: 1920,
 204        }),
 205        querySelector: selector => {
 206          // CTA selectors include 'button'
 207          if (selector === 'button' || selector.includes('btn') || selector.includes('cta'))
 208            return ctaEl;
 209          return null;
 210        },
 211        _style: { position: 'fixed', display: 'block', visibility: 'visible', opacity: '1' },
 212      };
 213  
 214      const computedStyles = new Map([
 215        [navEl, { position: 'fixed', display: 'block' }],
 216        [ctaEl, { display: 'block', visibility: 'visible', opacity: '1' }],
 217      ]);
 218  
 219      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 220      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920, computedStyles });
 221      const page = createDOMPage({ fakeDoc, fakeWin });
 222  
 223      const result = await analyzeCropBoundaries(page, 'desktop_above');
 224  
 225      assert.strictEqual(result.topCrop, 0);
 226      assert.strictEqual(result.metadata.navCropped, false);
 227      assert.match(result.metadata.navReasoning, /contains CTAs/);
 228    });
 229  
 230    test('preserves nav when not sticky (position: static)', async () => {
 231      const navEl = {
 232        getBoundingClientRect: () => ({
 233          top: 0,
 234          bottom: 80,
 235          height: 80,
 236          width: 1920,
 237          left: 0,
 238          right: 1920,
 239        }),
 240        querySelector: () => null,
 241        _style: { position: 'static', display: 'block' },
 242      };
 243  
 244      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 245      const fakeWin = buildFakeWindow({
 246        innerHeight: 1080,
 247        innerWidth: 1920,
 248        computedStyles: new Map([[navEl, { position: 'static', display: 'block' }]]),
 249      });
 250      const page = createDOMPage({ fakeDoc, fakeWin });
 251  
 252      const result = await analyzeCropBoundaries(page, 'desktop_above');
 253  
 254      assert.strictEqual(result.topCrop, 0);
 255      assert.strictEqual(result.metadata.navCropped, false);
 256      assert.match(result.metadata.navReasoning, /not sticky/);
 257    });
 258  
 259    test('preserves nav when too large (>30% of viewport height)', async () => {
 260      const navEl = {
 261        getBoundingClientRect: () => ({
 262          top: 0,
 263          bottom: 400,
 264          height: 400,
 265          width: 1920,
 266          left: 0,
 267          right: 1920,
 268        }),
 269        querySelector: () => null,
 270        _style: { position: 'fixed', display: 'block' },
 271      };
 272  
 273      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 274      const fakeWin = buildFakeWindow({
 275        innerHeight: 1080,
 276        innerWidth: 1920,
 277        computedStyles: new Map([[navEl, { position: 'fixed', display: 'block' }]]),
 278      });
 279      const page = createDOMPage({ fakeDoc, fakeWin });
 280  
 281      const result = await analyzeCropBoundaries(page, 'desktop_above');
 282  
 283      assert.strictEqual(result.topCrop, 0);
 284      assert.match(result.metadata.navReasoning, /too large/);
 285    });
 286  
 287    test('ignores nav element below viewport midpoint', async () => {
 288      const navEl = {
 289        getBoundingClientRect: () => ({
 290          top: 700,
 291          bottom: 780,
 292          height: 80,
 293          width: 1920,
 294          left: 0,
 295          right: 1920,
 296        }),
 297        querySelector: () => null,
 298        _style: { position: 'fixed', display: 'block' },
 299      };
 300  
 301      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 302      const fakeWin = buildFakeWindow({
 303        innerHeight: 1080,
 304        innerWidth: 1920,
 305        computedStyles: new Map([[navEl, { position: 'fixed', display: 'block' }]]),
 306      });
 307      const page = createDOMPage({ fakeDoc, fakeWin });
 308  
 309      const result = await analyzeCropBoundaries(page, 'desktop_above');
 310  
 311      assert.strictEqual(result.topCrop, 0);
 312      assert.match(result.metadata.navReasoning, /No navigation detected/);
 313    });
 314  
 315    test('handles zero-height nav elements (hidden nav)', async () => {
 316      const navEl = {
 317        getBoundingClientRect: () => ({
 318          top: 0,
 319          bottom: 0,
 320          height: 0,
 321          width: 1920,
 322          left: 0,
 323          right: 1920,
 324        }),
 325        querySelector: () => null,
 326        _style: { position: 'fixed', display: 'block' },
 327      };
 328  
 329      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 330      const fakeWin = buildFakeWindow({
 331        innerHeight: 1080,
 332        innerWidth: 1920,
 333        computedStyles: new Map([[navEl, { position: 'fixed', display: 'block' }]]),
 334      });
 335      const page = createDOMPage({ fakeDoc, fakeWin });
 336  
 337      const result = await analyzeCropBoundaries(page, 'desktop_above');
 338  
 339      // Zero-height nav should not be primary nav (area=0, height condition fails)
 340      assert.strictEqual(result.topCrop, 0);
 341    });
 342  
 343    test('selects largest nav when multiple nav elements present', async () => {
 344      const smallNav = {
 345        getBoundingClientRect: () => ({
 346          top: 0,
 347          bottom: 40,
 348          height: 40,
 349          width: 200,
 350          left: 0,
 351          right: 200,
 352        }),
 353        querySelector: () => null,
 354        _style: { position: 'fixed', display: 'block' },
 355      };
 356      const largeNav = {
 357        getBoundingClientRect: () => ({
 358          top: 0,
 359          bottom: 100,
 360          height: 100,
 361          width: 1920,
 362          left: 0,
 363          right: 1920,
 364        }),
 365        querySelector: () => null,
 366        _style: { position: 'sticky', display: 'block' },
 367      };
 368  
 369      const fakeDoc = buildFakeDocument({ navElements: [smallNav, largeNav] });
 370      const fakeWin = buildFakeWindow({
 371        innerHeight: 1080,
 372        innerWidth: 1920,
 373        computedStyles: new Map([
 374          [smallNav, { position: 'fixed', display: 'block' }],
 375          [largeNav, { position: 'sticky', display: 'block' }],
 376        ]),
 377      });
 378      const page = createDOMPage({ fakeDoc, fakeWin });
 379  
 380      const result = await analyzeCropBoundaries(page, 'desktop_above');
 381  
 382      // Should crop at 100px (the large nav)
 383      assert.strictEqual(result.topCrop, 100);
 384      assert.match(result.metadata.navReasoning, /Cropped 100px/);
 385    });
 386  });
 387  
 388  // ---------------------------------------------------------------------------
 389  // Tests: Trust signal detection (lines 87-122)
 390  // ---------------------------------------------------------------------------
 391  
 392  describe('Trust signal detection in nav elements', () => {
 393    test('preserves nav with visible trust signal (BBB badge >=50px)', async () => {
 394      const trustEl = {
 395        getBoundingClientRect: () => ({
 396          top: 10,
 397          bottom: 70,
 398          height: 60,
 399          width: 60,
 400          left: 100,
 401          right: 160,
 402        }),
 403        _style: { display: 'block', visibility: 'visible' },
 404      };
 405  
 406      const navEl = {
 407        getBoundingClientRect: () => ({
 408          top: 0,
 409          bottom: 80,
 410          height: 80,
 411          width: 1920,
 412          left: 0,
 413          right: 1920,
 414        }),
 415        querySelector: selector => {
 416          // First check CTA selectors (none match), then trust selectors
 417          const ctaSelectors = [
 418            'button',
 419            'a[href^="tel:"]',
 420            'a[href^="mailto:"]',
 421            'a[href*="contact"]',
 422            'a[href*="quote"]',
 423            'a[href*="booking"]',
 424            'a[href*="appointment"]',
 425            '[class*="cta"]',
 426            '[class*="btn"]',
 427            '[class*="button"]',
 428            '[class*="call"]',
 429            '[id*="cta"]',
 430            '[id*="call"]',
 431            'input[type="submit"]',
 432          ];
 433          if (ctaSelectors.includes(selector)) return null;
 434          // Trust signal selectors
 435          if (
 436            selector.includes('BBB') ||
 437            selector.includes('trust') ||
 438            selector.includes('badge') ||
 439            selector.includes('certified') ||
 440            selector.includes('certification') ||
 441            selector.includes('award') ||
 442            selector.includes('accredit') ||
 443            selector.includes('licensed') ||
 444            selector.includes('insured') ||
 445            selector.includes('rated')
 446          ) {
 447            return trustEl;
 448          }
 449          return null;
 450        },
 451        _style: { position: 'fixed', display: 'block' },
 452      };
 453  
 454      const computedStyles = new Map([
 455        [navEl, { position: 'fixed', display: 'block' }],
 456        [trustEl, { display: 'block', visibility: 'visible' }],
 457      ]);
 458  
 459      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 460      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920, computedStyles });
 461      const page = createDOMPage({ fakeDoc, fakeWin });
 462  
 463      const result = await analyzeCropBoundaries(page, 'desktop_above');
 464  
 465      assert.strictEqual(result.topCrop, 0);
 466      assert.match(result.metadata.navReasoning, /contains CTAs/);
 467    });
 468  
 469    test('ignores tiny trust signal badges (<50px)', async () => {
 470      const tinyTrustEl = {
 471        getBoundingClientRect: () => ({
 472          top: 10,
 473          bottom: 40,
 474          height: 30,
 475          width: 30,
 476          left: 100,
 477          right: 130,
 478        }),
 479        _style: { display: 'block', visibility: 'visible' },
 480      };
 481  
 482      const navEl = {
 483        getBoundingClientRect: () => ({
 484          top: 0,
 485          bottom: 80,
 486          height: 80,
 487          width: 1920,
 488          left: 0,
 489          right: 1920,
 490        }),
 491        querySelector: selector => {
 492          const ctaSelectors = [
 493            'button',
 494            'a[href^="tel:"]',
 495            'a[href^="mailto:"]',
 496            'a[href*="contact"]',
 497            'a[href*="quote"]',
 498            'a[href*="booking"]',
 499            'a[href*="appointment"]',
 500            '[class*="cta"]',
 501            '[class*="btn"]',
 502            '[class*="button"]',
 503            '[class*="call"]',
 504            '[id*="cta"]',
 505            '[id*="call"]',
 506            'input[type="submit"]',
 507          ];
 508          if (ctaSelectors.includes(selector)) return null;
 509          // Return tiny trust el for trust selectors
 510          if (selector.includes('BBB') || selector.includes('trust') || selector.includes('badge')) {
 511            return tinyTrustEl;
 512          }
 513          return null;
 514        },
 515        _style: { position: 'fixed', display: 'block' },
 516      };
 517  
 518      const computedStyles = new Map([
 519        [navEl, { position: 'fixed', display: 'block' }],
 520        [tinyTrustEl, { display: 'block', visibility: 'visible' }],
 521      ]);
 522  
 523      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 524      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920, computedStyles });
 525      const page = createDOMPage({ fakeDoc, fakeWin });
 526  
 527      // Tiny badge (<50x50) should be ignored, so nav without other CTAs should be cropped
 528      const result = await analyzeCropBoundaries(page, 'desktop_above');
 529  
 530      assert.strictEqual(result.topCrop, 80); // Cropped because tiny badge ignored
 531      assert.strictEqual(result.metadata.navCropped, true);
 532    });
 533  });
 534  
 535  // ---------------------------------------------------------------------------
 536  // Tests: CTA visibility checks (lines 67-84)
 537  // ---------------------------------------------------------------------------
 538  
 539  describe('CTA visibility checks', () => {
 540    test('ignores hidden CTA (display: none)', async () => {
 541      const hiddenCta = {
 542        getBoundingClientRect: () => ({ top: 0, bottom: 0, height: 0, width: 0, left: 0, right: 0 }),
 543        _style: { display: 'none', visibility: 'visible', opacity: '1' },
 544      };
 545  
 546      const navEl = {
 547        getBoundingClientRect: () => ({
 548          top: 0,
 549          bottom: 80,
 550          height: 80,
 551          width: 1920,
 552          left: 0,
 553          right: 1920,
 554        }),
 555        querySelector: selector => {
 556          if (selector === 'button') return hiddenCta;
 557          return null;
 558        },
 559        _style: { position: 'fixed', display: 'block' },
 560      };
 561  
 562      const computedStyles = new Map([
 563        [navEl, { position: 'fixed', display: 'block' }],
 564        [hiddenCta, { display: 'none', visibility: 'visible', opacity: '1' }],
 565      ]);
 566  
 567      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 568      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920, computedStyles });
 569      const page = createDOMPage({ fakeDoc, fakeWin });
 570  
 571      const result = await analyzeCropBoundaries(page, 'desktop_above');
 572  
 573      // Hidden CTA should be ignored, nav should be cropped
 574      assert.strictEqual(result.topCrop, 80);
 575      assert.strictEqual(result.metadata.navCropped, true);
 576    });
 577  
 578    test('ignores zero-width CTA element', async () => {
 579      const zeroCta = {
 580        getBoundingClientRect: () => ({
 581          top: 10,
 582          bottom: 50,
 583          height: 40,
 584          width: 0,
 585          left: 0,
 586          right: 0,
 587        }),
 588        _style: { display: 'block', visibility: 'visible', opacity: '1' },
 589      };
 590  
 591      const navEl = {
 592        getBoundingClientRect: () => ({
 593          top: 0,
 594          bottom: 80,
 595          height: 80,
 596          width: 1920,
 597          left: 0,
 598          right: 1920,
 599        }),
 600        querySelector: selector => {
 601          if (selector === 'button') return zeroCta;
 602          return null;
 603        },
 604        _style: { position: 'fixed', display: 'block' },
 605      };
 606  
 607      const computedStyles = new Map([
 608        [navEl, { position: 'fixed', display: 'block' }],
 609        [zeroCta, { display: 'block', visibility: 'visible', opacity: '1' }],
 610      ]);
 611  
 612      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 613      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920, computedStyles });
 614      const page = createDOMPage({ fakeDoc, fakeWin });
 615  
 616      const result = await analyzeCropBoundaries(page, 'desktop_above');
 617  
 618      // Zero-width CTA should be ignored
 619      assert.strictEqual(result.topCrop, 80);
 620      assert.strictEqual(result.metadata.navCropped, true);
 621    });
 622  });
 623  
 624  // ---------------------------------------------------------------------------
 625  // Tests: Main content margin detection (lines 168-216)
 626  // ---------------------------------------------------------------------------
 627  
 628  describe('Main content margin detection via real evaluate', () => {
 629    test('crops significant left and right margins (>5% viewport)', async () => {
 630      const mainEl = {
 631        getBoundingClientRect: () => ({
 632          top: 0,
 633          bottom: 1000,
 634          height: 1000,
 635          left: 200,
 636          right: 1720,
 637          width: 1520,
 638        }),
 639        _style: { display: 'block' },
 640      };
 641  
 642      const fakeDoc = buildFakeDocument({ mainElement: mainEl });
 643      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920 });
 644      const page = createDOMPage({ fakeDoc, fakeWin });
 645  
 646      const result = await analyzeCropBoundaries(page, 'desktop_above');
 647  
 648      assert.strictEqual(result.leftCrop, 200);
 649      assert.strictEqual(result.rightCrop, 200);
 650      assert.match(result.metadata.marginReasoning, /Cropped margins/);
 651    });
 652  
 653    test('does not crop small margins (<5% viewport)', async () => {
 654      const mainEl = {
 655        getBoundingClientRect: () => ({
 656          top: 0,
 657          bottom: 1000,
 658          height: 1000,
 659          left: 50,
 660          right: 1870,
 661          width: 1820,
 662        }),
 663        _style: { display: 'block' },
 664      };
 665  
 666      const fakeDoc = buildFakeDocument({ mainElement: mainEl });
 667      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920 });
 668      const page = createDOMPage({ fakeDoc, fakeWin });
 669  
 670      const result = await analyzeCropBoundaries(page, 'desktop_above');
 671  
 672      assert.strictEqual(result.leftCrop, 0);
 673      assert.strictEqual(result.rightCrop, 0);
 674      assert.strictEqual(result.metadata.marginReasoning, 'No margin cropping');
 675    });
 676  
 677    test('skips main element with zero dimensions', async () => {
 678      const mainEl = {
 679        getBoundingClientRect: () => ({
 680          top: 0,
 681          bottom: 0,
 682          height: 0,
 683          left: 200,
 684          right: 0,
 685          width: 0,
 686        }),
 687        _style: { display: 'block' },
 688      };
 689  
 690      const fakeDoc = buildFakeDocument({ mainElement: mainEl });
 691      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920 });
 692      const page = createDOMPage({ fakeDoc, fakeWin });
 693  
 694      const result = await analyzeCropBoundaries(page, 'desktop_above');
 695  
 696      assert.strictEqual(result.leftCrop, 0);
 697      assert.strictEqual(result.rightCrop, 0);
 698    });
 699  
 700    test('handles asymmetric margins (large left, small right)', async () => {
 701      const mainEl = {
 702        getBoundingClientRect: () => ({
 703          top: 0,
 704          bottom: 1000,
 705          height: 1000,
 706          left: 300,
 707          right: 1900,
 708          width: 1600,
 709        }),
 710        _style: { display: 'block' },
 711      };
 712  
 713      const fakeDoc = buildFakeDocument({ mainElement: mainEl });
 714      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920 });
 715      const page = createDOMPage({ fakeDoc, fakeWin });
 716  
 717      const result = await analyzeCropBoundaries(page, 'desktop_above');
 718  
 719      assert.strictEqual(result.leftCrop, 300); // 300 > 5% of 1920 (96px)
 720      assert.strictEqual(result.rightCrop, 0); // 20px < 5% threshold
 721    });
 722  
 723    test('tries multiple main content selectors until finding one', async () => {
 724      // Make 'main' return no result, but '.container' return valid element
 725      const containerEl = {
 726        getBoundingClientRect: () => ({
 727          top: 0,
 728          bottom: 1000,
 729          height: 1000,
 730          left: 150,
 731          right: 1770,
 732          width: 1620,
 733        }),
 734        _style: { display: 'block' },
 735      };
 736  
 737      const fakeDoc = {
 738        querySelectorAll: () => [],
 739        querySelector: selector => {
 740          // Only return element for .container
 741          if (selector === '.container') return containerEl;
 742          return null;
 743        },
 744      };
 745  
 746      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920 });
 747      const page = createDOMPage({ fakeDoc, fakeWin });
 748  
 749      const result = await analyzeCropBoundaries(page, 'desktop_above');
 750  
 751      assert.strictEqual(result.leftCrop, 150);
 752    });
 753  });
 754  
 755  // ---------------------------------------------------------------------------
 756  // Tests: Type-specific adjustments (lines 219-228)
 757  // ---------------------------------------------------------------------------
 758  
 759  describe('Type-specific adjustments via real evaluate', () => {
 760    test('desktop_below: resets topCrop to 0 even with sticky nav', async () => {
 761      const navEl = {
 762        getBoundingClientRect: () => ({
 763          top: 0,
 764          bottom: 80,
 765          height: 80,
 766          width: 1920,
 767          left: 0,
 768          right: 1920,
 769        }),
 770        querySelector: () => null,
 771        _style: { position: 'fixed', display: 'block' },
 772      };
 773  
 774      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 775      const fakeWin = buildFakeWindow({
 776        innerHeight: 1080,
 777        innerWidth: 1920,
 778        computedStyles: new Map([[navEl, { position: 'fixed', display: 'block' }]]),
 779      });
 780      const page = createDOMPage({ fakeDoc, fakeWin });
 781  
 782      const result = await analyzeCropBoundaries(page, 'desktop_below');
 783  
 784      assert.strictEqual(result.topCrop, 0);
 785      assert.match(result.metadata.navReasoning, /Below-fold/);
 786    });
 787  
 788    test('mobile_above: resets leftCrop and rightCrop to 0', async () => {
 789      const mainEl = {
 790        getBoundingClientRect: () => ({
 791          top: 0,
 792          bottom: 600,
 793          height: 600,
 794          left: 20,
 795          right: 355,
 796          width: 335,
 797        }),
 798        _style: { display: 'block' },
 799      };
 800  
 801      const fakeDoc = buildFakeDocument({ mainElement: mainEl });
 802      const fakeWin = buildFakeWindow({ innerHeight: 640, innerWidth: 375 });
 803      const page = createDOMPage({ fakeDoc, fakeWin });
 804  
 805      const result = await analyzeCropBoundaries(page, 'mobile_above');
 806  
 807      assert.strictEqual(result.leftCrop, 0);
 808      assert.strictEqual(result.rightCrop, 0);
 809      assert.match(result.metadata.marginReasoning, /Mobile/);
 810    });
 811  
 812    test('desktop_above: preserves both nav and margin crops', async () => {
 813      const navEl = {
 814        getBoundingClientRect: () => ({
 815          top: 0,
 816          bottom: 80,
 817          height: 80,
 818          width: 1920,
 819          left: 0,
 820          right: 1920,
 821        }),
 822        querySelector: () => null,
 823        _style: { position: 'sticky', display: 'block' },
 824      };
 825      const mainEl = {
 826        getBoundingClientRect: () => ({
 827          top: 0,
 828          bottom: 1000,
 829          height: 1000,
 830          left: 200,
 831          right: 1720,
 832          width: 1520,
 833        }),
 834        _style: { display: 'block' },
 835      };
 836  
 837      const fakeDoc = buildFakeDocument({ navElements: [navEl], mainElement: mainEl });
 838      const fakeWin = buildFakeWindow({
 839        innerHeight: 1080,
 840        innerWidth: 1920,
 841        computedStyles: new Map([[navEl, { position: 'sticky', display: 'block' }]]),
 842      });
 843      const page = createDOMPage({ fakeDoc, fakeWin });
 844  
 845      const result = await analyzeCropBoundaries(page, 'desktop_above');
 846  
 847      assert.strictEqual(result.topCrop, 80);
 848      assert.strictEqual(result.leftCrop, 200);
 849      assert.strictEqual(result.rightCrop, 200);
 850    });
 851  });
 852  
 853  // ---------------------------------------------------------------------------
 854  // Tests: Return value structure (lines 230-242)
 855  // ---------------------------------------------------------------------------
 856  
 857  describe('Return value structure from real evaluate', () => {
 858    test('metadata includes viewport dimensions from window', async () => {
 859      const fakeDoc = buildFakeDocument();
 860      const fakeWin = buildFakeWindow({ innerHeight: 768, innerWidth: 1366 });
 861      const page = createDOMPage({ fakeDoc, fakeWin });
 862  
 863      const result = await analyzeCropBoundaries(page, 'desktop_above');
 864  
 865      assert.strictEqual(result.metadata.viewportWidth, 1366);
 866      assert.strictEqual(result.metadata.viewportHeight, 768);
 867    });
 868  
 869    test('metadata includes navCropped=false when no nav detected', async () => {
 870      const fakeDoc = buildFakeDocument();
 871      const fakeWin = buildFakeWindow();
 872      const page = createDOMPage({ fakeDoc, fakeWin });
 873  
 874      const result = await analyzeCropBoundaries(page, 'desktop_above');
 875  
 876      assert.strictEqual(result.metadata.hadNav, false);
 877      assert.strictEqual(result.metadata.navCropped, false);
 878      assert.strictEqual(result.metadata.navReasoning, 'No navigation detected');
 879      assert.strictEqual(result.metadata.marginReasoning, 'No margin cropping');
 880    });
 881  
 882    test('result has all required properties', async () => {
 883      const fakeDoc = buildFakeDocument();
 884      const fakeWin = buildFakeWindow();
 885      const page = createDOMPage({ fakeDoc, fakeWin });
 886  
 887      const result = await analyzeCropBoundaries(page, 'desktop_above');
 888  
 889      assert.ok('topCrop' in result, 'Should have topCrop');
 890      assert.ok('leftCrop' in result, 'Should have leftCrop');
 891      assert.ok('rightCrop' in result, 'Should have rightCrop');
 892      assert.ok('metadata' in result, 'Should have metadata');
 893      assert.ok('hadNav' in result.metadata);
 894      assert.ok('navCropped' in result.metadata);
 895      assert.ok('navReasoning' in result.metadata);
 896      assert.ok('marginReasoning' in result.metadata);
 897      assert.ok('viewportWidth' in result.metadata);
 898      assert.ok('viewportHeight' in result.metadata);
 899    });
 900  });
 901  
 902  // ---------------------------------------------------------------------------
 903  // Tests: Error fallback path (lines 250-266)
 904  // ---------------------------------------------------------------------------
 905  
 906  describe('Error fallback path', () => {
 907    test('returns zero-crop fallback when evaluate throws', async () => {
 908      const page = {
 909        evaluate: async () => {
 910          throw new Error('Page crashed');
 911        },
 912      };
 913  
 914      const result = await analyzeCropBoundaries(page, 'desktop_above');
 915  
 916      assert.strictEqual(result.topCrop, 0);
 917      assert.strictEqual(result.leftCrop, 0);
 918      assert.strictEqual(result.rightCrop, 0);
 919      assert.strictEqual(result.metadata.fallback, true);
 920      assert.strictEqual(result.metadata.error, 'Page crashed');
 921      assert.match(result.metadata.navReasoning, /conservative crop/);
 922    });
 923  });
 924  
 925  // ---------------------------------------------------------------------------
 926  // Tests: Nav with invisible CTA (opacity: 0)
 927  // ---------------------------------------------------------------------------
 928  
 929  describe('Invisible CTA handling', () => {
 930    test('ignores CTA with opacity:0', async () => {
 931      const invisibleCta = {
 932        getBoundingClientRect: () => ({
 933          top: 10,
 934          bottom: 50,
 935          height: 40,
 936          width: 120,
 937          left: 0,
 938          right: 120,
 939        }),
 940        _style: { display: 'block', visibility: 'visible', opacity: '0' },
 941      };
 942  
 943      const navEl = {
 944        getBoundingClientRect: () => ({
 945          top: 0,
 946          bottom: 80,
 947          height: 80,
 948          width: 1920,
 949          left: 0,
 950          right: 1920,
 951        }),
 952        querySelector: selector => {
 953          if (selector === 'button') return invisibleCta;
 954          return null;
 955        },
 956        _style: { position: 'fixed', display: 'block' },
 957      };
 958  
 959      const computedStyles = new Map([
 960        [navEl, { position: 'fixed', display: 'block' }],
 961        [invisibleCta, { display: 'block', visibility: 'visible', opacity: '0' }],
 962      ]);
 963  
 964      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
 965      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920, computedStyles });
 966      const page = createDOMPage({ fakeDoc, fakeWin });
 967  
 968      const result = await analyzeCropBoundaries(page, 'desktop_above');
 969  
 970      // Opacity:0 CTA should be invisible, so nav should be cropped
 971      assert.strictEqual(result.topCrop, 80);
 972      assert.strictEqual(result.metadata.navCropped, true);
 973    });
 974  
 975    test('ignores CTA with visibility:hidden', async () => {
 976      const hiddenCta = {
 977        getBoundingClientRect: () => ({
 978          top: 10,
 979          bottom: 50,
 980          height: 40,
 981          width: 120,
 982          left: 0,
 983          right: 120,
 984        }),
 985        _style: { display: 'block', visibility: 'hidden', opacity: '1' },
 986      };
 987  
 988      const navEl = {
 989        getBoundingClientRect: () => ({
 990          top: 0,
 991          bottom: 80,
 992          height: 80,
 993          width: 1920,
 994          left: 0,
 995          right: 1920,
 996        }),
 997        querySelector: selector => {
 998          if (selector === 'button') return hiddenCta;
 999          return null;
1000        },
1001        _style: { position: 'fixed', display: 'block' },
1002      };
1003  
1004      const computedStyles = new Map([
1005        [navEl, { position: 'fixed', display: 'block' }],
1006        [hiddenCta, { display: 'block', visibility: 'hidden', opacity: '1' }],
1007      ]);
1008  
1009      const fakeDoc = buildFakeDocument({ navElements: [navEl] });
1010      const fakeWin = buildFakeWindow({ innerHeight: 1080, innerWidth: 1920, computedStyles });
1011      const page = createDOMPage({ fakeDoc, fakeWin });
1012  
1013      const result = await analyzeCropBoundaries(page, 'desktop_above');
1014  
1015      assert.strictEqual(result.topCrop, 80);
1016      assert.strictEqual(result.metadata.navCropped, true);
1017    });
1018  });