Files
Haven/test/forumLinkTopics.test.js
T
ancsemi f409f6fc36 Forum topics that are only a link show the link's title and picture (#5745)
A forum post whose text is just a YouTube link used the raw link as its
title and had no thumbnail, and the open topic showed only the link.

The topic card now takes the title and picture from the link's preview when
the post opens with a bare link and has no typed title, and the picture goes
through the media proxy like every other remote image (posted image
thumbnails did not before). The open topic gets the same video player or
link card the post gets in chat. A typed title still wins.

YouTube previews come from YouTube's oEmbed endpoint, since the watch page
is too large for the generic scrape to reach its title and image.
2026-10-09 23:34:36 -04:00

95 lines
5.0 KiB
JavaScript

'use strict';
/**
* A forum topic that is only a link (#5745): the card takes the link's title
* and picture from its preview, and the open topic gets the same player or
* link card the post gets in chat.
*
* node --test test/forumLinkTopics.test.js
*/
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const { pathToFileURL } = require('node:url');
const { youTubeVideoId } = require('../src/youtubeLink');
const ROOT = path.join(__dirname, '..');
const forumPath = path.join(ROOT, 'public', 'js', 'modules', 'app-forum.js');
const forumSrc = fs.readFileSync(forumPath, 'utf8');
const routeSrc = fs.readFileSync(path.join(ROOT, 'src', 'routes', 'link-preview.js'), 'utf8');
const VIDEO = 'https://www.youtube.com/watch?v=oDfMotCtS6M';
let F;
test.before(async () => {
const mod = (await import(pathToFileURL(forumPath).href)).default;
globalThis.t = globalThis.t || ((k) => k);
F = () => Object.assign({}, mod, {
_isImageUrl: (u) => /\.(png|jpe?g|gif|webp)(\?.*)?$/i.test(u),
});
});
test('the lead link is a first line that is only a link', () => {
const f = F();
assert.equal(f._forumLeadLinkOf({ content: VIDEO }), VIDEO);
assert.equal(f._forumLeadLinkOf({ content: ` ${VIDEO} \nmore words` }), VIDEO);
assert.equal(f._forumLeadLinkOf({ content: `watch this ${VIDEO}` }), null);
assert.equal(f._forumLeadLinkOf({ content: 'Just words' }), null);
assert.equal(f._forumLeadLinkOf({ content: `https://x.test/a.png\n${VIDEO}` }), VIDEO, 'a picture line is skipped');
assert.equal(f._forumLeadLinkOf({ content: `||${VIDEO}||` }), null, 'a spoilered link is left alone');
});
test('a link-only topic is titled after its preview once it is in', () => {
const f = F();
assert.equal(f._forumTitleOf({ content: VIDEO }), VIDEO, 'the link until the preview arrives');
f._forumLinkMeta = new Map([[VIDEO, { title: 'A lil VOTV before bed', image: 'https://i.ytimg.com/vi/oDfMotCtS6M/hqdefault.jpg' }]]);
assert.equal(f._forumTitleOf({ content: VIDEO }), 'A lil VOTV before bed');
assert.equal(f._forumTitleOf({ title: 'My own title', content: VIDEO }), 'My own title', 'a typed title wins');
assert.equal(f._forumSnippetOf({ content: VIDEO }), '', 'no snippet repeats the link');
});
test('the card picture is a posted image first, then the link preview image', () => {
const f = F();
assert.equal(f._forumThumbOf({ content: VIDEO }), null);
f._forumLinkMeta = new Map([[VIDEO, { title: 'x', image: 'https://i.ytimg.com/vi/oDfMotCtS6M/hqdefault.jpg' }]]);
assert.equal(f._forumThumbOf({ content: VIDEO }), 'https://i.ytimg.com/vi/oDfMotCtS6M/hqdefault.jpg');
assert.equal(f._forumThumbOf({ title: 'Titled', content: VIDEO }), 'https://i.ytimg.com/vi/oDfMotCtS6M/hqdefault.jpg');
assert.equal(f._forumThumbOf({ content: `${VIDEO}\nhttps://x.test/own.png` }), 'https://x.test/own.png');
f._forumLinkMeta = new Map([[VIDEO, { title: 'x', image: 'javascript:alert(1)' }]]);
assert.equal(f._forumThumbOf({ content: VIDEO }), null, 'only web addresses become pictures');
});
test('the preview of a lead link is asked for once and remembered', async () => {
const f = F();
let calls = 0;
f._forumTopics = new Map();
f._linkPreviewData = async () => { calls++; return { title: 'Video', image: 'https://i.ytimg.com/vi/oDfMotCtS6M/hqdefault.jpg' }; };
f._forumLoadLinkPreview({ id: 1, content: VIDEO });
f._forumLoadLinkPreview({ id: 2, content: VIDEO });
await new Promise(r => setImmediate(r));
assert.equal(calls, 1);
assert.equal(f._forumTitleOf({ content: VIDEO }), 'Video');
f._forumLoadLinkPreview({ id: 3, content: 'no link here' });
assert.equal(calls, 1);
});
test('cards and the open topic use the proxy and the chat link cards', () => {
assert.match(forumSrc, /this\._lazySrcAttr\(this\._imgSrcAttr\(thumb\)\)/, 'card picture goes through the media proxy');
assert.match(forumSrc, /thread-forum-thumb"><img \$\{this\._imgSrcAttr\(thumb\)\}/, 'topic header picture goes through the media proxy');
assert.match(forumSrc, /this\._forumLoadLinkPreview\(msg\);/);
assert.match(forumSrc, /container\.prepend\(body\);\s*\/\/[^\n]*\n\s*this\._fetchLinkPreviews\?\.\(body\);/);
});
test('the server reads YouTube links and asks oEmbed for their title and thumbnail', () => {
assert.equal(youTubeVideoId(VIDEO), 'oDfMotCtS6M');
assert.equal(youTubeVideoId('https://youtu.be/oDfMotCtS6M?t=30'), 'oDfMotCtS6M');
assert.equal(youTubeVideoId('https://m.youtube.com/shorts/oDfMotCtS6M'), 'oDfMotCtS6M');
assert.equal(youTubeVideoId('https://music.youtube.com/watch?v=oDfMotCtS6M&list=x'), 'oDfMotCtS6M');
assert.equal(youTubeVideoId('https://www.youtube.com/live/oDfMotCtS6M'), 'oDfMotCtS6M');
assert.equal(youTubeVideoId('https://example.com/watch?v=oDfMotCtS6M'), null);
assert.equal(youTubeVideoId('https://www.youtube.com/watch?v=short'), null);
assert.equal(youTubeVideoId('not a link'), null);
assert.match(routeSrc, /https:\/\/www\.youtube\.com\/oembed\?url=/);
});