feat: editor heading levels, outline, link rel, generic-anchor warning and image naming
- the blogeditor and mlblogeditor types serve headingLevels and genericAnchors through RequestOptions, per schema request - J1: H2 to H4 with a pasted or loaded H1 parsed as H2 while the H1 setting is on, H1 to H4 while it is off; H5 and H6 still parse - J2: the outline bar and panel (skipped, empty, link-only, H1 in Source, generic link texts), 300 ms after a change, collapsed, read-only - J3: nofollow/sponsored/ugc checkboxes for absolute http(s) links only, and a non-blocking warning for a generic link text - J4: one form per picked file with the slugged file name, alt required unless decorative (alt=""); Insert uploads the renamed file and the image is inserted on the upload result - assets/dist rebuilt
This commit is contained in:
209
tests/blogeditor/additions.test.ts
Normal file
209
tests/blogeditor/additions.test.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { readOptions } from '../../assets/src/blogeditor/element'
|
||||
import { mediaFileName, slugFileStem } from '../../assets/src/blogeditor/imageform'
|
||||
import { labelsFor, plural } from '../../assets/src/blogeditor/labels'
|
||||
import { isExternalUrl } from '../../assets/src/blogeditor/linkform'
|
||||
import { normalizeAnchor, OUTLINE_DEBOUNCE_MS } from '../../assets/src/blogeditor/outline'
|
||||
import { button, chooseImage, click, fill, form, insertAtBlock, mount, status } from './helpers'
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
document.body.replaceChildren()
|
||||
})
|
||||
|
||||
const ON = JSON.stringify({ headingLevels: [2, 3, 4], genericAnchors: ['click here', 'more', 'kliknij tutaj'] })
|
||||
const OFF = JSON.stringify({ headingLevels: [1, 2, 3, 4], genericAnchors: ['click here'] })
|
||||
|
||||
function outlineToggle(el: HTMLElement): HTMLButtonElement {
|
||||
return el.querySelector<HTMLButtonElement>('.blogeditor__outline-toggle')!
|
||||
}
|
||||
|
||||
function openOutline(el: HTMLElement): HTMLElement {
|
||||
click(outlineToggle(el))
|
||||
return el.querySelector<HTMLElement>('.blogeditor__outline-panel')!
|
||||
}
|
||||
|
||||
describe('options', () => {
|
||||
it('reads headingLevels and genericAnchors with defaults', () => {
|
||||
expect(readOptions(null)).toEqual({ headingLevels: [2, 3, 4], genericAnchors: expect.arrayContaining(['click here', 'czytaj więcej']) })
|
||||
expect(readOptions('not json').headingLevels).toEqual([2, 3, 4])
|
||||
expect(readOptions(JSON.stringify({ headingLevels: [4, 1, 9, 'x', 2.5, 2], genericAnchors: [' Read More! ', 3] }))).toEqual({
|
||||
headingLevels: [1, 2, 4],
|
||||
genericAnchors: ['read more'],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('J1 heading levels', () => {
|
||||
it('offers H2 to H4 and parses an H1 as an H2 while the H1 setting is on', () => {
|
||||
const { el, editor } = mount('<h1 class="lead">Title</h1><p>x</p><h5>Small</h5>', { options: ON })
|
||||
expect(el.querySelector('button[data-command="heading1"]')).toBeNull()
|
||||
expect(button(el, 'heading2')).toBeTruthy()
|
||||
const first = editor.state.doc.firstChild!
|
||||
expect(first.type.name).toBe('heading')
|
||||
expect(first.attrs.level).toBe(2)
|
||||
// Untouched, the value is the exact string (D-03).
|
||||
expect(el.value).toBe('<h1 class="lead">Title</h1><p>x</p><h5>Small</h5>')
|
||||
insertAtBlock(editor, 1, 'y')
|
||||
expect(el.value).toBe('<h1 class="lead">Title</h1><p>yx</p><h5>Small</h5>')
|
||||
insertAtBlock(editor, 0, 'A ')
|
||||
expect(el.value).toBe('<h2 class="lead">A Title</h2><p>yx</p><h5>Small</h5>')
|
||||
expect(editor.can().toggleHeading({ level: 1 as 2 })).toBe(false)
|
||||
})
|
||||
|
||||
it('offers H1 to H4 and keeps an H1 while the H1 setting is off', () => {
|
||||
const { el, editor } = mount('<h1>Title</h1>', { options: OFF })
|
||||
const commands = Array.from(el.querySelectorAll<HTMLButtonElement>('.blogeditor__group button')).map((b) => b.dataset.command)
|
||||
expect(commands.slice(0, 5)).toEqual(['paragraph', 'heading1', 'heading2', 'heading3', 'heading4'])
|
||||
expect(button(el, 'heading1').getAttribute('aria-label')).toBe('Heading 1')
|
||||
insertAtBlock(editor, 0, 'A ')
|
||||
expect(el.value).toBe('<h1>A Title</h1>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('J2 outline', () => {
|
||||
it('starts collapsed, never takes focus and counts headings and problems', () => {
|
||||
const { el } = mount('<h3>Deep</h3><h2></h2><h2><a href="/x">Only link</a></h2><p><a href="/y">click here</a></p>', { options: ON })
|
||||
const toggle = outlineToggle(el)
|
||||
expect(toggle.getAttribute('aria-expanded')).toBe('false')
|
||||
expect(document.activeElement).not.toBe(toggle)
|
||||
expect(el.querySelector<HTMLElement>('.blogeditor__outline-panel')!.hidden).toBe(true)
|
||||
expect(toggle.textContent).toBe('Outline · 3 headings4 problems')
|
||||
const panel = openOutline(el)
|
||||
expect(toggle.getAttribute('aria-expanded')).toBe('true')
|
||||
const items = Array.from(panel.querySelectorAll('li'))
|
||||
expect(items.map((li) => li.querySelector('.blogeditor__outline-level')!.textContent)).toEqual(['H3', 'H2', 'H2'])
|
||||
expect(items[0]!.textContent).toContain('Skipped level: H1 → H3')
|
||||
expect(items[1]!.textContent).toContain('Empty heading')
|
||||
expect(items[2]!.textContent).toContain('Heading is only a link')
|
||||
expect(items[0]!.style.getPropertyValue('--blogeditor-outline-depth')).toBe('1')
|
||||
expect(panel.querySelector('.blogeditor__outline-links')!.textContent).toBe('LinksLink text “click here” does not say where it leads')
|
||||
})
|
||||
|
||||
it('shows outlineEmpty without headings and uses Polish plurals', () => {
|
||||
const { el } = mount('<p>Tekst</p>', { options: ON, 'ui-locale': 'pl' })
|
||||
expect(outlineToggle(el).textContent).toBe('Konspekt · 0 nagłówków')
|
||||
expect(openOutline(el).textContent).toBe('Brak nagłówków. Nagłówki dzielą tekst na części dla czytelników i wyszukiwarek.')
|
||||
const pl = labelsFor('pl')
|
||||
expect(plural(pl, pl.outlineCount, 1)).toBe('1 nagłówek')
|
||||
expect(plural(pl, pl.outlineCount, 3)).toBe('3 nagłówki')
|
||||
expect(plural(pl, pl.outlineCount, 5)).toBe('5 nagłówków')
|
||||
expect(plural(pl, pl.outlineProblems, 22)).toBe('22 problemy')
|
||||
})
|
||||
|
||||
it('recomputes 300 ms after a change and flags an H1 in the Source tab while the setting is on', () => {
|
||||
vi.useFakeTimers()
|
||||
const { el, editor, changes } = mount('<h2>One</h2>', { options: ON })
|
||||
openOutline(el)
|
||||
insertAtBlock(editor, 0, 'x')
|
||||
expect(outlineToggle(el).textContent).toBe('Outline · 1 heading')
|
||||
el.showSource(true)
|
||||
fill(el.querySelector('textarea'), '<h1>Top</h1><h2>Two</h2>')
|
||||
vi.advanceTimersByTime(OUTLINE_DEBOUNCE_MS - 1)
|
||||
expect(outlineToggle(el).textContent).toBe('Outline · 1 heading')
|
||||
vi.advanceTimersByTime(1)
|
||||
expect(outlineToggle(el).textContent).toBe('Outline · 2 headings1 problem')
|
||||
expect(el.querySelector('.blogeditor__outline-panel li')!.textContent).toContain('H1 in the text: saved as H2')
|
||||
// The outline never edits: only the two user edits changed the value.
|
||||
expect(changes).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('does not flag an H1 while the setting is off', () => {
|
||||
const { el } = mount('<h1>Top</h1><h2>Two</h2>', { options: OFF })
|
||||
el.showSource(true)
|
||||
expect(outlineToggle(el).textContent).toBe('Outline · 2 headings')
|
||||
})
|
||||
})
|
||||
|
||||
describe('J3 link form', () => {
|
||||
it('offers rel only for absolute http(s) URLs and writes the checked tokens', () => {
|
||||
const { el, editor } = mount('<p>Partner</p>', { options: ON })
|
||||
editor.commands.setTextSelection({ from: 1, to: 8 })
|
||||
click(button(el, 'link'))
|
||||
const linkForm = form(el, 'link')!
|
||||
const box = (token: string) => linkForm.querySelector<HTMLInputElement>(`input[name="rel-${token}"]`)!
|
||||
expect(linkForm.querySelector('legend')!.textContent).toBe('Link attributes')
|
||||
expect(box('nofollow').disabled).toBe(true)
|
||||
expect(linkForm.querySelector<HTMLElement>('.blogeditor__hint')!.hidden).toBe(false)
|
||||
fill(linkForm.querySelector('input[name="url"]'), 'https://partner.test/')
|
||||
expect(box('nofollow').disabled).toBe(false)
|
||||
expect(linkForm.querySelector<HTMLElement>('.blogeditor__hint')!.hidden).toBe(true)
|
||||
box('nofollow').checked = true
|
||||
box('sponsored').checked = true
|
||||
click(linkForm.querySelector('[data-action="apply"]'))
|
||||
expect(el.value).toBe('<p><a rel="nofollow sponsored" href="https://partner.test/">Partner</a></p>')
|
||||
|
||||
editor.commands.setTextSelection(3)
|
||||
click(button(el, 'link'))
|
||||
const again = form(el, 'link')!
|
||||
expect(again.querySelector<HTMLInputElement>('input[name="rel-sponsored"]')!.checked).toBe(true)
|
||||
fill(again.querySelector('input[name="url"]'), '/blog/partner')
|
||||
click(again.querySelector('[data-action="apply"]'))
|
||||
expect(el.value).toBe('<p><a href="/blog/partner">Partner</a></p>')
|
||||
})
|
||||
|
||||
it('warns about a generic link text without blocking Apply', () => {
|
||||
const { el, editor } = mount('<p>Click here!</p>', { options: ON })
|
||||
editor.commands.setTextSelection({ from: 1, to: 12 })
|
||||
click(button(el, 'link'))
|
||||
const linkForm = form(el, 'link')!
|
||||
const warning = linkForm.querySelector<HTMLElement>('[data-warning]')!
|
||||
expect(warning.hidden).toBe(false)
|
||||
expect(warning.getAttribute('aria-live')).toBe('polite')
|
||||
expect(warning.textContent).toBe('“Click here!” does not say where the link leads. Use words that describe the target page.')
|
||||
fill(linkForm.querySelector('input[name="url"]'), '/blog/x')
|
||||
click(linkForm.querySelector('[data-action="apply"]'))
|
||||
expect(el.value).toBe('<p><a href="/blog/x">Click here!</a></p>')
|
||||
})
|
||||
|
||||
it('treats only absolute http(s) URLs as external', () => {
|
||||
for (const url of ['https://a.test', 'HTTP://a.test/x']) expect(isExternalUrl(url), url).toBe(true)
|
||||
for (const url of ['/a', 'a.test', 'mailto:a@b.test', '//a.test', 'https://', '#x']) expect(isExternalUrl(url), url).toBe(false)
|
||||
expect(normalizeAnchor(' Read MORE… ')).toBe('read more')
|
||||
})
|
||||
})
|
||||
|
||||
describe('J4 image naming', () => {
|
||||
const pick = (el: HTMLElement, files: File[]) => {
|
||||
const input = el.querySelector<HTMLInputElement>('input[type="file"]')!
|
||||
Object.defineProperty(input, 'files', { value: files, configurable: true })
|
||||
input.dispatchEvent(new Event('change'))
|
||||
}
|
||||
const answer = (el: HTMLElement, detail: Record<string, string>) => el.dispatchEvent(new CustomEvent('summer-upload-result', { detail }))
|
||||
|
||||
it('slugs the file name before the upload (UI-SPEC J4 backstop)', () => {
|
||||
expect(mediaFileName('Lwia Grzywa – w lesie!.JPG')).toBe('lwia-grzywa-w-lesie.jpg')
|
||||
expect(mediaFileName('.png')).toBe('image.png')
|
||||
expect(slugFileStem('')).toBe('image')
|
||||
expect(slugFileStem('ŻÓŁĆ gęślą jaźń')).toBe('zolc-gesla-jazn')
|
||||
expect(slugFileStem('a'.repeat(81))).toHaveLength(80)
|
||||
expect(slugFileStem('a'.repeat(79) + ' b')).toBe('a'.repeat(79))
|
||||
|
||||
const { el, uploads } = mount('', { options: ON })
|
||||
pick(el, [new File([new Uint8Array([255, 216, 255])], 'Lwia Grzywa – w lesie!.JPG', { type: 'image/jpeg' })])
|
||||
const imageForm = form(el, 'image')!
|
||||
const name = imageForm.querySelector<HTMLInputElement>('input[name="fileName"]')!
|
||||
expect(name.value).toBe('lwia-grzywa-w-lesie')
|
||||
expect(imageForm.querySelector('.blogeditor__ext')!.textContent).toBe('.jpg')
|
||||
fill(name, 'Lew w Lesie')
|
||||
name.dispatchEvent(new Event('blur'))
|
||||
expect(name.value).toBe('lew-w-lesie')
|
||||
chooseImage(el, { alt: 'Lew' })
|
||||
expect(uploads[0]!.file.name).toBe('lew-w-lesie.jpg')
|
||||
expect(uploads[0]!.file.type).toBe('image/jpeg')
|
||||
expect(status(el)).toBe('Uploading lew-w-lesie.jpg…')
|
||||
})
|
||||
|
||||
it('inserts alt="" for a decorative image', () => {
|
||||
const { el, uploads } = mount('', { options: ON })
|
||||
pick(el, [new File([new Uint8Array([137, 80, 78, 71])], 'kreska.png', { type: 'image/png' })])
|
||||
const imageForm = form(el, 'image')!
|
||||
const alt = imageForm.querySelector<HTMLInputElement>('input[name="alt"]')!
|
||||
fill(alt, 'temporary')
|
||||
chooseImage(el, { decorative: true })
|
||||
expect(alt.value).toBe('')
|
||||
expect(alt.disabled).toBe(true)
|
||||
answer(el, { id: uploads[0]!.id, url: '/kreska.png' })
|
||||
expect(el.value).toBe('<p><img src="/kreska.png" alt=""></p>')
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { EMBED_PROVIDERS, isEmbedProvider, parseEmbedUrl } from '../../assets/src/blogeditor/embed'
|
||||
import { isPolicyUrl } from '../../assets/src/blogeditor/extensions'
|
||||
import { format, labelsFor } from '../../assets/src/blogeditor/labels'
|
||||
import { button, click, fill, form, mount, status } from './helpers'
|
||||
import { button, chooseImage, click, fill, form, mount, status } from './helpers'
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
@@ -179,33 +179,36 @@ describe('upload flow', () => {
|
||||
}
|
||||
|
||||
it('ignores an answer with an id it did not send', () => {
|
||||
const { el, uploads, changes } = mount('<p>a</p>')
|
||||
const { el, uploads } = mount('<p>a</p>')
|
||||
pick(el, [png()])
|
||||
expect(uploads).toHaveLength(0)
|
||||
chooseImage(el, { alt: 'Grzyb' })
|
||||
answer(el, { id: 'not-ours', url: '/x.png' })
|
||||
answer(el, { id: 'not-ours', error: 'nope' })
|
||||
expect(form(el, 'image')).toBeNull()
|
||||
expect(status(el)).toBe('Uploading grzyb.png…')
|
||||
expect(el.value).toBe('<p>a</p>')
|
||||
answer(el, { id: uploads[0]!.id, url: '/ours.png' })
|
||||
expect(form(el, 'image')).not.toBeNull()
|
||||
expect(changes).toEqual([])
|
||||
expect(el.value).toContain('<img src="/ours.png" alt="Grzyb">')
|
||||
})
|
||||
|
||||
it('times out after 60 seconds and ignores the late answer', () => {
|
||||
vi.useFakeTimers()
|
||||
const { el, uploads } = mount('')
|
||||
pick(el, [png()])
|
||||
chooseImage(el, { alt: 'Grzyb' })
|
||||
vi.advanceTimersByTime(59_999)
|
||||
expect(status(el)).toBe('Uploading grzyb.png…')
|
||||
vi.advanceTimersByTime(1)
|
||||
expect(status(el)).toBe('The upload of grzyb.png took too long.')
|
||||
answer(el, { id: uploads[0]!.id, url: '/late.png' })
|
||||
expect(form(el, 'image')).toBeNull()
|
||||
expect(el.value).toBe('')
|
||||
})
|
||||
|
||||
it('answers each of several uploads by its own id', () => {
|
||||
const { el, uploads } = mount('')
|
||||
pick(el, [png(), new File([new Uint8Array([255, 216, 255])], 'b.jpg', { type: 'image/jpeg' })])
|
||||
chooseImage(el, { alt: 'A' })
|
||||
chooseImage(el, { alt: 'B' })
|
||||
expect(uploads).toHaveLength(2)
|
||||
expect(uploads[0]!.id).not.toBe(uploads[1]!.id)
|
||||
answer(el, { id: uploads[1]!.id, error: 'Too big' })
|
||||
@@ -215,6 +218,7 @@ describe('upload flow', () => {
|
||||
it('refuses an uploaded URL outside the policy', () => {
|
||||
const { el, uploads } = mount('')
|
||||
pick(el, [png()])
|
||||
chooseImage(el, { alt: 'Grzyb' })
|
||||
answer(el, { id: uploads[0]!.id, url: 'javascript:alert(1)' })
|
||||
expect(form(el, 'image')).toBeNull()
|
||||
expect(status(el)).toMatch(/^Upload failed/)
|
||||
|
||||
@@ -76,3 +76,21 @@ export function fill(input: Element | null | undefined, value: string): void {
|
||||
export function status(el: HTMLElement): string {
|
||||
return el.querySelector('[role="status"]')?.textContent ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* chooseImage fills the open J4 image form (alt, caption, decorative, file
|
||||
* name) and presses Insert, which uploads the file.
|
||||
*/
|
||||
export function chooseImage(el: HTMLElement, choice: { alt?: string; caption?: string; decorative?: boolean; fileName?: string } = {}): void {
|
||||
const imageForm = form(el, 'image')
|
||||
if (!imageForm) throw new Error('the image form is not open')
|
||||
if (choice.fileName !== undefined) fill(imageForm.querySelector('input[name="fileName"]'), choice.fileName)
|
||||
if (choice.decorative) {
|
||||
const box = imageForm.querySelector<HTMLInputElement>('input[name="decorative"]')!
|
||||
box.checked = true
|
||||
box.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
if (choice.alt !== undefined) fill(imageForm.querySelector('input[name="alt"]'), choice.alt)
|
||||
if (choice.caption !== undefined) fill(imageForm.querySelector('input[name="caption"]'), choice.caption)
|
||||
click(imageForm.querySelector('[data-action="insert"]'))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BlogEditorElement, TAG } from '../../assets/src/blogeditor/index'
|
||||
import { uploadRequestId } from '../../assets/src/blogeditor/element'
|
||||
import { parseEmbedUrl } from '../../assets/src/blogeditor/embed'
|
||||
import { isPolicyUrl } from '../../assets/src/blogeditor/extensions'
|
||||
import { button, click, fill, form, insertAtBlock, insertAtEveryTextblock, mount, status } from './helpers'
|
||||
import { button, chooseImage, click, fill, form, insertAtBlock, insertAtEveryTextblock, mount, status } from './helpers'
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
@@ -320,11 +320,12 @@ describe('image uploads', () => {
|
||||
const { el, uploads } = mount('')
|
||||
click(button(el, 'image'))
|
||||
pick(el, [png()])
|
||||
chooseImage(el, { alt: 'Grzyb' })
|
||||
expect(uploads).toHaveLength(1)
|
||||
expect(uploads[0]!.id).toMatch(uuid)
|
||||
expect(status(el)).toBe('Uploading grzyb.png…')
|
||||
answer(el, { id: uploads[0]!.id, url: '/storage/app/uploads/public/journal/grzyb.png' })
|
||||
expect(form(el, 'image')).not.toBeNull()
|
||||
expect(el.value).toContain('grzyb.png')
|
||||
|
||||
vi.stubGlobal('crypto', undefined)
|
||||
const plain = [uploadRequestId(), uploadRequestId()]
|
||||
@@ -336,44 +337,45 @@ describe('image uploads', () => {
|
||||
expect(globalThis.crypto).toBe(real)
|
||||
})
|
||||
|
||||
it('sends a picked image as summer-upload and inserts img with alt text', () => {
|
||||
it('opens the image form for a picked image and uploads it on Insert with the alt text (J4)', () => {
|
||||
const { el, uploads } = mount('')
|
||||
click(button(el, 'image'))
|
||||
pick(el, [png()])
|
||||
expect(uploads).toHaveLength(0)
|
||||
const imageForm = form(el, 'image')!
|
||||
expect((imageForm.querySelector('input[name="fileName"]') as HTMLInputElement).value).toBe('grzyb')
|
||||
expect(imageForm.querySelector('.blogeditor__ext')!.textContent).toBe('.png')
|
||||
click(imageForm.querySelector('[data-action="insert"]'))
|
||||
expect(imageForm.querySelector('[data-error]')!.textContent).toBe('Enter the alt text, or mark the image as decorative.')
|
||||
expect(uploads).toHaveLength(0)
|
||||
|
||||
chooseImage(el, { alt: 'Grzyb: czy to "Turkey Tail"?' })
|
||||
expect(form(el, 'image')).toBeNull()
|
||||
expect(uploads).toHaveLength(1)
|
||||
expect(uploads[0]!.file.name).toBe('grzyb.png')
|
||||
expect(uploads[0]!.id).toMatch(/[0-9a-f-]{36}/)
|
||||
expect(status(el)).toBe('Uploading grzyb.png…')
|
||||
|
||||
answer(el, { id: 'someone-else', url: '/x.png' })
|
||||
expect(form(el, 'image')).toBeNull()
|
||||
|
||||
answer(el, { id: uploads[0]!.id, url: '/storage/app/uploads/public/journal/grzyb.png' })
|
||||
const imageForm = form(el, 'image')!
|
||||
click(imageForm.querySelector('[data-action="insert"]'))
|
||||
expect(imageForm.querySelector('[data-error]')!.textContent).toBe('Enter the alt text.')
|
||||
expect(el.value).toBe('')
|
||||
|
||||
fill(imageForm.querySelector('input[name="alt"]'), 'Grzyb: czy to "Turkey Tail"?')
|
||||
click(imageForm.querySelector('[data-action="insert"]'))
|
||||
expect(form(el, 'image')).toBeNull()
|
||||
answer(el, { id: 'someone-else', url: '/x.png' })
|
||||
expect(el.value).toBe('')
|
||||
answer(el, { id: uploads[0]!.id, url: '/storage/app/uploads/public/journal/grzyb.png' })
|
||||
expect(status(el)).toBe('')
|
||||
expect(el.value).toBe('<p><img src="/storage/app/uploads/public/journal/grzyb.png" alt="Grzyb: czy to "Turkey Tail"?"></p>')
|
||||
})
|
||||
|
||||
it('inserts figure with figcaption when a caption is given', () => {
|
||||
const { el, uploads } = mount('')
|
||||
pick(el, [png()])
|
||||
chooseImage(el, { alt: 'Owocniki', caption: 'Owocniki na pniu.' })
|
||||
answer(el, { id: uploads[0]!.id, url: '/a.png' })
|
||||
const imageForm = form(el, 'image')!
|
||||
fill(imageForm.querySelector('input[name="alt"]'), 'Owocniki')
|
||||
fill(imageForm.querySelector('input[name="caption"]'), 'Owocniki na pniu.')
|
||||
click(imageForm.querySelector('[data-action="insert"]'))
|
||||
expect(el.value).toBe('<figure><img src="/a.png" alt="Owocniki"><figcaption>Owocniki na pniu.</figcaption></figure>')
|
||||
})
|
||||
|
||||
it('shows an upload error and inserts nothing', () => {
|
||||
const { el, uploads, changes } = mount('<p>a</p>')
|
||||
pick(el, [png()])
|
||||
chooseImage(el, { alt: 'Grzyb' })
|
||||
answer(el, { id: uploads[0]!.id, error: 'File too large' })
|
||||
expect(form(el, 'image')).toBeNull()
|
||||
expect(status(el)).toBe('Upload failed: File too large')
|
||||
@@ -385,13 +387,14 @@ describe('image uploads', () => {
|
||||
vi.useFakeTimers()
|
||||
const { el, uploads } = mount('')
|
||||
pick(el, [png()])
|
||||
chooseImage(el, { alt: 'Grzyb' })
|
||||
vi.advanceTimersByTime(60_000)
|
||||
expect(status(el)).toBe('The upload of grzyb.png took too long.')
|
||||
answer(el, { id: uploads[0]!.id, url: '/late.png' })
|
||||
expect(form(el, 'image')).toBeNull()
|
||||
expect(el.value).toBe('')
|
||||
})
|
||||
|
||||
it('uploads image files dropped on the editor', () => {
|
||||
it('opens the image form for image files dropped on the editor', () => {
|
||||
const { el, editor, uploads } = mount('<p>a</p>')
|
||||
let prevented = false
|
||||
const event = {
|
||||
@@ -403,6 +406,8 @@ describe('image uploads', () => {
|
||||
const handled = editor.view.someProp('handleDrop', (f) => f(editor.view, event, editor.state.doc.slice(0, 0), false))
|
||||
expect(handled).toBe(true)
|
||||
expect(prevented).toBe(true)
|
||||
expect(form(el, 'image')).not.toBeNull()
|
||||
chooseImage(el, { alt: 'Grzyb' })
|
||||
expect(uploads.map((u) => u.file.name)).toEqual(['grzyb.png'])
|
||||
expect(el.value).toBe('<p>a</p>')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user