- 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
117 lines
4.9 KiB
TypeScript
117 lines
4.9 KiB
TypeScript
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' })
|
||
})
|
||
})
|