Files
cinny/src/app/utils/dom.test.ts
T
jaredandClaude Opus 4.8 36369926ca test: cover dom + emoji pure helpers; fix syntaxErrorPosition regex
Test-coverage expansion (2-agent reviewed, both SHIP). The named candidates
(roomToUnread, markedUnread, serverAcl, plaintextCaches, recent*) were already
tested, so this targets genuinely-untested pure logic.

- dom.test.ts: getThumbnailDimensions (scaling math incl. just-over-cap
  boundaries), tryDecodeURIComponent, syntaxErrorPosition, and the three
  scroll-view geometry helpers (via duck-typed element mocks — no jsdom).
- emoji.test.ts: getHexcodeForEmoji (astral codepoints, 4-digit zero-pad,
  FE0F/FE0E/200D stripping on and off, keycap sequences, degenerate inputs)
  and the pre-load `undefined` contract for getShortcode(s)For.

Fix (found while writing the tests): syntaxErrorPosition required whitespace
AFTER the digits (`/position\s(\d+)\s/`), but real V8/Node JSON.parse errors
put the number at end-of-string ("... at position N"), so it returned
undefined for every real error and the three dev-tools JSON editors silently
pointed their cursor at position 0. Dropped the trailing `\s`; tests now assert
extraction at end-of-string.

Gates: tsc 0, eslint 0, prettier clean, 891 tests, build ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:49:51 -04:00

159 lines
6.2 KiB
TypeScript

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