- outline: facts from the editor and the Source tab, skipped, empty and link-only headings, the H1 flag only in the Source tab while level 1 is not offered, generic texts, the 300 ms debounce without edits - link form: rel follows the URL as typed, only checked tokens written, none for internal links, the generic warning never blocks - image form: the slug rule examples, decorative clears and locks alt
93 lines
4.4 KiB
TypeScript
93 lines
4.4 KiB
TypeScript
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('<h2> Intro text </h2><h4><a href="/a">Only</a> <a href="/b">links</a></h4><h3></h3><p>See <a href="/c">click here</a> and <a href="/d"> </a></p>')
|
|
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('<h2>Intro</h2><h3><a href="/x">All link</a></h3><p><a href="/y">click </a><a href="/y"><strong>here</strong></a> <a href="/z">more</a></p>', { 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('<h1>Top</h1><h3>Skip</h3><h2></h2><h2><a href="/x">Link</a></h2><h4>Deep</h4>')
|
|
|
|
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('<h2>One</h2>', { options: OFF })
|
|
const toggle = el.querySelector<HTMLButtonElement>('.blogeditor__outline-toggle')!
|
|
insertAtBlock(editor, 0, 'a')
|
|
vi.advanceTimersByTime(OUTLINE_DEBOUNCE_MS)
|
|
el.showSource(true)
|
|
fill(el.querySelector('textarea'), '<h2>One</h2><h2>Two</h2><h2>Three</h2>')
|
|
vi.advanceTimersByTime(OUTLINE_DEBOUNCE_MS - 50)
|
|
expect(toggle.textContent).toBe('Outline · 1 heading')
|
|
fill(el.querySelector('textarea'), '<h2>One</h2><h2>Two</h2>')
|
|
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)
|
|
})
|
|
})
|