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 });