1+ using AngleSharp . Dom ;
2+ using AngleSharp . Html . Dom ;
3+ using Bunit ;
4+ using FluentAssertions ;
5+ using Microsoft . AspNetCore . Components ;
6+ using Microsoft . JSInterop ;
7+ using Xunit ;
8+
9+ namespace SimpleBlazorMultiselect . Tests ;
10+
11+ public class SimpleMultiselectTests : TestContext
12+ {
13+ private readonly List < string > _testOptions = new ( ) { "Apple" , "Banana" , "Cherry" , "Date" , "Elderberry" } ;
14+
15+ public SimpleMultiselectTests ( )
16+ {
17+ JSInterop . SetupModule ( "./_content/SimpleBlazorMultiselect/js/simpleMultiselect.js" )
18+ . SetupModule ( "register" , invocation => invocation . Arguments . Count == 2 )
19+ . SetupVoid ( "dispose" ) ;
20+ }
21+
22+ [ Fact ]
23+ public void Component_RendersWithDefaultText_WhenNoOptionsSelected ( )
24+ {
25+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
26+ . Add ( p => p . Options , _testOptions )
27+ . Add ( p => p . DefaultText , "Choose items" ) ) ;
28+
29+ var button = component . Find ( "button" ) ;
30+ button . TextContent . Should ( ) . Contain ( "Choose items" ) ;
31+ }
32+
33+ [ Fact ]
34+ public void Component_RendersSelectedOptions_WhenOptionsAreSelected ( )
35+ {
36+ var selectedOptions = new HashSet < string > { "Apple" , "Banana" } ;
37+
38+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
39+ . Add ( p => p . Options , _testOptions )
40+ . Add ( p => p . SelectedOptions , selectedOptions ) ) ;
41+
42+ var button = component . Find ( "button" ) ;
43+ button . TextContent . Should ( ) . Contain ( "Apple, Banana" ) ;
44+ }
45+
46+ [ Fact ]
47+ public void Component_TogglesDropdown_WhenButtonClicked ( )
48+ {
49+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
50+ . Add ( p => p . Options , _testOptions ) ) ;
51+
52+ var button = component . Find ( "button" ) ;
53+ button . Click ( ) ;
54+
55+ var dropdown = component . Find ( ".dropdown-menu.show" ) ;
56+ dropdown . Should ( ) . NotBeNull ( ) ;
57+ }
58+
59+ [ Fact ]
60+ public void Component_ShowsAllOptions_WhenDropdownIsOpen ( )
61+ {
62+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
63+ . Add ( p => p . Options , _testOptions ) ) ;
64+
65+ var button = component . Find ( "button" ) ;
66+ button . Click ( ) ;
67+
68+ var dropdownItems = component . FindAll ( ".dropdown-item" ) ;
69+ dropdownItems . Should ( ) . HaveCount ( _testOptions . Count ) ;
70+
71+ foreach ( var option in _testOptions )
72+ {
73+ component . Markup . Should ( ) . Contain ( option ) ;
74+ }
75+ }
76+
77+ [ Fact ]
78+ public void Component_SelectsOption_WhenOptionClicked ( )
79+ {
80+ var selectedOptions = new HashSet < string > ( ) ;
81+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
82+ . Add ( p => p . Options , _testOptions )
83+ . Add ( p => p . SelectedOptions , selectedOptions )
84+ . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < string > > ( this , newSelection =>
85+ {
86+ selectedOptions = newSelection ;
87+ } ) ) ) ;
88+
89+ var button = component . Find ( "button" ) ;
90+ button . Click ( ) ;
91+
92+ var firstOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
93+ firstOption . Click ( ) ;
94+
95+ firstOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
96+ var checkbox = firstOption . QuerySelector < IHtmlInputElement > ( "input[type='checkbox']" ) ;
97+ checkbox . Should ( ) . NotBeNull ( ) ;
98+ checkbox . IsChecked . Should ( ) . BeTrue ( ) ;
99+ selectedOptions . Should ( ) . Contain ( "Apple" ) ;
100+ }
101+
102+ [ Fact ]
103+ public void Component_DeselectsOption_WhenSelectedOptionClicked ( )
104+ {
105+ var selectedOptions = new HashSet < string > { "Apple" } ;
106+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
107+ . Add ( p => p . Options , _testOptions )
108+ . Add ( p => p . SelectedOptions , selectedOptions )
109+ . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < string > > ( this , newSelection =>
110+ {
111+ selectedOptions = newSelection ;
112+ } ) ) ) ;
113+
114+ var button = component . Find ( "button" ) ;
115+ button . Click ( ) ;
116+
117+ var firstOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
118+ firstOption . Click ( ) ;
119+
120+ selectedOptions . Should ( ) . NotContain ( "Apple" ) ;
121+ foreach ( var option in component . FindAll ( ".dropdown-item" ) )
122+ {
123+ var cb = option . QuerySelector < IHtmlInputElement > ( "input[type='checkbox']" ) ;
124+ cb ! . IsChecked . Should ( ) . BeFalse ( ) ;
125+ }
126+ }
127+
128+ [ Fact ]
129+ public void Component_ShowsFilterInput_WhenCanFilterIsTrue ( )
130+ {
131+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
132+ . Add ( p => p . Options , _testOptions )
133+ . Add ( p => p . CanFilter , true ) ) ;
134+
135+ var button = component . Find ( "button" ) ;
136+ button . Click ( ) ;
137+
138+ var filterInput = component . Find ( ".simple-filter-input" ) ;
139+ filterInput . Should ( ) . NotBeNull ( ) ;
140+ filterInput . GetAttribute ( "placeholder" ) . Should ( ) . Be ( "Filter..." ) ;
141+ }
142+
143+ [ Fact ]
144+ public void Component_FiltersOptions_WhenFilterTextEntered ( )
145+ {
146+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
147+ . Add ( p => p . Options , _testOptions )
148+ . Add ( p => p . CanFilter , true ) ) ;
149+
150+ var button = component . Find ( "button" ) ;
151+ button . Click ( ) ;
152+
153+ var filterInput = component . Find ( ".simple-filter-input" ) ;
154+ filterInput . Input ( "App" ) ;
155+
156+ var dropdownItems = component . FindAll ( ".dropdown-item" ) ;
157+ dropdownItems . Should ( ) . HaveCount ( 1 ) ;
158+ dropdownItems [ 0 ] . TextContent . Should ( ) . Contain ( "Apple" ) ;
159+ }
160+
161+ [ Fact ]
162+ public void Component_UsesCustomStringSelector_WhenProvided ( )
163+ {
164+ var complexOptions = new List < TestItem >
165+ {
166+ new ( "1" , "Apple" ) ,
167+ new ( "2" , "Banana" )
168+ } ;
169+
170+ var component = RenderComponent < SimpleMultiselect < TestItem > > ( parameters => parameters
171+ . Add ( p => p . Options , complexOptions )
172+ . Add ( p => p . StringSelector , item => item . Name ) ) ;
173+
174+ var button = component . Find ( "button" ) ;
175+ button . Click ( ) ;
176+
177+ component . Markup . Should ( ) . Contain ( "Apple" ) ;
178+ component . Markup . Should ( ) . Contain ( "Banana" ) ;
179+ }
180+
181+ [ Fact ]
182+ public void Component_UsesCustomFilterPredicate_WhenProvided ( )
183+ {
184+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
185+ . Add ( p => p . Options , _testOptions )
186+ . Add ( p => p . CanFilter , true )
187+ . Add ( p => p . FilterPredicate , ( item , filter ) => item . StartsWith ( filter , StringComparison . OrdinalIgnoreCase ) ) ) ;
188+
189+ var button = component . Find ( "button" ) ;
190+ button . Click ( ) ;
191+
192+ var filterInput = component . Find ( ".simple-filter-input" ) ;
193+ filterInput . Input ( "B" ) ;
194+
195+ var dropdownItems = component . FindAll ( ".dropdown-item" ) ;
196+ dropdownItems . Should ( ) . HaveCount ( 1 ) ;
197+ dropdownItems [ 0 ] . TextContent . Should ( ) . Contain ( "Banana" ) ;
198+ }
199+
200+ [ Fact ]
201+ public void Component_HandlesEmptyOptions_Gracefully ( )
202+ {
203+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
204+ . Add ( p => p . Options , new List < string > ( ) ) ) ;
205+
206+ var button = component . Find ( "button" ) ;
207+ button . Click ( ) ;
208+
209+ var dropdownItems = component . FindAll ( ".dropdown-item" ) ;
210+ dropdownItems . Should ( ) . BeEmpty ( ) ;
211+ }
212+
213+ [ Fact ]
214+ public void Component_SingleSelectMode_SelectsOnlyOneOption ( )
215+ {
216+ var selectedOptions = new HashSet < string > ( ) ;
217+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
218+ . Add ( p => p . Options , _testOptions )
219+ . Add ( p => p . SelectedOptions , selectedOptions )
220+ . Add ( p => p . IsMultiSelect , false )
221+ . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < string > > ( this , newSelection =>
222+ {
223+ selectedOptions = newSelection ;
224+ } ) ) ) ;
225+
226+ var button = component . Find ( "button" ) ;
227+ button . Click ( ) ;
228+
229+ var firstOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
230+ firstOption . Click ( ) ;
231+ component . Render ( ) ;
232+
233+ JSInterop . VerifyInvoke ( "dispose" ) ;
234+ component . Instance . IsDropdownOpen . Should ( ) . BeFalse ( ) ;
235+
236+ button . Click ( ) ;
237+ var secondOption = component . FindAll ( ".dropdown-item" ) [ 1 ] ;
238+ secondOption . Click ( ) ;
239+
240+ selectedOptions . Should ( ) . HaveCount ( 1 ) ;
241+ selectedOptions . Should ( ) . Contain ( "Banana" ) ;
242+ selectedOptions . Should ( ) . NotContain ( "Apple" ) ;
243+ }
244+
245+ [ Fact ]
246+ public void Component_AppliesCustomCssClasses_WhenProvided ( )
247+ {
248+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
249+ . Add ( p => p . Options , _testOptions )
250+ . Add ( p => p . Class , "custom-class" ) ) ;
251+
252+ var container = component . Find ( ".simple-dropdown" ) ;
253+ container . ClassList . Should ( ) . Contain ( "custom-class" ) ;
254+ }
255+
256+ [ Fact ]
257+ public void Component_AppliesCustomStyles_WhenProvided ( )
258+ {
259+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
260+ . Add ( p => p . Options , _testOptions )
261+ . Add ( p => p . Style , "width: 300px;" ) ) ;
262+
263+ var container = component . Find ( ".simple-dropdown" ) ;
264+ container . GetAttribute ( "style" ) . Should ( ) . Contain ( "width: 300px;" ) ;
265+ }
266+
267+ [ Fact ]
268+ public void Component_SetsButtonId_WhenProvided ( )
269+ {
270+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
271+ . Add ( p => p . Options , _testOptions )
272+ . Add ( p => p . Id , "test-multiselect" ) ) ;
273+
274+ var button = component . Find ( "button" ) ;
275+ button . Id . Should ( ) . Be ( "test-multiselect" ) ;
276+ }
277+
278+ [ Fact ]
279+ public void Component_ShowsStandaloneStyles_WhenStandaloneIsTrue ( )
280+ {
281+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
282+ . Add ( p => p . Options , _testOptions )
283+ . Add ( p => p . Standalone , true ) ) ;
284+
285+ var container = component . Find ( ".simple-dropdown" ) ;
286+ container . ClassList . Should ( ) . Contain ( "simple-bs-compat" ) ;
287+ }
288+
289+ [ Fact ]
290+ public void Component_CachesFilteredOptions_ForPerformance ( )
291+ {
292+ var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
293+ . Add ( p => p . Options , _testOptions )
294+ . Add ( p => p . CanFilter , true ) ) ;
295+
296+ var button = component . Find ( "button" ) ;
297+ button . Click ( ) ;
298+
299+ var filterInput = component . Find ( ".simple-filter-input" ) ;
300+ filterInput . Input ( "Appl" ) ;
301+
302+ // Trigger multiple renders without changing filter
303+ component . Render ( ) ;
304+ component . Render ( ) ;
305+
306+ var dropdownItems = component . FindAll ( ".dropdown-item" ) ;
307+ dropdownItems . Should ( ) . HaveCount ( 1 ) ;
308+ dropdownItems [ 0 ] . TextContent . Should ( ) . Contain ( "Apple" ) ;
309+ }
310+
311+ private record TestItem ( string Id , string Name ) ;
312+ }
0 commit comments