feat(autocomplete): minor refactor and add unit tests

This commit is contained in:
Bijin A B
2026-02-06 20:47:19 +05:30
parent 3c0d9ccd4c
commit edee75e372
2 changed files with 142 additions and 7 deletions

View File

@@ -394,12 +394,15 @@ const getCurrentWordWithContext = (cm) => {
const extractNextSegmentSuggestions = (filteredHints, currentInput) => {
const prefixMatches = new Set();
const substringMatches = new Set();
const lowerInput = currentInput.toLowerCase();
filteredHints.forEach((hint) => {
const lowerHint = hint.toLowerCase();
// For prefix matches, use the original progressive logic
if (hint.toLowerCase().startsWith(currentInput.toLowerCase())) {
if (lowerHint.startsWith(lowerInput)) {
// Handle exact match case
if (hint.toLowerCase() === currentInput.toLowerCase()) {
if (lowerHint === lowerInput) {
prefixMatches.add(hint.substring(hint.lastIndexOf('.') + 1));
return;
}
@@ -417,13 +420,13 @@ const extractNextSegmentSuggestions = (filteredHints, currentInput) => {
const lastDotInInput = currentInput.lastIndexOf('.');
const currentSegmentStart = lastDotInInput + 1;
const nextDotAfterInput = hint.indexOf('.', currentSegmentStart);
const segment =
nextDotAfterInput === -1
const segment
= nextDotAfterInput === -1
? hint.substring(currentSegmentStart)
: hint.substring(currentSegmentStart, nextDotAfterInput);
prefixMatches.add(segment);
}
} else if (hint.toLowerCase().includes(currentInput.toLowerCase())) {
} else if (lowerHint.includes(lowerInput)) {
// For substring matches (search within words), suggest the complete hint
substringMatches.add(hint);
}
@@ -486,8 +489,9 @@ const filterHintsByContext = (categorizedHints, currentWord, context, showHintsF
const allowedHints = getAllowedHintsByContext(categorizedHints, context, showHintsFor);
const lowerWord = currentWord.toLowerCase();
const filtered = allowedHints.filter((hint) => {
return hint.toLowerCase().includes(currentWord.toLowerCase());
return hint.toLowerCase().includes(lowerWord);
});
const hintParts = getHintParts(filtered, currentWord);
@@ -719,6 +723,9 @@ export const setupAutoComplete = (editor, options = {}) => {
};
};
// Exported for testing
export { extractNextSegmentSuggestions };
// Initialize autocomplete command if not already present
if (!CodeMirror.commands.autocomplete) {
CodeMirror.commands.autocomplete = (cm, hint, options) => {

View File

@@ -18,7 +18,8 @@ jest.mock('codemirror', () => {
// Import the functions to test
import {
getAutoCompleteHints,
setupAutoComplete
setupAutoComplete,
extractNextSegmentSuggestions
} from './autocomplete';
describe('Bruno Autocomplete', () => {
@@ -403,6 +404,133 @@ describe('Bruno Autocomplete', () => {
});
});
describe('extractNextSegmentSuggestions', () => {
describe('prefix matching', () => {
it('should extract the current segment for a partial prefix match', () => {
const hints = ['req.getUrl()', 'req.getMethod()', 'req.setUrl(url)'];
const result = extractNextSegmentSuggestions(hints, 'req.get');
expect(result).toEqual(['getMethod()', 'getUrl()']);
});
it('should return the next segment after a trailing dot', () => {
const hints = ['bru.cookies.jar()', 'bru.runner.skipRequest()'];
const result = extractNextSegmentSuggestions(hints, 'bru.');
expect(result).toEqual(['cookies', 'runner']);
});
it('should return the last segment on exact match', () => {
const hints = ['req.url'];
const result = extractNextSegmentSuggestions(hints, 'req.url');
expect(result).toEqual(['url']);
});
it('should deduplicate segments from multiple hints', () => {
const hints = ['bru.cookies.jar().getCookie(url, name, callback)', 'bru.cookies.jar().getCookies(url, callback)'];
const result = extractNextSegmentSuggestions(hints, 'bru.');
expect(result).toEqual(['cookies']);
});
it('should extract top-level segment when input has no dots', () => {
const hints = ['req.url', 'req.getUrl()', 'res.url'];
const result = extractNextSegmentSuggestions(hints, 'r');
expect(result).toEqual(['req', 'res']);
});
});
describe('substring matching', () => {
it('should return full hints for substring-only matches', () => {
const hints = ['base_url', 'api_url', 'url_prefix'];
const result = extractNextSegmentSuggestions(hints, 'url');
// url_prefix is a prefix match (segment), base_url and api_url are substring matches (full hints)
expect(result).toEqual(['url_prefix', 'api_url', 'base_url']);
});
it('should return full hints for dotted substring matches', () => {
const hints = ['req.getUrl()', 'req.setUrl(url)', 'req.url'];
const result = extractNextSegmentSuggestions(hints, 'Url');
expect(result).toEqual(['req.getUrl()', 'req.setUrl(url)', 'req.url']);
});
it('should not include hints that do not contain the input', () => {
const hints = ['base_url', 'api_key', 'url_prefix'];
const result = extractNextSegmentSuggestions(hints, 'url');
expect(result).not.toContain('api_key');
});
});
describe('ordering', () => {
it('should return prefix matches before substring matches', () => {
const hints = ['base_url', 'url_prefix'];
const result = extractNextSegmentSuggestions(hints, 'url');
// url_prefix is prefix → segment "url_prefix"; base_url is substring → full hint
expect(result).toEqual(['url_prefix', 'base_url']);
});
it('should sort prefix matches alphabetically among themselves', () => {
const hints = ['req.setUrl(url)', 'req.getUrl()', 'req.getMethod()'];
const result = extractNextSegmentSuggestions(hints, 'req.');
expect(result).toEqual(['getMethod()', 'getUrl()', 'setUrl(url)']);
});
it('should sort substring matches alphabetically among themselves', () => {
const hints = ['z_url', 'a_url'];
const result = extractNextSegmentSuggestions(hints, 'url');
// Both are substring-only matches
expect(result).toEqual(['a_url', 'z_url']);
});
});
describe('case insensitivity', () => {
it('should match prefix regardless of case', () => {
const hints = ['Content-Type', 'Content-Length'];
const result = extractNextSegmentSuggestions(hints, 'content');
expect(result).toEqual(['Content-Length', 'Content-Type']);
});
it('should match substring regardless of case', () => {
const hints = ['X-Custom-Type', 'Accept'];
const result = extractNextSegmentSuggestions(hints, 'type');
expect(result).toEqual(['X-Custom-Type']);
});
});
describe('edge cases', () => {
it('should return an empty array when no hints match', () => {
const hints = ['foo', 'bar', 'baz'];
const result = extractNextSegmentSuggestions(hints, 'xyz');
expect(result).toEqual([]);
});
it('should return an empty array for empty hints list', () => {
const result = extractNextSegmentSuggestions([], 'url');
expect(result).toEqual([]);
});
it('should handle single-character input', () => {
const hints = ['apple', 'banana', 'avocado'];
const result = extractNextSegmentSuggestions(hints, 'a');
// apple and avocado are prefix matches, banana contains 'a' as substring
expect(result).toEqual(['apple', 'avocado', 'banana']);
});
});
});
describe('setupAutoComplete', () => {
let mockGetAllVariables;
let cleanupFn;