Files
sm-journal-plugin/tests/blogeditor/forms.test.ts
Jakub Zych 311c28af3a test: editor outline facts, flags and debounce; link and image form rules
- 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
2026-11-04 19:55:57 +01:00

117 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, describe, expect, it } from 'vitest'
import { mediaFileName, slugFileStem } from '../../assets/src/blogeditor/imageform'
import { isExternalUrl } from '../../assets/src/blogeditor/linkform'
import { button, chooseImage, click, fill, form, mount } from './helpers'
// J3 link form and J4 image form (UI-SPEC J3, J4): the rel checkboxes
// follow the URL as it is typed, the generic-text warning comes and goes
// without blocking, the file-name slug rule's examples, and a decorative
// image clears and locks its alt. The first cases are in additions.test.ts.
afterEach(() => {
document.body.replaceChildren()
})
const ON = JSON.stringify({ headingLevels: [2, 3, 4], genericAnchors: ['click here', 'czytaj więcej'] })
function openLink(text: string) {
const m = mount(`<p>${text}</p>`, { options: ON })
m.editor.commands.setTextSelection({ from: 1, to: 1 + text.length })
click(button(m.el, 'link'))
const linkForm = form(m.el, 'link')!
const box = (token: string) => linkForm.querySelector<HTMLInputElement>(`input[name="rel-${token}"]`)!
return { ...m, linkForm, box, url: linkForm.querySelector<HTMLInputElement>('input[name="url"]')! }
}
describe('J3 rel checkboxes', () => {
it('enable for an absolute http(s) URL and disable again for an internal one', () => {
const { box, url } = openLink('Partner')
for (const token of ['nofollow', 'sponsored', 'ugc']) expect(box(token).disabled, token).toBe(true)
fill(url, 'https://partner.test')
for (const token of ['nofollow', 'sponsored', 'ugc']) expect(box(token).disabled, token).toBe(false)
box('ugc').checked = true
fill(url, '/blog/partner')
expect(box('ugc').disabled).toBe(true)
})
it('writes only the checked tokens, in order, for an external link', () => {
const { el, box, url, linkForm } = openLink('Partner')
fill(url, 'https://partner.test/x')
box('ugc').checked = true
box('nofollow').checked = true
click(linkForm.querySelector('[data-action="apply"]'))
expect(el.value).toMatch(/^<p><a rel="nofollow ugc" href="https:\/\/partner\.test\/x">Partner<\/a><\/p>$/)
})
it('writes no rel for an internal link even when a box was checked', () => {
const { el, box, url, linkForm } = openLink('Partner')
fill(url, 'https://partner.test')
box('sponsored').checked = true
fill(url, '/oferta')
click(linkForm.querySelector('[data-action="apply"]'))
expect(el.value).toBe('<p><a href="/oferta">Partner</a></p>')
})
})
describe('J3 generic link text', () => {
it('warns for a listed text in any case or punctuation and stays quiet otherwise', () => {
const generic = openLink('Czytaj więcej…')
expect(generic.linkForm.querySelector<HTMLElement>('[data-warning]')!.hidden).toBe(false)
fill(generic.url, '/blog/a')
click(generic.linkForm.querySelector('[data-action="apply"]'))
expect(generic.el.value).toBe('<p><a href="/blog/a">Czytaj więcej…</a></p>')
document.body.replaceChildren()
const specific = openLink('Badania nad chagą')
expect(specific.linkForm.querySelector<HTMLElement>('[data-warning]')!.hidden).toBe(true)
})
it('reads external URLs by scheme only', () => {
for (const [u, want] of [
['https://a.test/x?y=1', true],
['http://a.test', true],
['/kontakt', false],
['./a', false],
['ftp://a.test', false],
['javascript:alert(1)', false],
['', false],
] as const) {
expect(isExternalUrl(u), u).toBe(want)
}
})
})
describe('J4 file names', () => {
it('follows the slug rule examples', () => {
for (const [name, want] of [
['Chaga – zdjęcie 1.JPEG', 'chaga-zdjecie-1.jpeg'],
[' Kurki w koszyku .png', 'kurki-w-koszyku.png'],
['IMG_2024.06.01.webp', 'img-2024-06-01.webp'],
['___.gif', 'image.gif'],
['bez-rozszerzenia', 'bez-rozszerzenia'],
] as const) {
expect(mediaFileName(name), name).toBe(want)
}
expect(slugFileStem('Łódź & Kraków')).toBe('lodz-krakow')
expect(slugFileStem('--a--b--')).toBe('a-b')
})
it('clears and locks the alt of a decorative image and frees it again', () => {
const { el } = mount('', { options: ON })
const input = el.querySelector<HTMLInputElement>('input[type="file"]')!
Object.defineProperty(input, 'files', { value: [new File([new Uint8Array([137, 80, 78, 71])], 'linia.png', { type: 'image/png' })], configurable: true })
input.dispatchEvent(new Event('change'))
const imageForm = form(el, 'image')!
const alt = imageForm.querySelector<HTMLInputElement>('input[name="alt"]')!
const decorative = imageForm.querySelector<HTMLInputElement>('input[name="decorative"]')!
fill(alt, 'Linia')
decorative.checked = true
decorative.dispatchEvent(new Event('change'))
expect(alt.value).toBe('')
expect(alt.disabled).toBe(true)
decorative.checked = false
decorative.dispatchEvent(new Event('change'))
expect(alt.disabled).toBe(false)
chooseImage(el, { alt: 'Linia' })
})
})