Files
edentext/tests/unit/notes.test.ts
T
Steffen Becker bbbe72bd22 feat: a note renders in the file's own style, marker included
The marker moves from a CSS ::before to a widget decoration: generated
content is no text node, so it reached neither the PDF nor a measurement.
The note also carries the paragraph style its file names, so font, size and
colour are the document's; only the hanging indent stays the editor's,
because the style model has no first-line half to pair with the margin.
Adds a unit test for the numbering and the settings, plus the architecture
doc and the two open parity findings.
2026-08-13 12:17:45 +00:00

62 lines
2.9 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { noteLabel, noteLabels, type NoteRefInfo } from '../../src/lib/editor/extensions/notes';
import { clampNoteSettings, DEFAULT_NOTE_SETTINGS, type NoteSettings } from '../../src/lib/storage/noteSettings';
const REF = (id: string, kind: 'footnote' | 'endnote'): NoteRefInfo => ({ id, kind, pos: 0 });
const withFootnote = (over: Partial<NoteSettings['footnote']>): NoteSettings =>
({ ...DEFAULT_NOTE_SETTINGS, footnote: { ...DEFAULT_NOTE_SETTINGS.footnote, ...over } });
describe('note numbering', () => {
it('follows LibreOffice by default: footnotes arabic, endnotes lower roman', () => {
const s = DEFAULT_NOTE_SETTINGS;
expect([0, 1, 2].map((i) => noteLabel(i, 'footnote', s))).toEqual(['1', '2', '3']);
expect([0, 1, 2].map((i) => noteLabel(i, 'endnote', s))).toEqual(['i', 'ii', 'iii']);
});
it('renders all five ODF formats', () => {
const at = (f: NoteSettings['footnote']['numFormat']) =>
[0, 1, 2].map((i) => noteLabel(i, 'footnote', withFootnote({ numFormat: f })));
expect(at('1')).toEqual(['1', '2', '3']);
expect(at('a')).toEqual(['a', 'b', 'c']);
expect(at('A')).toEqual(['A', 'B', 'C']);
expect(at('i')).toEqual(['i', 'ii', 'iii']);
expect(at('I')).toEqual(['I', 'II', 'III']);
});
it('counts from the start value and wraps the number in prefix/suffix', () => {
const s = withFootnote({ startAt: 4, prefix: '[', suffix: ']' });
expect([0, 1].map((i) => noteLabel(i, 'footnote', s))).toEqual(['[4]', '[5]']);
});
it('keeps a note the file numbered by hand', () => {
expect(noteLabel(0, 'footnote', DEFAULT_NOTE_SETTINGS, '*')).toBe('*');
});
it('counts the two classes separately, in anchor order', () => {
const refs = [REF('a', 'footnote'), REF('b', 'endnote'), REF('c', 'footnote'), REF('d', 'endnote')];
const labels = noteLabels(refs, DEFAULT_NOTE_SETTINGS);
expect([...labels.values()]).toEqual(['1', 'i', '2', 'ii']);
});
});
describe('note settings', () => {
it('falls back to the defaults for anything a stored value gets wrong', () => {
const s = clampNoteSettings({ footnote: { numFormat: 'z', startAt: -3, restart: 'nonsense' }, separator: { relWidthPercent: 500 } });
expect(s.footnote.numFormat).toBe('1');
expect(s.footnote.startAt).toBe(1);
expect(s.footnote.restart).toBe('document');
expect(s.separator.relWidthPercent).toBe(100);
});
it('keeps values that are in range', () => {
const s = clampNoteSettings({ ...DEFAULT_NOTE_SETTINGS, footnote: { ...DEFAULT_NOTE_SETTINGS.footnote, numFormat: 'I', startAt: 7, restart: 'page' } });
expect(s.footnote).toMatchObject({ numFormat: 'I', startAt: 7, restart: 'page' });
});
it('survives a garbage payload whole', () => {
expect(clampNoteSettings(null)).toEqual(DEFAULT_NOTE_SETTINGS);
expect(clampNoteSettings('nope')).toEqual(DEFAULT_NOTE_SETTINGS);
});
});