import { afterEach, describe, expect, it, vi } from 'vitest' import { labelsFor } from '../../assets/src/blogeditor/labels' import { factsFromEditor, factsFromSource, headingFlags, isGenericAnchor, normalizeAnchor, OUTLINE_DEBOUNCE_MS } from '../../assets/src/blogeditor/outline' import { fill, insertAtBlock, mount } from './helpers' // J2 outline (UI-SPEC J2): the facts read from the editor and from the // Source tab, the flags per heading and the debounce. The rendered panel // is in additions.test.ts. afterEach(() => { vi.useRealTimers() document.body.replaceChildren() }) const en = labelsFor('en') const ON = JSON.stringify({ headingLevels: [2, 3, 4], genericAnchors: ['click here', 'more'] }) const OFF = JSON.stringify({ headingLevels: [1, 2, 3, 4], genericAnchors: ['click here'] }) describe('outline facts', () => { it('reads headings and link texts from HTML source', () => { const facts = factsFromSource('

Intro text

Only links

See click here and

') expect(facts.headings).toEqual([ { level: 2, text: 'Intro text', linkOnly: false, pos: -1 }, { level: 4, text: 'Only links', linkOnly: true, pos: -1 }, { level: 3, text: '', linkOnly: false, pos: -1 }, ]) expect(facts.links).toEqual(['Only', 'links', 'click here']) }) it('reads the same facts from the editor document, adjacent marks of one link joined', () => { const { editor } = mount('

Intro

All link

click here more

', { options: ON }) const facts = factsFromEditor(editor) expect(facts.headings.map((h) => [h.level, h.text, h.linkOnly])).toEqual([ [2, 'Intro', false], [3, 'All link', true], ]) expect(facts.headings.every((h) => h.pos >= 0)).toBe(true) // A link inside a heading is a link text too. expect(facts.links).toEqual(['All link', 'click here', 'more']) }) }) describe('heading flags', () => { const facts = factsFromSource('

Top

Skip

Link

Deep

') it('flags skipped levels from the page title H1, empty and link-only headings', () => { const flags = headingFlags(facts, en, { allowsH1: true, source: true }) expect(flags[0]).toEqual([]) expect(flags[1]).toEqual(['Skipped level: H1 → H3']) expect(flags[2]).toEqual([en.flagEmpty]) expect(flags[3]).toEqual([en.flagLinkOnly]) expect(flags[4]).toEqual(['Skipped level: H2 → H4']) }) it('flags an H1 only in the Source tab while level 1 is not offered', () => { expect(headingFlags(facts, en, { allowsH1: false, source: true })[0]).toEqual([en.flagH1]) expect(headingFlags(facts, en, { allowsH1: false, source: false })[0]).toEqual([]) expect(headingFlags(facts, en, { allowsH1: true, source: true })[0]).toEqual([]) expect(headingFlags({ headings: [], links: [] }, en, { allowsH1: false, source: true })).toEqual([]) }) }) describe('generic link texts', () => { it('normalises and matches the generic list', () => { expect(normalizeAnchor(' Kliknij TUTAJ!!! ')).toBe('kliknij tutaj') expect(normalizeAnchor('»Więcej«')).toBe('więcej') expect(isGenericAnchor('Click here.', ['click here'])).toBe(true) expect(isGenericAnchor('Click here to read the study', ['click here'])).toBe(false) expect(isGenericAnchor('!!!', [''])).toBe(false) }) }) describe('outline timing', () => { it('recomputes the Source tab facts only after the 300 ms debounce and never edits the value', () => { vi.useFakeTimers() expect(OUTLINE_DEBOUNCE_MS).toBe(300) const { el, editor, changes } = mount('

One

', { options: OFF }) const toggle = el.querySelector('.blogeditor__outline-toggle')! insertAtBlock(editor, 0, 'a') vi.advanceTimersByTime(OUTLINE_DEBOUNCE_MS) el.showSource(true) fill(el.querySelector('textarea'), '

One

Two

Three

') vi.advanceTimersByTime(OUTLINE_DEBOUNCE_MS - 50) expect(toggle.textContent).toBe('Outline · 1 heading') fill(el.querySelector('textarea'), '

One

Two

') vi.advanceTimersByTime(OUTLINE_DEBOUNCE_MS - 50) expect(toggle.textContent).toBe('Outline · 1 heading') vi.advanceTimersByTime(50) expect(toggle.textContent).toBe('Outline · 2 headings') expect(changes).toHaveLength(3) }) })