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('.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('.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) }) })