fix: element tests drive the widget host protocol; the image element gets tests (WR-25)
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.
This commit is contained in:
@@ -1,7 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
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.
|
||||
// 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', () => {
|
||||
@@ -16,3 +27,74 @@ describe('E7 safeImageURL', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { applyState, ROW_CAP, shownRows, type PreviewData, type PreviewRow } from '../../assets/src/elements/transfer'
|
||||
import { labelsFor } from '../../assets/src/elements/labels'
|
||||
import { HostFailure, mountWidget, settle } from './host'
|
||||
|
||||
// E6 golem15-seo-transfer (UI-SPEC C13): Apply is enabled only after a
|
||||
// preview with no refused row and at least one change.
|
||||
@@ -82,3 +83,50 @@ describe('E6 apply gating', () => {
|
||||
expect(node.querySelector('.seo-transfer__actions')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('E6 host protocol (WR-25)', () => {
|
||||
function downloadButton(node: HTMLElement): HTMLButtonElement {
|
||||
return node.querySelector<HTMLButtonElement>('.seo-transfer__section button')!
|
||||
}
|
||||
|
||||
it('an export goes through the host and the buttons are busy until it settles', async () => {
|
||||
const host = mountWidget('golem15-seo-transfer', {
|
||||
attrs: { 'ui-locale': 'en', 'busy-label': 'Working…' },
|
||||
answer: () => ({ op: 'export', filename: 'seo-redirects.csv', csv: 'source\n' }),
|
||||
})
|
||||
URL.createObjectURL = () => 'blob:x'
|
||||
URL.revokeObjectURL = () => undefined
|
||||
downloadButton(host.node).click()
|
||||
expect(host.posted).toEqual([{ op: 'export', dataset: 'redirects' }])
|
||||
expect(downloadButton(host.node).disabled).toBe(true)
|
||||
expect(downloadButton(host.node).textContent).toBe('Working…')
|
||||
downloadButton(host.node).click()
|
||||
expect(host.emitted).toHaveLength(1)
|
||||
await settle()
|
||||
expect(downloadButton(host.node).disabled).toBe(false)
|
||||
})
|
||||
|
||||
it('state=error unblocks the next request', async () => {
|
||||
let fail = true
|
||||
const host = mountWidget('golem15-seo-transfer', {
|
||||
attrs: { 'ui-locale': 'en' },
|
||||
answer: () => {
|
||||
if (fail) {
|
||||
fail = false
|
||||
throw new HostFailure('export')
|
||||
}
|
||||
return { op: 'export', filename: 'seo-redirects.csv', csv: 'source\n' }
|
||||
},
|
||||
})
|
||||
URL.createObjectURL = () => 'blob:x'
|
||||
URL.revokeObjectURL = () => undefined
|
||||
downloadButton(host.node).click()
|
||||
await settle()
|
||||
expect(host.node.getAttribute('state')).toBe('error')
|
||||
expect(downloadButton(host.node).disabled).toBe(false)
|
||||
downloadButton(host.node).click()
|
||||
await settle()
|
||||
expect(host.posted).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user