- A per-element Preview toggle (aria-pressed, off by default) splits the editor 50/50 with an auto-fit grid that wraps the pane below on narrow widths. - The pane is an iframe with an empty sandbox, content set only through srcdoc, created on first use; it reads only the value getter, so previewing never edits or emits summer-change.
159 lines
6.2 KiB
TypeScript
159 lines
6.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import fixture from './fixtures/imported-post.html?raw'
|
|
import { serializeDocument } from '../../assets/src/blogeditor/serializer'
|
|
import { PREVIEW_CSP } from '../../assets/src/blogeditor/preview'
|
|
import type { BlogEditorElement } from '../../assets/src/blogeditor/index'
|
|
import { button, click, mount } from './helpers'
|
|
|
|
// Wrap the real serializer so the tests can tell whether the preview path
|
|
// ever serializes an untouched document.
|
|
vi.mock('../../assets/src/blogeditor/serializer', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('../../assets/src/blogeditor/serializer')>()
|
|
return { ...actual, serializeDocument: vi.fn(actual.serializeDocument) }
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
vi.mocked(serializeDocument).mockClear()
|
|
document.body.replaceChildren()
|
|
})
|
|
|
|
function pane(el: BlogEditorElement): HTMLElement {
|
|
const found = el.querySelector<HTMLElement>('.blogeditor__preview')
|
|
if (!found) throw new Error('no preview pane')
|
|
return found
|
|
}
|
|
|
|
function frame(el: BlogEditorElement): HTMLIFrameElement {
|
|
const found = el.querySelector<HTMLIFrameElement>('.blogeditor__preview iframe')
|
|
if (!found) throw new Error('no preview frame')
|
|
return found
|
|
}
|
|
|
|
function srcdoc(el: BlogEditorElement): string {
|
|
return frame(el).getAttribute('srcdoc') ?? ''
|
|
}
|
|
|
|
describe('blog editor preview: toggle', () => {
|
|
it('is off by default and toggles the split pane with aria-pressed', () => {
|
|
const { el } = mount('<p>Hello</p>')
|
|
const toggle = button(el, 'preview')
|
|
expect(toggle.textContent).toBe('Preview')
|
|
expect(toggle.getAttribute('aria-label')).toBe('Preview')
|
|
expect(toggle.getAttribute('aria-pressed')).toBe('false')
|
|
expect(pane(el).hidden).toBe(true)
|
|
|
|
click(toggle)
|
|
expect(toggle.getAttribute('aria-pressed')).toBe('true')
|
|
expect(pane(el).hidden).toBe(false)
|
|
expect(el.querySelector('.blogeditor__panes')!.classList.contains('blogeditor__panes--split')).toBe(true)
|
|
// Rendered right away, with no timer advanced.
|
|
expect(srcdoc(el)).toContain('<p>Hello</p>')
|
|
|
|
click(toggle)
|
|
expect(toggle.getAttribute('aria-pressed')).toBe('false')
|
|
expect(pane(el).hidden).toBe(true)
|
|
expect(el.querySelector('.blogeditor__panes')!.classList.contains('blogeditor__panes--split')).toBe(false)
|
|
})
|
|
|
|
it('is labelled through the ui-locale', () => {
|
|
const { el: pl } = mount('<p>x</p>', { 'ui-locale': 'pl' })
|
|
expect(button(pl, 'preview').textContent).toBe('Podgląd')
|
|
expect(button(pl, 'preview').getAttribute('aria-label')).toBe('Podgląd')
|
|
click(button(pl, 'preview'))
|
|
expect(frame(pl).title).toBe('Podgląd wpisu')
|
|
|
|
const { el: en } = mount('<p>x</p>', { 'ui-locale': 'en' })
|
|
click(button(en, 'preview'))
|
|
expect(frame(en).title).toBe('Post preview')
|
|
})
|
|
|
|
it('creates no iframe until the preview is first switched on', () => {
|
|
const { el } = mount('<p>x</p>')
|
|
expect(el.querySelector('iframe')).toBeNull()
|
|
click(button(el, 'preview'))
|
|
expect(el.querySelectorAll('iframe')).toHaveLength(1)
|
|
click(button(el, 'preview'))
|
|
click(button(el, 'preview'))
|
|
expect(el.querySelectorAll('iframe')).toHaveLength(1)
|
|
})
|
|
|
|
it('keeps its state per element', () => {
|
|
const { el: first } = mount('<p>one</p>')
|
|
const { el: second } = mount('<p>two</p>')
|
|
click(button(first, 'preview'))
|
|
expect(button(first, 'preview').getAttribute('aria-pressed')).toBe('true')
|
|
expect(button(second, 'preview').getAttribute('aria-pressed')).toBe('false')
|
|
expect(pane(second).hidden).toBe(true)
|
|
})
|
|
|
|
it('stays usable in the Source tab and when disabled', () => {
|
|
const { el: source } = mount('<p>s</p>')
|
|
source.showSource(true)
|
|
expect(button(source, 'preview').disabled).toBe(false)
|
|
click(button(source, 'preview'))
|
|
expect(button(source, 'preview').getAttribute('aria-pressed')).toBe('true')
|
|
expect(pane(source).hidden).toBe(false)
|
|
|
|
const { el: disabled } = mount('<p>d</p>', { disabled: '' })
|
|
expect(button(disabled, 'preview').disabled).toBe(false)
|
|
click(button(disabled, 'preview'))
|
|
expect(button(disabled, 'preview').getAttribute('aria-pressed')).toBe('true')
|
|
expect(pane(disabled).hidden).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('blog editor preview: sandbox', () => {
|
|
it('uses an empty sandbox and only srcdoc', () => {
|
|
const { el } = mount('<p>x</p>')
|
|
click(button(el, 'preview'))
|
|
const f = frame(el)
|
|
expect(f.getAttribute('sandbox')).toBe('')
|
|
const sandbox = f.getAttribute('sandbox') ?? ''
|
|
expect(sandbox).not.toMatch(/allow-scripts/)
|
|
expect(sandbox).not.toMatch(/allow-same-origin/)
|
|
expect(f.hasAttribute('src')).toBe(false)
|
|
expect(f.hasAttribute('allow')).toBe(false)
|
|
expect(f.getAttribute('referrerpolicy')).toBe('no-referrer')
|
|
const doc = srcdoc(el)
|
|
expect(doc).toContain(`<meta http-equiv="Content-Security-Policy" content="${PREVIEW_CSP}">`)
|
|
expect(doc).toContain('<meta name="referrer" content="no-referrer">')
|
|
// The head starts with the charset, then the CSP, then the referrer meta.
|
|
expect(doc.indexOf('<meta charset="utf-8">')).toBeLessThan(doc.indexOf('Content-Security-Policy'))
|
|
expect(doc.indexOf('Content-Security-Policy')).toBeLessThan(doc.indexOf('name="referrer"'))
|
|
})
|
|
})
|
|
|
|
describe('blog editor preview: value contract', () => {
|
|
it('never touches an untouched imported post', () => {
|
|
const { el, editor, changes } = mount(fixture)
|
|
const before = editor.state.doc
|
|
const toggle = button(el, 'preview')
|
|
click(toggle)
|
|
click(toggle)
|
|
click(toggle)
|
|
expect(changes).toEqual([])
|
|
expect(el.value).toBe(fixture)
|
|
expect(editor.state.doc).toBe(before)
|
|
expect(serializeDocument).not.toHaveBeenCalled()
|
|
expect(srcdoc(el)).toContain('Jan Kowalski')
|
|
|
|
el.value = fixture
|
|
expect(changes).toEqual([])
|
|
})
|
|
|
|
it('reads a touched document through the value getter without an event', () => {
|
|
const { el, editor, changes } = mount('<p>a</p>')
|
|
editor.view.dispatch(editor.state.tr.insertText('X', 1))
|
|
const toggle = button(el, 'preview')
|
|
click(toggle)
|
|
expect(changes).toHaveLength(1)
|
|
expect(srcdoc(el)).toContain('<p>Xa</p>')
|
|
expect(serializeDocument).toHaveBeenCalled()
|
|
|
|
click(toggle)
|
|
click(toggle)
|
|
expect(changes).toHaveLength(1)
|
|
})
|
|
})
|