test(addon-search): add regression tests for search state and edge cases

Add SearchState and SearchResultTracker unit tests plus whole-word,
reverse-regex, and lowercase cache coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Daniel Imms
2026-05-25 18:02:22 -07:00
co-authored by Cursor
parent ee322c811f
commit 6d08f5938d
4 changed files with 131 additions and 0 deletions
@@ -256,6 +256,12 @@ describe('SearchEngine', () => {
assert.strictEqual(searchEngine.find('.*?', 0, 0, { regex: true }), undefined); // Empty matches should be ignored
});
it('should ignore empty regex matches when searching backwards', async () => {
await writeP(terminal, 'Hello World');
assert.strictEqual(searchEngine.findPreviousWithSelection('.*?', { regex: true }), undefined);
});
});
describe('combined options', () => {
@@ -244,6 +244,17 @@ describe('SearchLineCache', () => {
assert.equal(retrieved![1].length, 4);
});
it('should lazily cache lowercased line text via SearchEngine usage', async () => {
await writeP(terminal, 'Hello WORLD');
cache.initLinesCache();
const entry = cache.translateBufferLineToStringWithWrap(0, true);
cache.setLineInCache(0, entry);
assert.equal(entry[2], undefined);
entry[2] = entry[0].toLowerCase();
cache.setLineInCache(0, entry);
assert.strictEqual(cache.getLineFromCache(0)?.[2], 'hello world');
});
it('should handle unicode characters in cache entries', () => {
const entry: LineCacheEntry = [
'Hello 世界 🌍 测试',
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { SearchResultTracker } from './SearchResultTracker';
import type { ISearchResult } from './SearchEngine';
describe('SearchResultTracker', () => {
let tracker: SearchResultTracker;
beforeEach(() => {
tracker = new SearchResultTracker();
});
afterEach(() => {
tracker.dispose();
});
describe('updateResults', () => {
it('should store results without copying when within limit', () => {
const results: ISearchResult[] = [{ term: 'a', row: 0, col: 0, size: 1 }];
tracker.updateResults(results, 10);
assert.strictEqual(tracker.searchResults, results);
});
});
describe('findResultIndex', () => {
it('should return the index of a tracked result', () => {
const results: ISearchResult[] = [
{ term: 'a', row: 0, col: 0, size: 1 },
{ term: 'b', row: 0, col: 2, size: 1 }
];
tracker.updateResults(results, 10);
assert.strictEqual(tracker.findResultIndex(results[1]), 1);
});
it('should return -1 when the result is not tracked', () => {
tracker.updateResults([{ term: 'a', row: 0, col: 0, size: 1 }], 10);
assert.strictEqual(tracker.findResultIndex({ term: 'z', row: 1, col: 0, size: 1 }), -1);
});
});
describe('fireResultsChanged', () => {
it('should not fire when decorations are disabled', () => {
let fired = false;
tracker.onDidChangeResults(() => fired = true);
tracker.updateResults([{ term: 'a', row: 0, col: 0, size: 1 }], 10);
tracker.fireResultsChanged(false);
assert.strictEqual(fired, false);
});
});
});
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { SearchState } from './SearchState';
import type { ISearchOptions } from '@xterm/addon-search';
const DECORATIONS_A: ISearchOptions = {
decorations: {
activeMatchColorOverviewRuler: '#ff0000',
matchOverviewRuler: '#00ffff'
}
};
const DECORATIONS_B: ISearchOptions = {
decorations: {
activeMatchColorOverviewRuler: '#00ff00',
matchOverviewRuler: '#00ffff'
}
};
describe('SearchState', () => {
let state: SearchState;
beforeEach(() => {
state = new SearchState();
});
describe('didOptionsChange', () => {
it('should detect caseSensitive changes', () => {
state.lastSearchOptions = { caseSensitive: false };
assert.strictEqual(state.didOptionsChange({ caseSensitive: true }), true);
});
it('should detect decoration option changes', () => {
state.lastSearchOptions = DECORATIONS_A;
assert.strictEqual(state.didOptionsChange(DECORATIONS_B), true);
});
it('should return false when semantic options are unchanged', () => {
const options: ISearchOptions = { caseSensitive: false, regex: false, wholeWord: false };
state.lastSearchOptions = options;
assert.strictEqual(state.didOptionsChange({ caseSensitive: false, regex: false, wholeWord: false }), false);
});
});
describe('shouldUpdateHighlighting', () => {
it('should update when options change for the same term', () => {
state.cachedSearchTerm = 'test';
state.lastSearchOptions = { caseSensitive: false, decorations: DECORATIONS_A.decorations };
assert.strictEqual(state.shouldUpdateHighlighting('test', { caseSensitive: true, decorations: DECORATIONS_A.decorations }), true);
});
it('should not update without decorations', () => {
state.cachedSearchTerm = 'test';
state.lastSearchOptions = { caseSensitive: false };
assert.strictEqual(state.shouldUpdateHighlighting('test', { caseSensitive: true }), false);
});
});
});