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.
133 lines
5.5 KiB
TypeScript
133 lines
5.5 KiB
TypeScript
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.
|
|
|
|
beforeAll(async () => {
|
|
await import('../../assets/src/elements/index')
|
|
})
|
|
|
|
afterEach(() => {
|
|
document.body.replaceChildren()
|
|
})
|
|
|
|
function row(line: number, change: PreviewRow['change']): PreviewRow {
|
|
return { line, change, address: '/a' + line, field: 'target', before: '', after: '/b', note: change === 'refused' ? 'Loop' : '' }
|
|
}
|
|
|
|
function previewData(rows: PreviewRow[]): PreviewData {
|
|
const summary = { new: 0, changed: 0, unchanged: 0, refused: 0, warning: 0 }
|
|
for (const r of rows) {
|
|
summary[r.change]++
|
|
}
|
|
return { op: 'preview', token: 'tok-1', dataset: 'redirects', summary, rows, total: rows.length, count: summary.new + summary.changed }
|
|
}
|
|
|
|
/** A transfer element answered with a preview; returns it and its actions. */
|
|
function screen(data: PreviewData): { node: HTMLElement; actions: unknown[] } {
|
|
const node = document.createElement('golem15-seo-transfer')
|
|
node.setAttribute('ui-locale', 'en')
|
|
const actions: unknown[] = []
|
|
node.addEventListener('summer-action', (ev) => actions.push((ev as CustomEvent).detail.payload))
|
|
document.body.append(node)
|
|
node.dispatchEvent(new CustomEvent('summer-result', { detail: { data } }))
|
|
return { node, actions }
|
|
}
|
|
|
|
function applyButton(node: HTMLElement): HTMLButtonElement {
|
|
const buttons = node.querySelectorAll<HTMLButtonElement>('.seo-transfer__actions button')
|
|
return buttons[buttons.length - 1]
|
|
}
|
|
|
|
describe('E6 apply gating', () => {
|
|
it('one refused row disables Apply and shows refusedBlocks', () => {
|
|
const { node } = screen(previewData([row(2, 'new'), row(3, 'changed'), row(4, 'refused')]))
|
|
expect(applyButton(node).disabled).toBe(true)
|
|
expect(node.textContent).toContain(labelsFor('en').refusedBlocks)
|
|
const first = node.querySelector('tbody tr')
|
|
expect(first?.className).toBe('seo-transfer__tr--refused')
|
|
})
|
|
|
|
it('zero changes disables Apply and shows the nothing note, not refusedBlocks', () => {
|
|
const { node } = screen(previewData([row(2, 'unchanged')]))
|
|
expect(applyButton(node).disabled).toBe(true)
|
|
expect(node.textContent).toContain(labelsFor('en').nothing)
|
|
expect(node.textContent).not.toContain(labelsFor('en').refusedBlocks)
|
|
})
|
|
|
|
it('a clean preview enables Apply, which sends the preview token', () => {
|
|
const { node, actions } = screen(previewData([row(2, 'new'), row(3, 'unchanged')]))
|
|
const apply = applyButton(node)
|
|
expect(apply.disabled).toBe(false)
|
|
expect(node.textContent).not.toContain(labelsFor('en').refusedBlocks)
|
|
apply.click()
|
|
expect(actions).toContainEqual({ op: 'apply', token: 'tok-1' })
|
|
})
|
|
|
|
it('applyState and shownRows', () => {
|
|
expect(applyState(previewData([row(1, 'refused'), row(2, 'new')]))).toBe('refused')
|
|
expect(applyState(previewData([row(1, 'unchanged')]))).toBe('nothing')
|
|
expect(applyState(previewData([row(1, 'changed')]))).toBe('ready')
|
|
const many = [...Array.from({ length: ROW_CAP + 5 }, (_, i) => row(i + 2, 'new')), row(999, 'refused')]
|
|
const shown = shownRows(many)
|
|
expect(shown.length).toBe(ROW_CAP)
|
|
expect(shown[0]).toEqual(row(999, 'refused'))
|
|
})
|
|
|
|
it('a refused preview answer shows its message and no Apply', () => {
|
|
const { node } = screen({ op: 'preview', error: 'stale', message: 'The data changed since the preview.' } as unknown as PreviewData)
|
|
expect(node.textContent).toContain('The data changed since the preview.')
|
|
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)
|
|
})
|
|
})
|
|
|