Files
Chris Troutner 19e57be1ec Review post-image-rendering: cover isImageUrl guard, stamp manifests
Merged the refactorer handoff (e77e174). The architecture holds: the image
helpers isImageUrl/imageAltText are pure post-links functions, failed-images
is a pure state transition, and PostImage/PostContent stay presentational
with no IO. Added a unit test for isImageUrl non-string input to kill the
last surviving language mutant; mutation and acceptance manifests are
tool-written.

By architect.
2026-09-16 07:34:13 -07:00

180 lines
6.8 KiB
JavaScript

/*
Unit tests for the post link parser.
The parser turns a post's text into text and link segments. A link segment
is either an http(s) URL (scheme preserved) or a bare domain (linked with an
https scheme while its visible text is left as written). Trailing sentence
punctuation never becomes part of the link. Plain text and email addresses
stay plain text.
*/
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const {
parsePostLinks,
isImageUrl,
imageAltText
} = require('../../src/services/post-links')
test('parsePostLinks returns a single text segment for plain text', () => {
const text = 'just a normal memo'
assert.deepEqual(parsePostLinks(text), [{ type: 'text', text }])
})
test('parsePostLinks returns an empty text segment for an empty string', () => {
assert.deepEqual(parsePostLinks(''), [{ type: 'text', text: '' }])
})
test('parsePostLinks links an https URL and preserves its scheme', () => {
assert.deepEqual(parsePostLinks('visit https://memo.fullstackcash.net for details'), [
{ type: 'text', text: 'visit ' },
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'https://memo.fullstackcash.net' },
{ type: 'text', text: ' for details' }
])
})
test('parsePostLinks links an http URL and preserves its scheme', () => {
assert.deepEqual(parsePostLinks('link http://example.com/path here'), [
{ type: 'text', text: 'link ' },
{ type: 'link', href: 'http://example.com/path', text: 'http://example.com/path' },
{ type: 'text', text: ' here' }
])
})
test('parsePostLinks keeps trailing punctuation out of the link', () => {
assert.deepEqual(parsePostLinks('read https://memo.fullstackcash.net, then reply'), [
{ type: 'text', text: 'read ' },
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'https://memo.fullstackcash.net' },
{ type: 'text', text: ', then reply' }
])
})
test('parsePostLinks links a bare domain with an https href and unchanged visible text', () => {
assert.deepEqual(parsePostLinks('visit memo.fullstackcash.net for details'), [
{ type: 'text', text: 'visit ' },
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'memo.fullstackcash.net' },
{ type: 'text', text: ' for details' }
])
})
test('parsePostLinks links a bare domain with a path', () => {
assert.deepEqual(parsePostLinks('see memo.fullstackcash.net/feed now'), [
{ type: 'text', text: 'see ' },
{ type: 'link', href: 'https://memo.fullstackcash.net/feed', text: 'memo.fullstackcash.net/feed' },
{ type: 'text', text: ' now' }
])
})
test('parsePostLinks links a bare www domain', () => {
assert.deepEqual(parsePostLinks('go to www.example.com now'), [
{ type: 'text', text: 'go to ' },
{ type: 'link', href: 'https://www.example.com', text: 'www.example.com' },
{ type: 'text', text: ' now' }
])
})
test('parsePostLinks keeps trailing punctuation out of a bare-domain link', () => {
assert.deepEqual(parsePostLinks('visit memo.fullstackcash.net, it is live'), [
{ type: 'text', text: 'visit ' },
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'memo.fullstackcash.net' },
{ type: 'text', text: ', it is live' }
])
})
test('parsePostLinks handles multiple links in one post', () => {
assert.deepEqual(
parsePostLinks('watch https://youtu.be/dQw4w9WgXcQ then read https://memo.fullstackcash.net'),
[
{ type: 'text', text: 'watch ' },
{ type: 'link', href: 'https://youtu.be/dQw4w9WgXcQ', text: 'https://youtu.be/dQw4w9WgXcQ' },
{ type: 'text', text: ' then read ' },
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'https://memo.fullstackcash.net' }
]
)
})
test('parsePostLinks does not link an email address', () => {
const text = 'write to me at chris@example.com please'
assert.deepEqual(parsePostLinks(text), [{ type: 'text', text }])
})
test('parsePostLinks does not link a dotted user name before an @ sign', () => {
const text = 'write to first.last@example.com please'
assert.deepEqual(parsePostLinks(text), [{ type: 'text', text }])
})
test('parsePostLinks links a bare domain at the start of the text', () => {
assert.deepEqual(parsePostLinks('memo.fullstackcash.net is live'), [
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'memo.fullstackcash.net' },
{ type: 'text', text: ' is live' }
])
})
test('parsePostLinks does not link a bare domain glued to a leading token character', () => {
assert.deepEqual(parsePostLinks('-example.com'), [{ type: 'text', text: '-example.com' }])
})
test('parsePostLinks round-trips: segments reconstruct the original text', () => {
const samples = [
'just a normal memo',
'visit memo.fullstackcash.net for details',
'read https://memo.fullstackcash.net, then reply',
'go to www.example.com now',
'watch https://youtu.be/dQw4w9WgXcQ then read https://memo.fullstackcash.net',
'write to chris@example.com please',
''
]
for (const text of samples) {
const rebuilt = parsePostLinks(text)
.map((segment) => segment.text)
.join('')
assert.equal(rebuilt, text)
}
})
test('isImageUrl recognizes every supported image extension', () => {
for (const ext of ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']) {
assert.equal(isImageUrl(`https://example.com/photo.${ext}`), true, ext)
}
})
test('isImageUrl is case-insensitive', () => {
assert.equal(isImageUrl('https://example.com/photo.PNG'), true)
assert.equal(isImageUrl('https://example.com/photo.JPEG'), true)
})
test('isImageUrl ignores query strings and fragments', () => {
assert.equal(isImageUrl('https://example.com/photo.webp?w=500'), true)
assert.equal(isImageUrl('https://example.com/photo.png#section'), true)
})
test('isImageUrl rejects URLs that are not images', () => {
assert.equal(isImageUrl('https://example.com/page'), false)
assert.equal(isImageUrl('https://example.com/logo.svg'), false)
assert.equal(isImageUrl('https://example.com/photo?format=jpg'), false)
assert.equal(isImageUrl('not a url'), false)
})
test('isImageUrl rejects non-string input', () => {
assert.equal(isImageUrl(null), false)
assert.equal(isImageUrl(undefined), false)
assert.equal(isImageUrl(12345), false)
assert.equal(isImageUrl({}), false)
})
test('imageAltText returns the URL filename', () => {
assert.equal(imageAltText('https://i.imgur.com/swCI56T.jpeg'), 'swCI56T.jpeg')
assert.equal(imageAltText('https://cdn.example.com/pics/Sunset.PNG'), 'Sunset.PNG')
})
test('imageAltText ignores query strings when deriving the filename', () => {
assert.equal(imageAltText('https://example.com/img/photo.webp?w=500'), 'photo.webp')
})
test('imageAltText falls back to "post image" when there is no filename', () => {
assert.equal(imageAltText('https://example.com/'), 'post image')
assert.equal(imageAltText('not a url'), 'post image')
})