import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getFeedPostHeightEstimate, getReplyHeightEstimates, getReplyItemSizeFromElement, resolveCatalogVirtualizationMode, resolveFeedVirtualizationMode, resolveReplyVirtualizationMode, } from '../pretext-height-estimates'; import { EXPANDED_MEDIA_DATA_ATTRIBUTE } from '../measurement-attributes'; describe('pretext-height-estimates', () => { beforeEach(() => { Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', { configurable: true, value: vi.fn(() => ({ font: '', measureText: vi.fn((text: string) => ({ actualBoundingBoxAscent: 10, actualBoundingBoxDescent: 3, fontBoundingBoxAscent: 10, fontBoundingBoxDescent: 3, width: text.length * 7, })), })), writable: true, }); }); it('parses reply virtualization mode from search params', () => { expect(resolveReplyVirtualizationMode('?pretextReplies=item-size')).toBe('item-size'); expect(resolveReplyVirtualizationMode(new URLSearchParams('pretextReplies=off'))).toBe('off'); expect(resolveReplyVirtualizationMode('?pretextReplies=unknown')).toBe('item-size'); }); it('parses feed virtualization mode from search params', () => { expect(resolveFeedVirtualizationMode('?pretextFeed=item-size')).toBe('item-size'); expect(resolveFeedVirtualizationMode(new URLSearchParams('pretextFeed=off'))).toBe('off'); expect(resolveFeedVirtualizationMode('?pretextFeed=unknown')).toBe('off'); }); it('parses catalog virtualization mode from search params', () => { expect(resolveCatalogVirtualizationMode('?pretextCatalog=item-size')).toBe('item-size'); expect(resolveCatalogVirtualizationMode(new URLSearchParams('pretextCatalog=off'))).toBe('off'); expect(resolveCatalogVirtualizationMode('?pretextCatalog=unknown')).toBe('off'); }); it('falls back to hash-fragment query params for hash-routed URLs', () => { const previousHref = window.location.href; window.history.replaceState({}, '', '/#/mu?pretextFeed=item-size&pretextReplies=off'); expect(resolveFeedVirtualizationMode(window.location.search)).toBe('item-size'); expect(resolveReplyVirtualizationMode(window.location.search)).toBe('off'); expect(resolveCatalogVirtualizationMode(window.location.search)).toBe('off'); window.history.replaceState({}, '', previousHref); }); it('prefers cached Pretext height when itemSize reads a reply row', () => { const element = document.createElement('div'); Object.defineProperty(element, 'offsetHeight', { configurable: true, value: 321 }); Object.defineProperty(element, 'offsetWidth', { configurable: true, value: 654 }); element.dataset.pretextHeight = '123'; expect(getReplyItemSizeFromElement(element, 'offsetHeight')).toBe(123); expect(getReplyItemSizeFromElement(element, 'offsetWidth')).toBe(654); delete element.dataset.pretextHeight; expect(getReplyItemSizeFromElement(element, 'offsetHeight')).toBe(321); }); it('falls back to a nested Pretext height when the wrapper has none', () => { const element = document.createElement('div'); const child = document.createElement('div'); Object.defineProperty(element, 'offsetHeight', { configurable: true, value: 321 }); Object.defineProperty(element, 'offsetWidth', { configurable: true, value: 654 }); child.dataset.pretextHeight = '222'; element.appendChild(child); expect(getReplyItemSizeFromElement(element, 'offsetHeight')).toBe(222); }); it('uses live DOM height when expanded media is inside an estimated item', () => { const element = document.createElement('div'); const child = document.createElement('div'); Object.defineProperty(element, 'offsetHeight', { configurable: true, value: 421 }); element.dataset.pretextHeight = '123'; child.setAttribute(EXPANDED_MEDIA_DATA_ATTRIBUTE, 'true'); element.appendChild(child); expect(getReplyItemSizeFromElement(element, 'offsetHeight')).toBe(421); }); it('adds desktop board-label height for multiboard text posts without media', () => { const metrics = { abbrFontSizePx: 13, bodyFontFamily: 'Arial, Helvetica, sans-serif', bodyFontSizePx: 13, mobileContentFontSizePx: 15, }; const post = { cid: 'post-1', communityAddress: 'music-posting.eth', content: 'A short desktop board post', postCid: 'post-1', state: 'succeeded', }; const withoutBoardLabel = getFeedPostHeightEstimate({ isMobile: false, metrics, post, previewReplies: [], windowWidth: 1280, }); const withBoardLabel = getFeedPostHeightEstimate({ isMobile: false, metrics, post, previewReplies: [], showBoardLabel: true, windowWidth: 1280, }); expect(withBoardLabel).toBeGreaterThan(withoutBoardLabel); }); it('accounts for the board-view long-comment notice on truncated desktop posts', () => { const metrics = { abbrFontSizePx: 13, bodyFontFamily: 'Arial, Helvetica, sans-serif', bodyFontSizePx: 13, mobileContentFontSizePx: 15, }; const visiblePrefix = 'word '.repeat(200); const shortPost = { cid: 'post-short', communityAddress: 'music-posting.eth', content: visiblePrefix, postCid: 'post-short', state: 'succeeded', }; const truncatedPost = { ...shortPost, cid: 'post-long', content: `${visiblePrefix}${'overflow '.repeat(40)}`, postCid: 'post-long', }; const shortEstimate = getFeedPostHeightEstimate({ isMobile: false, metrics, post: shortPost, previewReplies: [], windowWidth: 1280, }); const truncatedEstimate = getFeedPostHeightEstimate({ isMobile: false, metrics, post: truncatedPost, previewReplies: [], windowWidth: 1280, }); expect(truncatedEstimate).toBeGreaterThan(shortEstimate); }); it('adds desktop preview reply estimates without subtracting a per-reply calibration', () => { const metrics = { abbrFontSizePx: 13, bodyFontFamily: 'Arial, Helvetica, sans-serif', bodyFontSizePx: 13, mobileContentFontSizePx: 15, }; const post = { cid: 'post-preview-parent', communityAddress: 'music-posting.eth', content: 'A longer desktop board post '.repeat(24), postCid: 'post-preview-parent', state: 'succeeded', }; const previewReply = { cid: 'post-preview-reply', communityAddress: 'music-posting.eth', content: 'A preview reply', parentCid: 'post-preview-parent', postCid: 'post-preview-parent', state: 'succeeded', }; const previewEstimate = getReplyHeightEstimates({ isMobile: false, metrics, replies: [previewReply], windowWidth: 1280, })[0]; const withoutPreview = getFeedPostHeightEstimate({ isMobile: false, metrics, post, previewReplies: [], windowWidth: 1280, }); const withPreview = getFeedPostHeightEstimate({ isMobile: false, metrics, post, previewReplies: [previewReply], previewReplyEstimates: [previewEstimate], windowWidth: 1280, }); expect(withPreview - withoutPreview).toBe(previewEstimate); }); it('keeps most mobile preview-reply height on board cards', () => { const metrics = { abbrFontSizePx: 13, bodyFontFamily: 'Arial, Helvetica, sans-serif', bodyFontSizePx: 13, mobileContentFontSizePx: 15, }; const post = { cid: 'post-mobile-preview-parent', communityAddress: 'music-posting.eth', content: 'A mobile board post with preview replies', link: 'https://example.com/image.jpg', linkHeight: 720, linkWidth: 1280, postCid: 'post-mobile-preview-parent', state: 'succeeded', }; const previewReplies = Array.from({ length: 5 }, (_, index) => ({ cid: `post-mobile-preview-reply-${index}`, communityAddress: 'music-posting.eth', content: `Preview reply ${index} ${'text '.repeat(index + 1)}`, parentCid: 'post-mobile-preview-parent', postCid: 'post-mobile-preview-parent', state: 'succeeded', })); const previewEstimates = getReplyHeightEstimates({ isMobile: true, metrics, replies: previewReplies, windowWidth: 393, }); const withoutPreview = getFeedPostHeightEstimate({ isMobile: true, metrics, post, previewReplies: [], windowWidth: 393, }); const withPreview = getFeedPostHeightEstimate({ isMobile: true, metrics, post, previewReplies, previewReplyEstimates: previewEstimates, windowWidth: 393, }); expect(withPreview).toBeGreaterThan(withoutPreview + 300); expect(withPreview - withoutPreview).toBeGreaterThan(previewEstimates.reduce((sum, value) => sum + value, 0) - 220); }); });