Skip to content

Instantly share code, notes, and snippets.

@shaggun
Last active June 10, 2025 14:22
Show Gist options
  • Select an option

  • Save shaggun/0b3f2aab4282b781d0a7acef28e19bfc to your computer and use it in GitHub Desktop.

Select an option

Save shaggun/0b3f2aab4282b781d0a7acef28e19bfc to your computer and use it in GitHub Desktop.
Expanded onHighlight tests with edge cases
import * as React from 'react';
import { expect } from 'chai';
import { createClientRender, fireEvent, screen, act } from 'test/utils';
import { spy } from 'sinon';
import Autocomplete from './Autocomplete';
import TextField from '../TextField';
describe('<Autocomplete /> - Bug fix for onHighlightChange with input changes', () => {
const render = createClientRender();
it('should call onHighlightChange with correct option when input changes result in different filtered options', () => {
const handleHighlightChange = spy();
const options = ['option 1 1', 'option 2 12', 'option 3 123'];
render(
<Autocomplete
options={options}
autoHighlight
onHighlightChange={handleHighlightChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);
const textbox = screen.getByRole('textbox');
// Clear any initial calls
handleHighlightChange.resetHistory();
// Type "1" - should highlight "option 1 1" (first match)
act(() => {
fireEvent.change(textbox, { target: { value: '1' } });
});
// Check that the first option matching "1" is highlighted
expect(handleHighlightChange.callCount).to.be.greaterThan(0);
const firstCall = handleHighlightChange.lastCall;
expect(firstCall.args[1]).to.equal('option 1 1');
expect(firstCall.args[2]).to.equal('auto');
// Reset spy to track next call clearly
handleHighlightChange.resetHistory();
// Type "2" (so input becomes "12") - should highlight "option 2 12" (first match for "12")
act(() => {
fireEvent.change(textbox, { target: { value: '12' } });
});
// This should now correctly highlight "option 2 12"
expect(handleHighlightChange.callCount).to.be.greaterThan(0);
const secondCall = handleHighlightChange.lastCall;
expect(secondCall.args[1]).to.equal('option 2 12');
expect(secondCall.args[2]).to.equal('auto');
});
it('should handle rapid typing correctly', () => {
const handleHighlightChange = spy();
const options = ['apple', 'application', 'apply', 'banana', 'band', 'bandana'];
render(
<Autocomplete
options={options}
autoHighlight
onHighlightChange={handleHighlightChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>,
);
const textbox = screen.getByRole('textbox');
handleHighlightChange.resetHistory();
// Type "a" - should highlight "apple"
act(() => {
fireEvent.change(textbox, { target: { value: 'a' } });
});
// Type "p" (input becomes "ap") - should highlight "apple"
act(() => {
fireEvent.change(textbox, { target: { value: 'ap' } });
});
// Type "p" (input becomes "app") - should highlight "apple"
act(() => {
fireEvent.change(textbox, { target: { value: 'app' } });
});
// Verify the last call has the correct option
const lastCall = handleHighlightChange.lastCall;
expect(lastCall.args[1]).to.equal('apple');
});
it('should update highlight when first option changes without input change', async () => {
const handleHighlightChange = spy();
const TestComponent = () => {
const [options, setOptions] = React.useState(['apple', 'apricot', 'application']);
React.useEffect(() => {
// After a short delay, reorder options so 'application' becomes first
const timer = setTimeout(() => {
act(() => {
setOptions(['application', 'apricot', 'apple']);
});
}, 50);
return () => clearTimeout(timer);
}, []);
return (
<Autocomplete
options={options}
autoHighlight
onHighlightChange={handleHighlightChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>
);
};
render(<TestComponent />);
const textbox = screen.getByRole('textbox');
// Type "ap" - initially should highlight "apple" (first match)
act(() => {
fireEvent.change(textbox, { target: { value: 'ap' } });
});
handleHighlightChange.resetHistory();
// Wait for options to change - this creates the edge case where:
// - inputValue stays "ap" (no change)
// - filteredOptions.length stays 3 (no change)
// - but first option changes from "apple" to "application"
await act(async () => {
await new Promise((resolve) => {
setTimeout(() => {
// This should highlight "application" as the new first option
expect(handleHighlightChange.callCount).to.be.greaterThan(0);
const lastCall = handleHighlightChange.lastCall;
expect(lastCall.args[1]).to.equal('application');
expect(lastCall.args[2]).to.equal('auto');
resolve();
}, 100);
});
});
});
it('should update highlight when options array changes with same input and same length', async () => {
const handleHighlightChange = spy();
const TestComponent = () => {
const [options, setOptions] = React.useState(['banana', 'band', 'bandana']);
React.useEffect(() => {
// Change options while keeping same length and same filtering pattern
const timer = setTimeout(() => {
act(() => {
setOptions(['band', 'banana', 'bandana']); // 'band' becomes first instead of 'banana'
});
}, 50);
return () => clearTimeout(timer);
}, []);
return (
<Autocomplete
options={options}
autoHighlight
onHighlightChange={handleHighlightChange}
renderInput={(params) => <TextField {...params} autoFocus />}
/>
);
};
render(<TestComponent />);
const textbox = screen.getByRole('textbox');
// Type "ban" - initially should highlight "banana"
act(() => {
fireEvent.change(textbox, { target: { value: 'ban' } });
});
handleHighlightChange.resetHistory();
// Wait for options to change
await act(async () => {
await new Promise((resolve) => {
setTimeout(() => {
// Should now highlight "band" as new first option
expect(handleHighlightChange.callCount).to.be.greaterThan(0);
const lastCall = handleHighlightChange.lastCall;
expect(lastCall.args[1]).to.equal('band');
expect(lastCall.args[2]).to.equal('auto');
resolve();
}, 100);
});
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment