The transfer element's busy and state=error handling is now tested through the fake widget host, and golem15-seo-image gets tests for the upload id matching, the error display, Remove and the disabled state. The README's test gate runs npm test.
101 lines
4.2 KiB
TypeScript
101 lines
4.2 KiB
TypeScript
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
|
import { safeImageURL } from '../../assets/src/elements/image'
|
|
import { labelsFor } from '../../assets/src/elements/labels'
|
|
|
|
// E7 golem15-seo-image: which stored URLs the preview may load, the
|
|
// upload round trip through summer-upload / summer-upload-result, errors
|
|
// and Remove.
|
|
|
|
beforeAll(async () => {
|
|
await import('../../assets/src/elements/index')
|
|
})
|
|
|
|
afterEach(() => {
|
|
document.body.replaceChildren()
|
|
})
|
|
|
|
describe('E7 safeImageURL', () => {
|
|
it('accepts a path and an http(s) URL', () => {
|
|
for (const url of ['/media/a.png', 'https://cdn.example/a.png', 'HTTP://cdn.example/a.png']) {
|
|
expect(safeImageURL(url), url).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('refuses addresses a browser reads as another host or another scheme (WR-20)', () => {
|
|
for (const url of ['//evil.example/x.png', '/\\evil.example/x.png', 'javascript:alert(1)', 'data:image/png;base64,AA', 'a.png', '']) {
|
|
expect(safeImageURL(url), url).toBe(false)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('E7 element (WR-25)', () => {
|
|
type ImageNode = HTMLElement & { value: string }
|
|
|
|
function mount(value = ''): { node: ImageNode; changes: string[]; uploads: { id: string; file: File }[] } {
|
|
const node = document.createElement('golem15-seo-image') as ImageNode
|
|
node.setAttribute('ui-locale', 'en')
|
|
document.body.append(node)
|
|
node.value = value
|
|
const changes: string[] = []
|
|
const uploads: { id: string; file: File }[] = []
|
|
node.addEventListener('summer-change', (ev) => changes.push((ev as CustomEvent).detail.value))
|
|
node.addEventListener('summer-upload', (ev) => uploads.push((ev as CustomEvent).detail))
|
|
return { node, changes, uploads }
|
|
}
|
|
|
|
function choose(node: HTMLElement): File {
|
|
const input = node.querySelector<HTMLInputElement>('input[type="file"]')!
|
|
const file = new File(['x'], 'a.png', { type: 'image/png' })
|
|
Object.defineProperty(input, 'files', { value: [file] })
|
|
input.dispatchEvent(new Event('change'))
|
|
return file
|
|
}
|
|
|
|
function status(node: HTMLElement): string {
|
|
return node.querySelector('.seo-image__status')?.textContent ?? ''
|
|
}
|
|
|
|
it('shows a safe stored image and hides an unsafe one', () => {
|
|
expect(mount('/media/a.png').node.querySelector('img')?.getAttribute('src')).toBe('/media/a.png')
|
|
expect(mount('/\\evil.example/x.png').node.querySelector('img')).toBeNull()
|
|
})
|
|
|
|
it('uploads through summer-upload and takes only the answer to its own id', () => {
|
|
const { node, changes, uploads } = mount()
|
|
const file = choose(node)
|
|
expect(uploads).toHaveLength(1)
|
|
expect(uploads[0].file).toBe(file)
|
|
expect(status(node)).toBe(labelsFor('en').imageUploading)
|
|
node.dispatchEvent(new CustomEvent('summer-upload-result', { detail: { id: 'other', url: '/media/wrong.png' } }))
|
|
expect(changes).toEqual([])
|
|
expect(status(node)).toBe(labelsFor('en').imageUploading)
|
|
node.dispatchEvent(new CustomEvent('summer-upload-result', { detail: { id: uploads[0].id, url: '/media/new.png' } }))
|
|
expect(changes).toEqual(['/media/new.png'])
|
|
expect(node.value).toBe('/media/new.png')
|
|
expect(node.querySelector('img')?.getAttribute('src')).toBe('/media/new.png')
|
|
expect(status(node)).toBe('')
|
|
})
|
|
|
|
it('shows the upload error, or imageFailed when the answer names none', () => {
|
|
const { node, changes, uploads } = mount()
|
|
choose(node)
|
|
node.dispatchEvent(new CustomEvent('summer-upload-result', { detail: { id: uploads[0].id, error: 'Too big.' } }))
|
|
expect(status(node)).toBe('Too big.')
|
|
choose(node)
|
|
node.dispatchEvent(new CustomEvent('summer-upload-result', { detail: { id: uploads[1].id } }))
|
|
expect(status(node)).toBe(labelsFor('en').imageFailed)
|
|
expect(node.querySelector('.seo-image__status--error')).not.toBeNull()
|
|
expect(changes).toEqual([])
|
|
})
|
|
|
|
it('Remove clears the value; a disabled element offers no actions', () => {
|
|
const { node, changes } = mount('/media/a.png')
|
|
node.querySelector<HTMLButtonElement>('.seo-image__button--secondary')!.click()
|
|
expect(changes).toEqual([''])
|
|
expect(node.querySelector('img')).toBeNull()
|
|
const off = mount('/media/a.png').node
|
|
off.setAttribute('disabled', '')
|
|
expect(off.querySelector('.seo-image__actions')).toBeNull()
|
|
})
|
|
})
|