Files
edentext/tests/unit/notes.test.ts
T
Steffen Becker bf564fbab4 fix: footnote counting per page/chapter and collecting at the end really work
Both were selectable in the dialog and changed nothing — they only rode the
file formats, so an imported document numbered and placed its notes wrong.
noteLabels now counts within a bucket (class, chapter, or the page pageBreaks
publishes through setNoteAnchorPages); a collected footnote leaves position:
absolute and opens its own page, as LibreOffice does. Counting and position
move to the footnote tab, the only place LibreOffice offers them.

The separator's own alignment moves the line alone — probed, notes stay left.
2026-08-14 23:11:10 +02:00

82 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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', chapter = 0): NoteRefInfo =>
({ id, kind, pos: 0, chapter });
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']);
});
// LibreOffice, probed: the count starts over on each page / at each chapter.
it('restarts per page, from the page each anchor landed on', () => {
const refs = [REF('a', 'footnote'), REF('b', 'footnote'), REF('c', 'footnote')];
const pages = new Map([['a', 1], ['b', 2], ['c', 2]]);
const labels = noteLabels(refs, withFootnote({ restart: 'page' }), pages);
expect([...labels.values()]).toEqual(['1', '1', '2']);
});
it('leaves an endnote document-wide: LibreOffice offers no per-page count for one', () => {
const refs = [REF('a', 'endnote'), REF('b', 'endnote')];
const s: NoteSettings = { ...DEFAULT_NOTE_SETTINGS, endnote: { ...DEFAULT_NOTE_SETTINGS.endnote, restart: 'page' } };
expect([...noteLabels(refs, s, new Map()).values()]).toEqual(['i', 'ii']);
});
it('restarts per chapter, off the anchor’s own chapter', () => {
const refs = [REF('a', 'footnote', 1), REF('b', 'footnote', 1), REF('c', 'footnote', 2)];
expect([...noteLabels(refs, withFootnote({ restart: 'chapter' })).values()]).toEqual(['1', '2', '1']);
});
});
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);
});
});