- golem15-journal-blogeditor (BlogEditorElement): light-DOM element with a TipTap WYSIWYG tab and a Source tab that edits the exact HTML string - The value is the exact served string until a user edit; external values go in with emitUpdate false; summer-change fires on user edits only and an echo set is a no-op - StarterKit without the trailing node, links without target, rel or class; injectCSS false with the ProseMirror base rules in blogeditor.css under the element tag, colours from --c-* only - Committed bundle embedded through admin.go; vitest smoke on an imported post fixture (footer, style block, spans, figure, table, entities)
99 lines
3.6 KiB
TypeScript
99 lines
3.6 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest'
|
|
import fixture from './fixtures/imported-post.html?raw'
|
|
import '../../assets/src/blogeditor/index'
|
|
import { BlogEditorElement, TAG } from '../../assets/src/blogeditor/index'
|
|
|
|
function mount(value?: string): { el: BlogEditorElement; changes: string[] } {
|
|
const el = document.createElement(TAG) as BlogEditorElement
|
|
const changes: string[] = []
|
|
el.addEventListener('summer-change', (e) => changes.push((e as CustomEvent<{ value: string }>).detail.value))
|
|
if (value !== undefined) el.value = value
|
|
document.body.append(el)
|
|
return { el, changes }
|
|
}
|
|
|
|
afterEach(() => {
|
|
document.body.replaceChildren()
|
|
})
|
|
|
|
describe('blog editor element (tracer)', () => {
|
|
it('defines the custom element tag once', () => {
|
|
expect(customElements.get(TAG)).toBe(BlogEditorElement)
|
|
expect(document.createElement(TAG)).toBeInstanceOf(BlogEditorElement)
|
|
})
|
|
|
|
it('returns the imported post byte for byte without an edit', () => {
|
|
const { el, changes } = mount(fixture)
|
|
expect(el.editor).toBeDefined()
|
|
expect(el.value).toBe(fixture)
|
|
|
|
// Setting the value after connect is untouched too.
|
|
const { el: late, changes: lateChanges } = mount()
|
|
late.value = fixture
|
|
expect(late.value).toBe(fixture)
|
|
expect(changes).toEqual([])
|
|
expect(lateChanges).toEqual([])
|
|
})
|
|
|
|
it('emits no summer-change on set or on an echo set', () => {
|
|
const { el, changes } = mount(fixture)
|
|
el.value = fixture
|
|
el.value = el.value
|
|
expect(changes).toEqual([])
|
|
expect(el.value).toBe(fixture)
|
|
})
|
|
|
|
it('emits exactly one summer-change per user edit with the element value', () => {
|
|
const { el, changes } = mount('<p>Hello</p>')
|
|
const editor = el.editor!
|
|
editor.view.dispatch(editor.state.tr.insertText('X', 1))
|
|
expect(changes).toHaveLength(1)
|
|
expect(changes[0]).toBe(el.value)
|
|
expect(el.value).toBe('<p>XHello</p>')
|
|
|
|
// The SPA echoes the emitted value back: no reset, no event.
|
|
el.value = changes[0]!
|
|
expect(changes).toHaveLength(1)
|
|
expect(el.value).toBe('<p>XHello</p>')
|
|
})
|
|
|
|
it('shows the exact value in the source tab', () => {
|
|
const { el, changes } = mount(fixture)
|
|
el.showSource(true)
|
|
const textarea = el.querySelector('textarea')!
|
|
expect(textarea.hidden).toBe(false)
|
|
expect(textarea.value).toBe(fixture)
|
|
|
|
textarea.value = '<p>Edited in source</p>\n<footer>kept</footer>'
|
|
textarea.dispatchEvent(new Event('input'))
|
|
expect(changes).toEqual(['<p>Edited in source</p>\n<footer>kept</footer>'])
|
|
expect(el.value).toBe('<p>Edited in source</p>\n<footer>kept</footer>')
|
|
|
|
// Back to WYSIWYG: re-parsed, but the value stays the exact text.
|
|
el.showSource(false)
|
|
expect(el.value).toBe('<p>Edited in source</p>\n<footer>kept</footer>')
|
|
expect(changes).toHaveLength(1)
|
|
})
|
|
|
|
it('a new external value replaces the content without an event', () => {
|
|
const { el, changes } = mount('<p>One</p>')
|
|
el.value = '<p>Two</p>'
|
|
expect(el.value).toBe('<p>Two</p>')
|
|
expect(el.editor!.state.doc.textContent).toBe('Two')
|
|
expect(changes).toEqual([])
|
|
})
|
|
|
|
it('disabled makes the editor and the source read-only; invalid sets aria-invalid', () => {
|
|
const { el } = mount('<p>x</p>')
|
|
el.setAttribute('disabled', '')
|
|
el.setAttribute('invalid', '')
|
|
expect(el.editor!.isEditable).toBe(false)
|
|
expect(el.querySelector('textarea')!.readOnly).toBe(true)
|
|
expect(el.editor!.view.dom.getAttribute('aria-invalid')).toBe('true')
|
|
el.removeAttribute('disabled')
|
|
el.removeAttribute('invalid')
|
|
expect(el.editor!.isEditable).toBe(true)
|
|
expect(el.editor!.view.dom.hasAttribute('aria-invalid')).toBe(false)
|
|
})
|
|
})
|