// Unsaved edits of the editors (src/lib/edits.ts): one pending value per field, typing back to the saved value drops // it, undo / redo (typing into one field within COALESCE_MS is one step), discard can be undone, areas and apps stay // apart, and the ops sent to project.edit. import { describe, expect, test } from 'bun:test'; import { COALESCE_MS, EditStore, keyOf } from '../src/lib/edits'; const sub = (value: string) => ({ kind: 'listing' as const, store: 'apple' as const, lang: 'en-US', field: 'subtitle', value }); describe('EditStore', () => { test('pending values, back to the saved value drops the edit', () => { let now = 0; const s = new EditStore(() => now); s.set('p1', sub('Tidy'), 'listings', 'Old'); expect(s.count('p1')).toBe(1); expect(s.get('p1', keyOf(sub('')))!.op).toEqual(sub('Tidy')); now += 10; s.set('p1', sub('Old'), 'listings', 'Old'); expect(s.count('p1')).toBe(0); expect(s.count('p2')).toBe(0); }); test('undo / redo, one step per field burst', () => { let now = 0; const s = new EditStore(() => now); s.set('p', sub('T'), 'listings', 'Old'); now += 100; s.set('p', sub('Ti'), 'listings', 'Old'); now += 100; s.set('p', sub('Tid'), 'listings', 'Old'); now += COALESCE_MS + 1; s.set('p', { kind: 'config', path: 'app.supportUrl', value: 'https://example.com/h' }, 'listings', 'https://example.com/s'); expect(s.count('p')).toBe(2); s.undo('p'); expect(s.count('p')).toBe(1); expect((s.get('p', keyOf(sub('')))!.op as { value: string }).value).toBe('Tid'); s.undo('p'); expect(s.count('p')).toBe(0); expect(s.canUndo('p')).toBe(false); s.redo('p'); s.redo('p'); expect(s.count('p')).toBe(2); }); test('areas, discard (undoable), clear after a save, ops in a stable order', () => { const s = new EditStore(() => 0); s.set('p', sub('Tidy'), 'listings', 'Old'); s.set('p', { kind: 'config', path: 'apple.info.free', value: false }, 'appinfo', true); expect(s.count('p', 'listings')).toBe(1); expect(s.ops('p', 'appinfo')).toEqual([{ kind: 'config', path: 'apple.info.free', value: false }]); expect(s.ops('p').map(keyOf)).toEqual(['config:apple.info.free', 'listing:apple:en-US:subtitle']); s.clear('p', 'listings', { undoable: true }); expect(s.count('p')).toBe(1); s.undo('p'); expect(s.count('p')).toBe(2); expect(s.dirtyProjects()).toEqual(['p']); s.clear('p'); expect(s.dirtyProjects()).toEqual([]); expect(s.canUndo('p')).toBe(false); }); test('subscribers hear every change', () => { const s = new EditStore(() => 0); let n = 0; const off = s.subscribe(() => { n++; }); s.set('p', sub('x'), 'listings', ''); s.undo('p'); off(); s.set('p', sub('y'), 'listings', ''); expect(n).toBe(2); expect(s.snapshot()).toBe(3); }); });