| [d9ad6c5] | 1 | // One post, one rendering. De Krant, Berichten and the Guardian PWA all run a
|
|---|
| 2 | // note through partials/note-body, so a quote card, a link preview, the media
|
|---|
| 3 | // and the custom emojis show up wherever the post turns up — not only in the
|
|---|
| 4 | // feed it happened to arrive in.
|
|---|
| 5 | import { test } from 'node:test';
|
|---|
| 6 | import assert from 'node:assert/strict';
|
|---|
| 7 | import fs from 'fs';
|
|---|
| 8 | import path from 'path';
|
|---|
| 9 |
|
|---|
| 10 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 11 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 12 |
|
|---|
| 13 | const dbMod = await import('../src/config/database.js');
|
|---|
| 14 | const db = dbMod.default;
|
|---|
| 15 | dbMod.initializeDatabase();
|
|---|
| 16 | const { renderNoteBody } = await import('../src/middleware/render.js');
|
|---|
| 17 | const AP = (await import('../src/services/ActivityPubService.js')).default;
|
|---|
| 18 |
|
|---|
| 19 | const VIEWS = path.join(process.cwd(), 'src', 'views', 'partials');
|
|---|
| 20 | const EMOJI = JSON.stringify({ ':party:': 'https://cdn.test/party.png' });
|
|---|
| 21 | const MEDIA = JSON.stringify([{ url: 'https://cdn.test/capture.png', type: 'image/png' }]);
|
|---|
| 22 | const QUOTE = JSON.stringify({ url: 'https://q.test/notes/7', author: { name: 'Opie', handle: '@opie@q.test' }, content: '<p>het origineel</p>', media: [] });
|
|---|
| 23 | const EMBED = JSON.stringify({ url: 'https://video.test/watch?v=1', title: 'Een filmpje', provider: 'video.test', media: [{ url: 'https://video.test/thumb.jpg', type: 'image/jpeg' }] });
|
|---|
| 24 |
|
|---|
| 25 | test('the content renders with its custom emojis', () => {
|
|---|
| 26 | const html = renderNoteBody({ content: '<p>hoi :party:</p>', emoji_json: EMOJI }, 'nl');
|
|---|
| 27 | assert.match(html, /class="tl-content"/);
|
|---|
| 28 | assert.match(html, /<img[^>]+class="emoji"[^>]+party\.png/, ':party: became an image, not a shortcode');
|
|---|
| 29 | });
|
|---|
| 30 |
|
|---|
| 31 | test('a quoted post renders as the quote card', () => {
|
|---|
| 32 | const html = renderNoteBody({ content: '<p>kijk</p>', quote_json: QUOTE }, 'nl');
|
|---|
| 33 | assert.match(html, /class="tl-quote"/);
|
|---|
| 34 | assert.match(html, /het origineel/);
|
|---|
| 35 | assert.match(html, /@opie@q\.test/);
|
|---|
| 36 | });
|
|---|
| 37 |
|
|---|
| 38 | test('an external link preview renders as that same card, with the title escaped', () => {
|
|---|
| 39 | const html = renderNoteBody({ content: '<p>kijk</p>', embed_json: EMBED }, 'nl');
|
|---|
| 40 | assert.match(html, /class="tl-quote"/, 'one card for both, only the origin differs');
|
|---|
| 41 | assert.match(html, /Een filmpje/);
|
|---|
| 42 | assert.ok(!/<iframe/i.test(html), 'a preview is a thumbnail, never an embedded player');
|
|---|
| 43 | });
|
|---|
| 44 |
|
|---|
| 45 | test('a quote wins over a link preview: only one card', () => {
|
|---|
| 46 | const html = renderNoteBody({ content: '<p>x</p>', quote_json: QUOTE, embed_json: EMBED }, 'nl');
|
|---|
| 47 | assert.equal((html.match(/class="tl-quote"/g) || []).length, 1);
|
|---|
| 48 | assert.match(html, /het origineel/);
|
|---|
| 49 | assert.ok(!html.includes('Een filmpje'));
|
|---|
| 50 | });
|
|---|
| 51 |
|
|---|
| 52 | test('media renders, and a sensitive note keeps its veil', () => {
|
|---|
| 53 | const plain = renderNoteBody({ content: '<p>x</p>', media_json: MEDIA }, 'nl');
|
|---|
| 54 | assert.match(plain, /class="tl-media-img"/);
|
|---|
| 55 | const nsfw = renderNoteBody({ content: '<p>x</p>', media_json: MEDIA, nsfw: 1, cw: 'spoiler' }, 'nl');
|
|---|
| 56 | assert.match(nsfw, /nsfw-media/, 'the veil survives outside de Krant too');
|
|---|
| 57 | });
|
|---|
| 58 |
|
|---|
| 59 | test('an empty note renders nothing at all', () => {
|
|---|
| 60 | assert.equal(renderNoteBody({ content: '' }, 'nl'), '');
|
|---|
| 61 | assert.equal(renderNoteBody(null, 'nl'), '');
|
|---|
| 62 | });
|
|---|
| 63 |
|
|---|
| 64 | test('de Krant and Berichten both go through the shared partial', () => {
|
|---|
| 65 | for (const f of ['tl-item.ejs', 'msg-item.ejs']) {
|
|---|
| 66 | const src = fs.readFileSync(path.join(VIEWS, f), 'utf8');
|
|---|
| 67 | assert.match(src, /partials\/note-body/, `${f} must render a post through the shared partial`);
|
|---|
| 68 | assert.ok(!/class="tl-media-img"/.test(src), `${f} must not carry its own copy of the media markup`);
|
|---|
| 69 | }
|
|---|
| 70 | });
|
|---|
| 71 |
|
|---|
| 72 | test('Berichten receives the columns that partial needs', () => {
|
|---|
| 73 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 74 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'me', 'Me', 'u1', 1);
|
|---|
| 75 | db.prepare(`INSERT INTO ap_mentions (slug, object_uri, actor_uri, actor_name, actor_handle, content, emoji_json, media_json, quote_json, help_request)
|
|---|
| 76 | VALUES ('me','https://r.test/notes/1','https://r.test/u/a','Anna','@a@r.test','<p>hoi :party:</p>',?,?,?,1)`).run(EMOJI, MEDIA, QUOTE);
|
|---|
| 77 | const m = AP.getNotifications('me', 20).find((n) => n.type === 'mention');
|
|---|
| 78 | assert.ok(m);
|
|---|
| 79 | assert.equal(m.emoji_json, EMOJI);
|
|---|
| 80 | assert.equal(m.media_json, MEDIA);
|
|---|
| 81 | assert.equal(m.quote_json, QUOTE);
|
|---|
| 82 | assert.equal(m.help_request, 1, 'so Berichten can mark a 🛟 as one');
|
|---|
| 83 | // The proof: the same object, handed to the same renderer, comes out whole.
|
|---|
| 84 | const html = renderNoteBody(m, 'nl');
|
|---|
| 85 | assert.match(html, /party\.png/);
|
|---|
| 86 | assert.match(html, /class="tl-quote"/);
|
|---|
| 87 | assert.match(html, /class="tl-media-img"/);
|
|---|
| 88 | });
|
|---|