source: Klonkt/test/media-alt.test.js@ d18c60e

main
Last change on this file since d18c60e was d18c60e, checked in by roboburr <roboburr@…>, 2 months ago

feat(fediverse): alt text for images (accessibility → AS2 attachment name)

Media federated to the fediverse now carries a description (AS2 name on the
attachment), which Mastodon shows and screen readers read. The cover gets an alt
field in the editor; inline images keep their own <img alt="…">. Also used as the
cover's on-site alt.

  • src/config/database.js — posts.cover_alt column.
  • src/services/ActivityPubService.js — buildNote carries alt per media URL (cover_alt + inline <img alt>) and emits it as the attachment name (and on the image fallback).
  • src/routes/posts.js — capture/store cover_alt on create/save and pass it to the federation hooks.
  • src/services/Scheduler.js — carry cover_alt when a scheduled post goes live.
  • src/views/pages/post-edit.ejs — "Alt text (description)" field under the cover URL.
  • src/views/pages/post.ejs — the on-page cover <img> uses cover_alt.
  • src/services/i18n.js — pedit.f_cover_alt + pedit.cover_alt_placeholder (nl/en/de).
  • test/media-alt.test.js — cover alt, inline alt, and no-alt = no name.
  • CHANGELOG(.nl/.de).md — "Alt text for images" under Unreleased.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 2.2 KB
Line 
1// Media alt text — the cover's alt and inline <img alt="…"> federate as the AS2 attachment
2// `name` (accessibility). In-memory SQLite. Run: npm test
3import { test } from 'node:test';
4import assert from 'node:assert/strict';
5
6process.env.DATABASE_PATH = ':memory:';
7process.env.PUBLIC_BASE_URL = 'https://test.example';
8
9const dbMod = await import('../src/config/database.js');
10const db = dbMod.default;
11dbMod.initializeDatabase();
12const AP = (await import('../src/services/ActivityPubService.js')).default;
13
14const BASE = 'https://test.example';
15db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
16db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'demo', 'Demo', 'u1', 1);
17const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
18site.primary_slug = 'demo';
19
20test('the cover alt federates as the attachment name', () => {
21 const post = { id: 'a1', slug: 'x', title: '', content: '<p>hi</p>', tags: '[]', cover_image_url: '/media/c.webp', cover_alt: 'A red bicycle', created_at: '2026-01-01T00:00:00Z' };
22 const note = AP.buildNote(BASE, site, post);
23 const att = (note.attachment || []).find((a) => a.url.endsWith('c.webp'));
24 assert.ok(att, 'cover attachment present');
25 assert.equal(att.name, 'A red bicycle');
26});
27
28test('an inline <img alt="…"> federates its alt as the attachment name', () => {
29 const post = { id: 'a2', slug: 'y', title: '', content: '<p><img src="/media/cat.png" alt="a sleeping cat"></p>', tags: '[]', created_at: '2026-01-01T00:00:00Z' };
30 const note = AP.buildNote(BASE, site, post);
31 const att = (note.attachment || []).find((a) => a.url.endsWith('cat.png'));
32 assert.ok(att, 'inline image attachment present');
33 assert.equal(att.name, 'a sleeping cat');
34});
35
36test('no alt → no name field (stays valid AS2)', () => {
37 const post = { id: 'a3', slug: 'z', title: '', content: '<p>x</p>', tags: '[]', cover_image_url: '/media/n.webp', created_at: '2026-01-01T00:00:00Z' };
38 const note = AP.buildNote(BASE, site, post);
39 const att = (note.attachment || []).find((a) => a.url.endsWith('n.webp'));
40 assert.ok(att, 'cover attachment present');
41 assert.ok(!('name' in att), 'no name when no alt');
42});
Note: See TracBrowser for help on using the repository browser.