import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { JSDOM } from 'jsdom'; import { applyManualEditPatch, readManualEditAttributes, readManualEditFields, readManualEditOuterHtml, readManualEditStyles, } from '../../src/edit-mode/source-patches'; const baseSource = `

Original title

Start Buy now Old image
Card

Nested copy

Generated path text

`; describe('manual edit source patches', () => { beforeEach(() => { const dom = new JSDOM(''); globalThis.DOMParser = dom.window.DOMParser; globalThis.CSS = { escape: (value: string) => value.replace(/"/g, '\\"') } as typeof CSS; }); afterEach(() => { Reflect.deleteProperty(globalThis, 'DOMParser'); Reflect.deleteProperty(globalThis, 'CSS'); }); it('updates only the selected text target', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-text', id: 'hero-title', value: 'Edited title' }); expect(result.ok).toBe(true); expect(readManualEditFields(result.source, 'hero-title').text).toBe('Edited title'); expect(readManualEditFields(result.source, 'cta').text).toBe('Start'); }); it('updates link label and href', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-link', id: 'cta', text: 'Buy now', href: '/buy' }); expect(result.ok).toBe(true); expect(readManualEditFields(result.source, 'cta')).toEqual({ text: 'Buy now', href: '/buy' }); }); it('treats buttons as label-only text targets instead of persisting href attributes', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-text', id: 'button-cta', value: 'Buy button' }); expect(result.ok).toBe(true); const html = readManualEditOuterHtml(result.source, 'button-cta'); expect(html).toContain('Buy button'); expect(html).not.toContain('href='); expect(readManualEditFields(result.source, 'button-cta')).toEqual({ text: 'Buy button' }); }); it('preserves nested link markup when only href changes', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-link', id: 'nested-cta', text: 'Buy now', href: '/buy' }); expect(result.ok).toBe(true); const html = readManualEditOuterHtml(result.source, 'nested-cta'); expect(html).toContain('href="/buy"'); expect(html).toContain('Buy now'); expect(html).toContain(' { const result = applyManualEditPatch(baseSource, { kind: 'set-link', id: 'nested-cta', text: 'Purchase', href: '/buy' }); expect(result.ok).toBe(false); expect(result.error).toContain('nested markup'); }); it('updates image src and alt', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-image', id: 'hero-image', src: '/new.png', alt: 'New image' }); expect(result.ok).toBe(true); expect(readManualEditFields(result.source, 'hero-image')).toEqual({ src: '/new.png', alt: 'New image' }); }); it('adds and removes inline style properties', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-style', id: 'card', styles: { color: '', backgroundColor: 'blue', fontSize: '24px' }, }); expect(result.ok).toBe(true); const styles = readManualEditStyles(result.source, 'card'); expect(styles.color).toBe(''); expect(styles.backgroundColor).toBe('blue'); expect(styles.fontSize).toBe('24px'); expect(styles.padding).toBe('8px'); }); it('applies attributes additively and preserves class/style unless explicitly updated', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-attributes', id: 'card', attributes: { 'aria-label': 'Hero card', 'data-empty': '', 'data-od-id': 'blocked' }, }); expect(result.ok).toBe(true); const attrs = readManualEditAttributes(result.source, 'card'); expect(attrs['aria-label']).toBe('Hero card'); expect(attrs.class).toBe('hero'); expect(attrs.style).toContain('color: red'); expect(attrs['data-od-id']).toBe('card'); expect(attrs['data-empty']).toBeUndefined(); }); it('preserves data-od-id when selected outerHTML omits it', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-outer-html', id: 'card', html: '
Replaced
', }); expect(result.ok).toBe(true); const html = readManualEditOuterHtml(result.source, 'card'); expect(html).toContain('data-od-id="card"'); expect(html).toContain('class="replacement"'); }); it('replaces full source for snapshot-based undo history', () => { const source = '

Snapshot

'; const result = applyManualEditPatch(baseSource, { kind: 'set-full-source', source }); expect(result).toEqual({ ok: true, source }); }); it('updates CSS tokens in style tags', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-token', token: '--brand', value: '#f00' }); expect(result.ok).toBe(true); expect(result.source).toContain('--brand: #f00;'); }); it('preserves fragment-shaped HTML when saving patches', () => { const source = '

Original title

'; const result = applyManualEditPatch(source, { kind: 'set-text', id: 'hero-title', value: 'Edited title' }); expect(result.ok).toBe(true); expect(result.source).toBe('

Edited title

'); expect(result.source).not.toContain(' { const source = [ '', '', '

Original title

', ].join('\n'); const result = applyManualEditPatch(source, { kind: 'set-text', id: 'hero-title', value: 'Edited title' }); expect(result.ok).toBe(true); expect(result.source).toContain(''); expect(result.source).toContain(''); expect(result.source).toContain(''); expect(result.source).toContain('

Edited title

'); }); it('addresses unannotated elements with generated DOM path ids', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-text', id: 'path-0-7', value: 'Path target' }); expect(result.ok).toBe(true); expect(result.source).toContain('Path target'); }); it('rejects text patches for nested markup', () => { const result = applyManualEditPatch(baseSource, { kind: 'set-text', id: 'nested', value: 'Flat text' }); expect(result.ok).toBe(false); expect(result.error).toContain('nested markup'); }); });