Files
task-stream/libs/web/tracker/presenters/src/lib/ui/rich-text.spec.ts
Josep Piguillem 836c6f63bc feat(web): write descriptions and comments with a visual editor
ts-rich-editor (TipTap) shows the formatting while writing: bold,
italic, strike, code, headings, lists, task lists, quotes, code blocks
and links, with @ suggesting the project's members and pasted
screenshots uploaded as attachments. It reads and writes the same
Markdown the API keeps (mentions as @[Name](id), images as
attachment:<id>, tables too), so nothing already written changes.

Used for comments, replies, editing a comment or a description, a new
ticket from the board and the public proposal form; the Markdown hint
and the paste directive are gone. List markers sat on a line of their
own, and task lists showed a stray bullet, in the editor and when read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:00:41 +02:00

89 lines
2.3 KiB
TypeScript

import { Editor } from '@tiptap/core';
import { markdownOf, richTextExtensions } from './rich-text';
/** Markdown in, the editor's Markdown out: what an edit that changes nothing saves. */
function roundTrip(markdown: string): string {
const editor = new Editor({
element: document.createElement('div'),
extensions: richTextExtensions(),
content: markdown,
contentType: 'markdown',
});
const out = markdownOf(editor);
editor.destroy();
return out;
}
describe('rich text', () => {
it('keeps mentions as the API reads them', () => {
expect(roundTrip('Hola @[Ana López](101), ¿lo miras?')).toBe(
'Hola @[Ana López](101), ¿lo miras?',
);
});
it('keeps pasted images pointing at their attachment', () => {
expect(roundTrip('Mira:\n\n![captura.png](attachment:abc-123)')).toContain(
'![captura.png](attachment:abc-123)',
);
});
it('keeps formatting, lists, task lists, quotes, code and links', () => {
const text = [
'## Pasos',
'',
'Texto con **negrita**, *cursiva*, ~~tachado~~ y `código`.',
'',
'- uno',
'- dos',
'',
'1. primero',
'2. segundo',
'',
'- [ ] pendiente',
'- [x] hecho',
'',
'> una cita',
'',
'```',
'const a = 1;',
'```',
'',
'[enlace](https://example.com)',
].join('\n');
const out = roundTrip(text);
for (const piece of [
'## Pasos',
'**negrita**',
'*cursiva*',
'~~tachado~~',
'`código`',
'- uno',
'1. primero',
'- [ ] pendiente',
'- [x] hecho',
'> una cita',
'const a = 1;',
'[enlace](https://example.com)',
])
expect(out).toContain(piece);
});
it('keeps tables', () => {
const out = roundTrip('| a | b |\n| --- | --- |\n| 1 | 2 |');
expect(out).toMatch(/\|\s*a\s*\|\s*b\s*\|/);
expect(out).toMatch(/\|\s*1\s*\|\s*2\s*\|/);
});
it('answers an empty string for an empty editor', () => {
expect(roundTrip('')).toBe('');
});
});
describe('rich text, line breaks', () => {
it('keeps a single line break as a line break', () => {
const out = roundTrip('primera línea\nsegunda línea');
expect(out).not.toBe('primera línea segunda línea');
expect(out).toMatch(/primera línea(\s{2}|\\)?\nsegunda línea/);
});
});