import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { canFitInScrollView, getThumbnailDimensions, isInScrollView, isIntersectingScrollView, syntaxErrorPosition, tryDecodeURIComponent, } from './dom'; // The scroll-view helpers only read numeric layout properties off their // elements, so a plain duck-typed object stands in for an HTMLElement. type ElLike = { offsetTop?: number; scrollTop?: number; offsetHeight?: number; clientHeight?: number; }; const el = (props: ElLike): HTMLElement => props as unknown as HTMLElement; describe('getThumbnailDimensions', () => { it('leaves dimensions within the 400x300 cap untouched', () => { assert.deepEqual(getThumbnailDimensions(200, 150), [200, 150]); assert.deepEqual(getThumbnailDimensions(400, 300), [400, 300]); assert.deepEqual(getThumbnailDimensions(100, 100), [100, 100]); }); it('scales down by height when taller than 300', () => { // 200x600 -> width * (300/600) = 100, height clamped to 300 assert.deepEqual(getThumbnailDimensions(200, 600), [100, 300]); }); it('scales down by width when wider than 400', () => { // 800x200 -> height * (400/800) = 100, width clamped to 400 assert.deepEqual(getThumbnailDimensions(800, 200), [400, 100]); }); it('applies the height clamp first, then the width clamp', () => { // 800x600 -> height clamp: 400x300 (width already at cap, no further change) assert.deepEqual(getThumbnailDimensions(800, 600), [400, 300]); // 1200x600 -> height clamp: 600x300 -> width clamp: 400x200 assert.deepEqual(getThumbnailDimensions(1200, 600), [400, 200]); }); it('floors fractional results', () => { // 300x700 -> width * (300/700) = 128.57 -> floored to 128 assert.deepEqual(getThumbnailDimensions(300, 700), [128, 300]); }); it('scales on a just-over-boundary input (strict > comparisons)', () => { // one over the height cap -> scales; one over the width cap -> scales assert.deepEqual(getThumbnailDimensions(400, 301), [398, 300]); assert.deepEqual(getThumbnailDimensions(401, 300), [400, 299]); }); }); describe('tryDecodeURIComponent', () => { it('decodes a valid encoded component', () => { assert.equal(tryDecodeURIComponent('a%20b'), 'a b'); assert.equal(tryDecodeURIComponent('%C3%A9'), 'é'); }); it('returns the input unchanged when it has no escapes', () => { assert.equal(tryDecodeURIComponent('hello'), 'hello'); }); it('returns the raw input on a malformed sequence instead of throwing', () => { assert.equal(tryDecodeURIComponent('%'), '%'); assert.equal(tryDecodeURIComponent('%E0%A4%A'), '%E0%A4%A'); }); }); describe('syntaxErrorPosition', () => { it('extracts the position when the number ends the message (real V8/Node shape)', () => { // Real JSON.parse errors read "... at position N" with N at end-of-string. assert.equal( syntaxErrorPosition(new SyntaxError('Unexpected end of JSON input at position 10')), 10, ); }); it('extracts the position when it is followed by more text', () => { // Newer V8 appends "(line N column M)" after the number. assert.equal( syntaxErrorPosition(new SyntaxError('bad token in JSON at position 6 (line 1 column 7)')), 6, ); assert.equal(syntaxErrorPosition(new SyntaxError('bad at position 42 more')), 42); }); it('returns undefined when the message has no position', () => { assert.equal(syntaxErrorPosition(new SyntaxError('Unexpected end of input')), undefined); }); }); describe('isIntersectingScrollView', () => { // Viewport spans 0..100 (offsetTop 0 + scrollTop 0, height 100). const view = el({ offsetTop: 0, scrollTop: 0, offsetHeight: 100 }); it('is true for a child fully inside the view', () => { assert.equal(isIntersectingScrollView(view, el({ offsetTop: 20, clientHeight: 30 })), true); }); it('is true for a child straddling the top edge', () => { // -10..20 -> bottom (20) is within 0..100 assert.equal(isIntersectingScrollView(view, el({ offsetTop: -10, clientHeight: 30 })), true); }); it('is true for a child taller than and spanning the whole view', () => { // -20..180 -> top above, bottom below assert.equal(isIntersectingScrollView(view, el({ offsetTop: -20, clientHeight: 200 })), true); }); it('is false for a child entirely above or below the view', () => { assert.equal(isIntersectingScrollView(view, el({ offsetTop: -50, clientHeight: 20 })), false); assert.equal(isIntersectingScrollView(view, el({ offsetTop: 200, clientHeight: 20 })), false); }); it('respects the strict pixel boundaries (> vs >=)', () => { // child bottom sits exactly on scrollTop (0) -> not intersecting (childBottom > scrollTop is strict) assert.equal(isIntersectingScrollView(view, el({ offsetTop: -10, clientHeight: 10 })), false); // child top sits exactly on scrollBottom (100) -> not intersecting (childTop < scrollBottom is strict) assert.equal(isIntersectingScrollView(view, el({ offsetTop: 100, clientHeight: 20 })), false); }); it('accounts for the view scrollTop offset', () => { // View 0..100 in layout, scrolled by 100 -> logical window 100..200. const scrolled = el({ offsetTop: 0, scrollTop: 100, offsetHeight: 100 }); assert.equal( isIntersectingScrollView(scrolled, el({ offsetTop: 120, clientHeight: 10 })), true, ); assert.equal( isIntersectingScrollView(scrolled, el({ offsetTop: 20, clientHeight: 10 })), false, ); }); }); describe('isInScrollView', () => { const view = el({ offsetTop: 0, scrollTop: 0, offsetHeight: 100 }); it('is true only when the child is fully within the view', () => { assert.equal(isInScrollView(view, el({ offsetTop: 10, offsetHeight: 50 })), true); // straddles the bottom edge -> not fully in assert.equal(isInScrollView(view, el({ offsetTop: 80, offsetHeight: 50 })), false); }); }); describe('canFitInScrollView', () => { it('is true when the child is shorter than the view', () => { const view = el({ offsetHeight: 100 }); assert.equal(canFitInScrollView(view, el({ offsetHeight: 60 })), true); assert.equal(canFitInScrollView(view, el({ offsetHeight: 100 })), false); assert.equal(canFitInScrollView(view, el({ offsetHeight: 140 })), false); }); });