Files
sm-seo-plugin/tests/elements/transfer.test.ts
Jakub Zych 5c2ad6aee1 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.
2026-10-27 15:44:22 +01:00

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)
})
})