Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'test: batch 16 — suggesting, search-state, tab-handler (74 tests)' (#258) from test/batch16-suggesting-search-tab into main

scott 3b412061 bd080685

+631
+274
tests/search-state.test.ts
··· 1 + /** 2 + * Tests for SearchState — find-and-replace pure logic (src/docs/search-state.ts). 3 + * Covers findMatches, navigation (next/prev), replaceOne, replaceAll, and case sensitivity. 4 + */ 5 + import { describe, it, expect } from 'vitest'; 6 + import { SearchState } from '../src/docs/search-state.js'; 7 + 8 + // ===================================================================== 9 + // Constructor 10 + // ===================================================================== 11 + 12 + describe('SearchState — constructor', () => { 13 + it('defaults to case-insensitive', () => { 14 + const s = new SearchState(); 15 + expect(s.caseSensitive).toBe(false); 16 + }); 17 + 18 + it('accepts caseSensitive option', () => { 19 + const s = new SearchState({ caseSensitive: true }); 20 + expect(s.caseSensitive).toBe(true); 21 + }); 22 + 23 + it('starts with no matches', () => { 24 + const s = new SearchState(); 25 + expect(s.matches).toEqual([]); 26 + expect(s.currentIndex).toBe(-1); 27 + expect(s.matchCount).toBe(0); 28 + expect(s.currentMatch).toBeNull(); 29 + }); 30 + }); 31 + 32 + // ===================================================================== 33 + // findMatches 34 + // ===================================================================== 35 + 36 + describe('SearchState — findMatches', () => { 37 + it('finds single occurrence', () => { 38 + const s = new SearchState(); 39 + const matches = s.findMatches('hello world', 'world'); 40 + expect(matches).toEqual([{ from: 6, to: 11 }]); 41 + expect(s.currentIndex).toBe(0); 42 + }); 43 + 44 + it('finds multiple occurrences', () => { 45 + const s = new SearchState(); 46 + const matches = s.findMatches('abcabc', 'abc'); 47 + expect(matches.length).toBe(2); 48 + expect(matches[0]).toEqual({ from: 0, to: 3 }); 49 + expect(matches[1]).toEqual({ from: 3, to: 6 }); 50 + }); 51 + 52 + it('finds overlapping occurrences', () => { 53 + const s = new SearchState(); 54 + const matches = s.findMatches('aaa', 'aa'); 55 + expect(matches.length).toBe(2); 56 + expect(matches[0]).toEqual({ from: 0, to: 2 }); 57 + expect(matches[1]).toEqual({ from: 1, to: 3 }); 58 + }); 59 + 60 + it('returns empty for no match', () => { 61 + const s = new SearchState(); 62 + const matches = s.findMatches('hello', 'xyz'); 63 + expect(matches).toEqual([]); 64 + expect(s.currentIndex).toBe(-1); 65 + }); 66 + 67 + it('returns empty for empty term', () => { 68 + const s = new SearchState(); 69 + expect(s.findMatches('hello', '')).toEqual([]); 70 + }); 71 + 72 + it('case-insensitive by default', () => { 73 + const s = new SearchState(); 74 + const matches = s.findMatches('Hello HELLO hello', 'hello'); 75 + expect(matches.length).toBe(3); 76 + }); 77 + 78 + it('case-sensitive when enabled', () => { 79 + const s = new SearchState({ caseSensitive: true }); 80 + const matches = s.findMatches('Hello HELLO hello', 'hello'); 81 + expect(matches.length).toBe(1); 82 + expect(matches[0]).toEqual({ from: 12, to: 17 }); 83 + }); 84 + 85 + it('resets previous matches on new search', () => { 86 + const s = new SearchState(); 87 + s.findMatches('aaa', 'a'); 88 + expect(s.matchCount).toBe(3); 89 + s.findMatches('bb', 'b'); 90 + expect(s.matchCount).toBe(2); 91 + }); 92 + }); 93 + 94 + // ===================================================================== 95 + // Navigation — next / prev 96 + // ===================================================================== 97 + 98 + describe('SearchState — next / prev', () => { 99 + it('next advances currentIndex', () => { 100 + const s = new SearchState(); 101 + s.findMatches('abab', 'ab'); 102 + expect(s.currentIndex).toBe(0); 103 + s.next(); 104 + expect(s.currentIndex).toBe(1); 105 + }); 106 + 107 + it('next wraps around', () => { 108 + const s = new SearchState(); 109 + s.findMatches('abab', 'ab'); 110 + s.next(); // 1 111 + s.next(); // wraps to 0 112 + expect(s.currentIndex).toBe(0); 113 + }); 114 + 115 + it('prev goes backwards', () => { 116 + const s = new SearchState(); 117 + s.findMatches('ababab', 'ab'); 118 + expect(s.currentIndex).toBe(0); 119 + s.prev(); // wraps to last 120 + expect(s.currentIndex).toBe(2); 121 + }); 122 + 123 + it('prev wraps around from 0', () => { 124 + const s = new SearchState(); 125 + s.findMatches('abab', 'ab'); 126 + s.prev(); // wraps to 1 127 + expect(s.currentIndex).toBe(1); 128 + }); 129 + 130 + it('next does nothing with no matches', () => { 131 + const s = new SearchState(); 132 + s.findMatches('hello', 'xyz'); 133 + s.next(); 134 + expect(s.currentIndex).toBe(-1); 135 + }); 136 + 137 + it('prev does nothing with no matches', () => { 138 + const s = new SearchState(); 139 + s.findMatches('hello', 'xyz'); 140 + s.prev(); 141 + expect(s.currentIndex).toBe(-1); 142 + }); 143 + }); 144 + 145 + // ===================================================================== 146 + // currentMatch 147 + // ===================================================================== 148 + 149 + describe('SearchState — currentMatch', () => { 150 + it('returns the current match object', () => { 151 + const s = new SearchState(); 152 + s.findMatches('hello world', 'world'); 153 + expect(s.currentMatch).toEqual({ from: 6, to: 11 }); 154 + }); 155 + 156 + it('returns null when no matches', () => { 157 + const s = new SearchState(); 158 + expect(s.currentMatch).toBeNull(); 159 + }); 160 + 161 + it('updates after next()', () => { 162 + const s = new SearchState(); 163 + s.findMatches('abab', 'ab'); 164 + s.next(); 165 + expect(s.currentMatch).toEqual({ from: 2, to: 4 }); 166 + }); 167 + }); 168 + 169 + // ===================================================================== 170 + // replaceOne 171 + // ===================================================================== 172 + 173 + describe('SearchState — replaceOne', () => { 174 + it('replaces the current match', () => { 175 + const s = new SearchState(); 176 + s.findMatches('hello world', 'world'); 177 + const result = s.replaceOne('hello world', 'earth'); 178 + expect(result.text).toBe('hello earth'); 179 + expect(result.replaced).toBe(true); 180 + }); 181 + 182 + it('returns unchanged text when no matches', () => { 183 + const s = new SearchState(); 184 + s.findMatches('hello', 'xyz'); 185 + const result = s.replaceOne('hello', 'abc'); 186 + expect(result.text).toBe('hello'); 187 + expect(result.replaced).toBe(false); 188 + }); 189 + 190 + it('replaces with empty string', () => { 191 + const s = new SearchState(); 192 + s.findMatches('hello world', 'world'); 193 + const result = s.replaceOne('hello world', ''); 194 + expect(result.text).toBe('hello '); 195 + }); 196 + 197 + it('replaces with longer string', () => { 198 + const s = new SearchState(); 199 + s.findMatches('ab', 'ab'); 200 + const result = s.replaceOne('ab', 'abcdef'); 201 + expect(result.text).toBe('abcdef'); 202 + }); 203 + }); 204 + 205 + // ===================================================================== 206 + // replaceAll 207 + // ===================================================================== 208 + 209 + describe('SearchState — replaceAll', () => { 210 + it('replaces all occurrences', () => { 211 + const s = new SearchState(); 212 + s.findMatches('aXbXc', 'X'); 213 + const result = s.replaceAll('aXbXc', 'Y'); 214 + expect(result.text).toBe('aYbYc'); 215 + expect(result.count).toBe(2); 216 + }); 217 + 218 + it('returns original text when no matches', () => { 219 + const s = new SearchState(); 220 + s.findMatches('hello', 'xyz'); 221 + const result = s.replaceAll('hello', 'abc'); 222 + expect(result.text).toBe('hello'); 223 + expect(result.count).toBe(0); 224 + }); 225 + 226 + it('replaces all with empty string', () => { 227 + const s = new SearchState(); 228 + s.findMatches('a-b-c', '-'); 229 + const result = s.replaceAll('a-b-c', ''); 230 + expect(result.text).toBe('abc'); 231 + expect(result.count).toBe(2); 232 + }); 233 + 234 + it('replaces all with longer string', () => { 235 + const s = new SearchState(); 236 + s.findMatches('ab', 'a'); 237 + const result = s.replaceAll('ab', 'xyz'); 238 + expect(result.text).toBe('xyzb'); 239 + expect(result.count).toBe(1); 240 + }); 241 + 242 + it('handles adjacent matches correctly', () => { 243 + const s = new SearchState(); 244 + s.findMatches('aaa', 'a'); 245 + const result = s.replaceAll('aaa', 'bb'); 246 + expect(result.text).toBe('bbbbbb'); 247 + expect(result.count).toBe(3); 248 + }); 249 + }); 250 + 251 + // ===================================================================== 252 + // setCaseSensitive 253 + // ===================================================================== 254 + 255 + describe('SearchState — setCaseSensitive', () => { 256 + it('toggles case sensitivity', () => { 257 + const s = new SearchState(); 258 + expect(s.caseSensitive).toBe(false); 259 + s.setCaseSensitive(true); 260 + expect(s.caseSensitive).toBe(true); 261 + s.setCaseSensitive(false); 262 + expect(s.caseSensitive).toBe(false); 263 + }); 264 + 265 + it('affects subsequent findMatches calls', () => { 266 + const s = new SearchState(); 267 + s.findMatches('Hello hello', 'hello'); 268 + expect(s.matchCount).toBe(2); 269 + 270 + s.setCaseSensitive(true); 271 + s.findMatches('Hello hello', 'hello'); 272 + expect(s.matchCount).toBe(1); 273 + }); 274 + });
+342
tests/suggesting.test.ts
··· 1 + /** 2 + * Tests for the suggesting/track-changes module (src/lib/suggesting.ts). 3 + * Covers SuggestionSession, SuggestionManager, createSuggestionAttrs, and constants. 4 + */ 5 + import { describe, it, expect } from 'vitest'; 6 + import { 7 + SUGGESTION_TYPES, 8 + SESSION_TIMEOUT_MS, 9 + createSuggestionAttrs, 10 + SuggestionSession, 11 + SuggestionManager, 12 + } from '../src/lib/suggesting.js'; 13 + 14 + // ===================================================================== 15 + // Constants 16 + // ===================================================================== 17 + 18 + describe('suggesting constants', () => { 19 + it('SUGGESTION_TYPES has INSERT and DELETE', () => { 20 + expect(SUGGESTION_TYPES.INSERT).toBe('suggestion-insert'); 21 + expect(SUGGESTION_TYPES.DELETE).toBe('suggestion-delete'); 22 + }); 23 + 24 + it('SESSION_TIMEOUT_MS is 2000', () => { 25 + expect(SESSION_TIMEOUT_MS).toBe(2000); 26 + }); 27 + }); 28 + 29 + // ===================================================================== 30 + // createSuggestionAttrs 31 + // ===================================================================== 32 + 33 + describe('createSuggestionAttrs', () => { 34 + it('creates attrs with generated ID', () => { 35 + const attrs = createSuggestionAttrs({ type: 'insert', author: 'Alice' }); 36 + expect(attrs.suggestionId).toBeTruthy(); 37 + expect(attrs.author).toBe('Alice'); 38 + expect(attrs.type).toBe('insert'); 39 + expect(attrs.timestamp).toBeTruthy(); 40 + }); 41 + 42 + it('generates unique IDs', () => { 43 + const a = createSuggestionAttrs({ type: 'insert', author: 'A' }); 44 + const b = createSuggestionAttrs({ type: 'insert', author: 'A' }); 45 + expect(a.suggestionId).not.toBe(b.suggestionId); 46 + }); 47 + 48 + it('uses provided timestamp', () => { 49 + const attrs = createSuggestionAttrs({ type: 'delete', author: 'Bob', timestamp: '2026-01-01T00:00:00Z' }); 50 + expect(attrs.timestamp).toBe('2026-01-01T00:00:00Z'); 51 + }); 52 + 53 + it('handles delete type', () => { 54 + const attrs = createSuggestionAttrs({ type: 'delete', author: 'Carol' }); 55 + expect(attrs.type).toBe('delete'); 56 + }); 57 + }); 58 + 59 + // ===================================================================== 60 + // SuggestionSession 61 + // ===================================================================== 62 + 63 + describe('SuggestionSession', () => { 64 + function createSession(timeMs = 0) { 65 + let clock = timeMs; 66 + return { 67 + session: new SuggestionSession({ timeoutMs: 2000, now: () => clock }), 68 + advance(ms: number) { clock += ms; }, 69 + }; 70 + } 71 + 72 + it('starts with no active session', () => { 73 + const { session } = createSession(); 74 + expect(session.currentId).toBeNull(); 75 + }); 76 + 77 + it('creates a new session on first getAttrs', () => { 78 + const { session } = createSession(); 79 + const attrs = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 80 + expect(attrs.suggestionId).toBeTruthy(); 81 + expect(session.currentId).toBe(attrs.suggestionId); 82 + }); 83 + 84 + it('reuses session ID for consecutive edits by same author/type/position', () => { 85 + const { session } = createSession(); 86 + const a = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 87 + session.updateCursor(6); 88 + const b = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 6 }); 89 + expect(a.suggestionId).toBe(b.suggestionId); 90 + }); 91 + 92 + it('starts new session when author changes', () => { 93 + const { session } = createSession(); 94 + const a = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 95 + const b = session.getAttrs({ type: 'insert', author: 'Bob', cursorPos: 6 }); 96 + expect(a.suggestionId).not.toBe(b.suggestionId); 97 + }); 98 + 99 + it('starts new session when type changes', () => { 100 + const { session } = createSession(); 101 + const a = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 102 + const b = session.getAttrs({ type: 'delete', author: 'Alice', cursorPos: 5 }); 103 + expect(a.suggestionId).not.toBe(b.suggestionId); 104 + }); 105 + 106 + it('starts new session after timeout', () => { 107 + const { session, advance } = createSession(); 108 + const a = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 109 + advance(2001); 110 + const b = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 6 }); 111 + expect(a.suggestionId).not.toBe(b.suggestionId); 112 + }); 113 + 114 + it('reuses session within timeout', () => { 115 + const { session, advance } = createSession(); 116 + const a = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 117 + session.updateCursor(6); 118 + advance(1999); 119 + const b = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 6 }); 120 + expect(a.suggestionId).toBe(b.suggestionId); 121 + }); 122 + 123 + it('starts new session on cursor jump (>1 position)', () => { 124 + const { session } = createSession(); 125 + const a = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 126 + const b = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 10 }); 127 + expect(a.suggestionId).not.toBe(b.suggestionId); 128 + }); 129 + 130 + it('allows adjacent cursor (exactly 1 position away)', () => { 131 + const { session } = createSession(); 132 + const a = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 133 + const b = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 6 }); 134 + expect(a.suggestionId).toBe(b.suggestionId); 135 + }); 136 + 137 + it('handles null cursor position', () => { 138 + const { session } = createSession(); 139 + const a = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: null }); 140 + const b = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: null }); 141 + expect(a.suggestionId).toBe(b.suggestionId); 142 + }); 143 + 144 + it('resetSession clears the active session', () => { 145 + const { session } = createSession(); 146 + session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 147 + expect(session.currentId).not.toBeNull(); 148 + session.resetSession(); 149 + expect(session.currentId).toBeNull(); 150 + }); 151 + 152 + it('new session starts after resetSession', () => { 153 + const { session } = createSession(); 154 + const a = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 155 + session.resetSession(); 156 + const b = session.getAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 157 + expect(a.suggestionId).not.toBe(b.suggestionId); 158 + }); 159 + }); 160 + 161 + // ===================================================================== 162 + // SuggestionManager 163 + // ===================================================================== 164 + 165 + describe('SuggestionManager', () => { 166 + it('starts in editing mode', () => { 167 + const mgr = new SuggestionManager(); 168 + expect(mgr.isSuggesting()).toBe(false); 169 + }); 170 + 171 + it('setMode to suggesting', () => { 172 + const mgr = new SuggestionManager(); 173 + mgr.setMode('suggesting'); 174 + expect(mgr.isSuggesting()).toBe(true); 175 + }); 176 + 177 + it('setMode back to editing', () => { 178 + const mgr = new SuggestionManager(); 179 + mgr.setMode('suggesting'); 180 + mgr.setMode('editing'); 181 + expect(mgr.isSuggesting()).toBe(false); 182 + }); 183 + 184 + it('toggleMode switches between editing and suggesting', () => { 185 + const mgr = new SuggestionManager(); 186 + mgr.toggleMode(); 187 + expect(mgr.isSuggesting()).toBe(true); 188 + mgr.toggleMode(); 189 + expect(mgr.isSuggesting()).toBe(false); 190 + }); 191 + 192 + it('addSuggestion and getSuggestion', () => { 193 + const mgr = new SuggestionManager(); 194 + const attrs = createSuggestionAttrs({ type: 'insert', author: 'Alice' }); 195 + mgr.addSuggestion(attrs); 196 + expect(mgr.getSuggestion(attrs.suggestionId)).toEqual(attrs); 197 + }); 198 + 199 + it('getSuggestion returns null for unknown ID', () => { 200 + const mgr = new SuggestionManager(); 201 + expect(mgr.getSuggestion('nonexistent')).toBeNull(); 202 + }); 203 + 204 + it('getSuggestions returns all tracked', () => { 205 + const mgr = new SuggestionManager(); 206 + const a = createSuggestionAttrs({ type: 'insert', author: 'Alice' }); 207 + const b = createSuggestionAttrs({ type: 'delete', author: 'Bob' }); 208 + mgr.addSuggestion(a); 209 + mgr.addSuggestion(b); 210 + expect(mgr.getSuggestions().length).toBe(2); 211 + }); 212 + 213 + it('getSuggestionsByAuthor filters correctly', () => { 214 + const mgr = new SuggestionManager(); 215 + mgr.addSuggestion(createSuggestionAttrs({ type: 'insert', author: 'Alice' })); 216 + mgr.addSuggestion(createSuggestionAttrs({ type: 'insert', author: 'Bob' })); 217 + mgr.addSuggestion(createSuggestionAttrs({ type: 'delete', author: 'Alice' })); 218 + expect(mgr.getSuggestionsByAuthor('Alice').length).toBe(2); 219 + expect(mgr.getSuggestionsByAuthor('Bob').length).toBe(1); 220 + }); 221 + 222 + describe('accept', () => { 223 + it('returns null for unknown ID', () => { 224 + const mgr = new SuggestionManager(); 225 + expect(mgr.accept('unknown')).toBeNull(); 226 + }); 227 + 228 + it('accept INSERT → remove-mark', () => { 229 + const mgr = new SuggestionManager(); 230 + const attrs = createSuggestionAttrs({ type: 'insert', author: 'Alice' }); 231 + mgr.addSuggestion(attrs); 232 + const action = mgr.accept(attrs.suggestionId); 233 + expect(action).toEqual({ action: 'remove-mark', type: 'insert', suggestionId: attrs.suggestionId }); 234 + }); 235 + 236 + it('accept DELETE → delete-text', () => { 237 + const mgr = new SuggestionManager(); 238 + const attrs = createSuggestionAttrs({ type: 'delete', author: 'Alice' }); 239 + mgr.addSuggestion(attrs); 240 + const action = mgr.accept(attrs.suggestionId); 241 + expect(action).toEqual({ action: 'delete-text', type: 'delete', suggestionId: attrs.suggestionId }); 242 + }); 243 + 244 + it('removes suggestion after accept', () => { 245 + const mgr = new SuggestionManager(); 246 + const attrs = createSuggestionAttrs({ type: 'insert', author: 'Alice' }); 247 + mgr.addSuggestion(attrs); 248 + mgr.accept(attrs.suggestionId); 249 + expect(mgr.getSuggestion(attrs.suggestionId)).toBeNull(); 250 + }); 251 + }); 252 + 253 + describe('reject', () => { 254 + it('returns null for unknown ID', () => { 255 + const mgr = new SuggestionManager(); 256 + expect(mgr.reject('unknown')).toBeNull(); 257 + }); 258 + 259 + it('reject INSERT → delete-text', () => { 260 + const mgr = new SuggestionManager(); 261 + const attrs = createSuggestionAttrs({ type: 'insert', author: 'Alice' }); 262 + mgr.addSuggestion(attrs); 263 + const action = mgr.reject(attrs.suggestionId); 264 + expect(action).toEqual({ action: 'delete-text', type: 'insert', suggestionId: attrs.suggestionId }); 265 + }); 266 + 267 + it('reject DELETE → remove-mark', () => { 268 + const mgr = new SuggestionManager(); 269 + const attrs = createSuggestionAttrs({ type: 'delete', author: 'Alice' }); 270 + mgr.addSuggestion(attrs); 271 + const action = mgr.reject(attrs.suggestionId); 272 + expect(action).toEqual({ action: 'remove-mark', type: 'delete', suggestionId: attrs.suggestionId }); 273 + }); 274 + 275 + it('removes suggestion after reject', () => { 276 + const mgr = new SuggestionManager(); 277 + const attrs = createSuggestionAttrs({ type: 'delete', author: 'A' }); 278 + mgr.addSuggestion(attrs); 279 + mgr.reject(attrs.suggestionId); 280 + expect(mgr.getSuggestion(attrs.suggestionId)).toBeNull(); 281 + }); 282 + }); 283 + 284 + describe('acceptAll / rejectAll', () => { 285 + it('acceptAll returns actions for all suggestions', () => { 286 + const mgr = new SuggestionManager(); 287 + mgr.addSuggestion(createSuggestionAttrs({ type: 'insert', author: 'A' })); 288 + mgr.addSuggestion(createSuggestionAttrs({ type: 'delete', author: 'B' })); 289 + const actions = mgr.acceptAll(); 290 + expect(actions.length).toBe(2); 291 + expect(mgr.getSuggestions().length).toBe(0); 292 + }); 293 + 294 + it('rejectAll returns actions for all suggestions', () => { 295 + const mgr = new SuggestionManager(); 296 + mgr.addSuggestion(createSuggestionAttrs({ type: 'insert', author: 'A' })); 297 + mgr.addSuggestion(createSuggestionAttrs({ type: 'delete', author: 'B' })); 298 + const actions = mgr.rejectAll(); 299 + expect(actions.length).toBe(2); 300 + expect(mgr.getSuggestions().length).toBe(0); 301 + }); 302 + 303 + it('acceptAll on empty returns empty array', () => { 304 + const mgr = new SuggestionManager(); 305 + expect(mgr.acceptAll()).toEqual([]); 306 + }); 307 + 308 + it('rejectAll on empty returns empty array', () => { 309 + const mgr = new SuggestionManager(); 310 + expect(mgr.rejectAll()).toEqual([]); 311 + }); 312 + }); 313 + 314 + describe('session integration', () => { 315 + it('getSessionAttrs returns attrs with session grouping', () => { 316 + let clock = 0; 317 + const mgr = new SuggestionManager({ now: () => clock }); 318 + const a = mgr.getSessionAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 319 + mgr.updateSessionCursor(6); 320 + const b = mgr.getSessionAttrs({ type: 'insert', author: 'Alice', cursorPos: 6 }); 321 + expect(a.suggestionId).toBe(b.suggestionId); 322 + }); 323 + 324 + it('resetSession forces new session', () => { 325 + const mgr = new SuggestionManager(); 326 + const a = mgr.getSessionAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 327 + mgr.resetSession(); 328 + const b = mgr.getSessionAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 329 + expect(a.suggestionId).not.toBe(b.suggestionId); 330 + }); 331 + 332 + it('switching to editing mode resets session', () => { 333 + const mgr = new SuggestionManager(); 334 + mgr.setMode('suggesting'); 335 + const a = mgr.getSessionAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 336 + mgr.setMode('editing'); 337 + mgr.setMode('suggesting'); 338 + const b = mgr.getSessionAttrs({ type: 'insert', author: 'Alice', cursorPos: 5 }); 339 + expect(a.suggestionId).not.toBe(b.suggestionId); 340 + }); 341 + }); 342 + });
+15
tests/tab-handler.test.ts
··· 1 + /** 2 + * Tests for sheet tab key handler (src/sheets/tab-handler.ts). 3 + */ 4 + import { describe, it, expect } from 'vitest'; 5 + import { resolveSheetTabAction } from '../src/sheets/tab-handler.js'; 6 + 7 + describe('resolveSheetTabAction', () => { 8 + it('returns moveRight for Tab (no shift)', () => { 9 + expect(resolveSheetTabAction({ shiftKey: false })).toBe('moveRight'); 10 + }); 11 + 12 + it('returns moveLeft for Shift+Tab', () => { 13 + expect(resolveSheetTabAction({ shiftKey: true })).toBe('moveLeft'); 14 + }); 15 + });