// Embeds, the URL rule, uploads and labels of the Blog Editor (Phase 16.2
// D-04, D-05, D-13, D-18).
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'
afterEach(() => {
vi.useRealTimers()
document.body.replaceChildren()
})
const ID = 'dQw4w9WgXcQ'
const PLACEHOLDER = `
Film:
${PLACEHOLDER}Koniec
` const { el, editor } = mount(html) const node = editor.state.doc.child(1) expect(node.type.name).toBe('embed') expect(node.attrs).toMatchObject({ provider: 'youtube', id: ID }) expect(el.value).toBe(html) editor.view.dispatch(editor.state.tr.insertText('X', 1)) expect(el.value).toBe(`XFilm:
${PLACEHOLDER}Koniec
`) }) it('shows a card and never an iframe', () => { const { el } = mount(PLACEHOLDER) expect(el.querySelector('iframe')).toBeNull() expect(el.querySelector('.blogeditor__embed')!.textContent).toBe(`${labelsFor("en").embedCard}: YouTube ${ID}`) }) it.each([ ['another provider', `a
Xa
a
') click(button(el, 'embed')) const embedForm = form(el, 'embed')! fill(embedForm.querySelector('input'), 'javascript:alert(1)') click(embedForm.querySelector('[data-action="insert"]')) expect(form(el, 'embed')).not.toBeNull() expect(el.value).toBe('a
') expect(changes).toEqual([]) click(embedForm.querySelector('[data-action="cancel"]')) expect(form(el, 'embed')).toBeNull() click(button(el, 'embed')) fill(form(el, 'embed')!.querySelector('input'), `https://www.youtube.com/shorts/${ID}`) click(form(el, 'embed')!.querySelector('[data-action="insert"]')) expect(el.value).toContain(PLACEHOLDER) expect(changes).toHaveLength(1) }) }) describe('isPolicyUrl', () => { it.each(['http://a.test', 'https://a.test/x?y#z', 'HTTPS://A.TEST', 'mailto:a@b.test', '/rel', 'rel/path', '../up', '#fragment', '?q=1', '', 'https://a.test/javascript:x'])( 'accepts %j', (url) => { expect(isPolicyUrl(url)).toBe(true) }, ) it.each([ 'javascript:alert(1)', 'JavaScript:alert(1)', ' javascript:alert(1)', 'java\tscript:alert(1)', 'java\nscript:alert(1)', 'java\rscript:alert(1)', 'javascript\u0000:alert(1)', 'vbscript:msgbox(1)', 'data:text/html,x', 'tel:123', 'ftp://a.test', 'file:///etc/passwd', ])('refuses %j', (url) => { expect(isPolicyUrl(url)).toBe(false) }) it('refuses null and undefined', () => { expect(isPolicyUrl(null)).toBe(false) expect(isPolicyUrl(undefined)).toBe(false) }) }) describe('upload flow', () => { const png = () => new File([new Uint8Array([137, 80, 78, 71])], 'grzyb.png', { type: 'image/png' }) function pick(el: HTMLElement, files: File[]): void { const input = el.querySelectora
') pick(el, [png()]) 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…') answer(el, { id: uploads[0]!.id, url: '/ours.png' }) expect(form(el, 'image')).not.toBeNull() expect(changes).toEqual([]) }) it('times out after 60 seconds and ignores the late answer', () => { vi.useFakeTimers() const { el, uploads } = mount('') pick(el, [png()]) 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' })]) expect(uploads).toHaveLength(2) expect(uploads[0]!.id).not.toBe(uploads[1]!.id) answer(el, { id: uploads[1]!.id, error: 'Too big' }) expect(status(el)).toBe('Upload failed: Too big') }) it('refuses an uploaded URL outside the policy', () => { const { el, uploads } = mount('') pick(el, [png()]) answer(el, { id: uploads[0]!.id, url: 'javascript:alert(1)' }) expect(form(el, 'image')).toBeNull() expect(status(el)).toMatch(/^Upload failed/) expect(el.value).toBe('') }) }) describe('labels', () => { it('has Polish labels, an English fallback and every key in both', () => { const en = labelsFor('en') const pl = labelsFor('pl') expect(Object.keys(pl).sort()).toEqual(Object.keys(en).sort()) for (const key of Object.keys(en) as (keyof typeof en)[]) { expect(en[key], key).not.toBe('') expect(pl[key], key).not.toBe('') } expect(pl.tabSource).not.toBe(en.tabSource) expect(labelsFor('pl-PL')).toBe(pl) expect(labelsFor('PL')).toBe(pl) expect(labelsFor('de')).toBe(en) expect(labelsFor('')).toBe(en) expect(labelsFor(null)).toBe(en) expect(labelsFor(undefined)).toBe(en) }) it('follows the ui-locale attribute of the element', () => { const { el } = mount('a
', { 'ui-locale': 'pl' }) expect(el.querySelector('button[data-tab="source"]')!.textContent).toBe(labelsFor('pl').tabSource) const { el: other } = mount('a
', { 'ui-locale': 'fr' }) expect(other.querySelector('button[data-tab="source"]')!.textContent).toBe(labelsFor('en').tabSource) }) it('format fills known placeholders and leaves unknown ones', () => { expect(format('Uploading {name}…', { name: 'a.png' })).toBe('Uploading a.png…') expect(format('{a} and {b}', { a: '1' })).toBe('1 and {b}') }) }) describe('disabled and invalid', () => { it('disables the formatting buttons and the source while disabled, and marks the editor invalid', () => { const { el } = mount('a
', { disabled: '', invalid: '' }) expect(el.editor!.isEditable).toBe(false) expect(button(el, 'bold').disabled).toBe(true) expect(el.querySelector('textarea')!.readOnly).toBe(true) expect(el.editor!.view.dom.getAttribute('aria-invalid')).toBe('true') // The preview toggle stays usable: it never edits. expect(button(el, 'preview').disabled).toBe(false) el.removeAttribute('disabled') expect(button(el, 'bold').disabled).toBe(false) }) it('emits nothing for clicks on a disabled editor', () => { const { el, changes } = mount('a
', { disabled: '' }) click(button(el, 'bold')) click(button(el, 'embed')) expect(changes).toEqual([]) expect(el.value).toBe('a
') }) })