@@ -11,14 +11,14 @@ namespace SimpleBlazorMultiselect.Tests;
1111public class SimpleMultiselectTests : TestContext
1212{
1313 private readonly List < string > _testOptions = new ( ) { "Apple" , "Banana" , "Cherry" , "Date" , "Elderberry" } ;
14-
14+
1515 public SimpleMultiselectTests ( )
1616 {
1717 JSInterop . SetupModule ( "./_content/SimpleBlazorMultiselect/js/simpleMultiselect.js" )
1818 . SetupModule ( "register" , invocation => invocation . Arguments . Count == 2 )
1919 . SetupVoid ( "dispose" ) ;
2020 }
21-
21+
2222 [ Fact ]
2323 public void Component_RendersWithDefaultText_WhenNoOptionsSelected ( )
2424 {
@@ -67,7 +67,7 @@ public void Component_ShowsAllOptions_WhenDropdownIsOpen()
6767
6868 var dropdownItems = component . FindAll ( ".dropdown-item" ) ;
6969 dropdownItems . Should ( ) . HaveCount ( _testOptions . Count ) ;
70-
70+
7171 foreach ( var option in _testOptions )
7272 {
7373 component . Markup . Should ( ) . Contain ( option ) ;
@@ -81,17 +81,14 @@ public void Component_SelectsOption_WhenOptionClicked()
8181 var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
8282 . Add ( p => p . Options , _testOptions )
8383 . Add ( p => p . SelectedOptions , selectedOptions )
84- . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < string > > ( this , newSelection =>
85- {
86- selectedOptions = newSelection ;
87- } ) ) ) ;
84+ . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < string > > ( this , newSelection => { selectedOptions = newSelection ; } ) ) ) ;
8885
8986 var button = component . Find ( "button" ) ;
9087 button . Click ( ) ;
91-
88+
9289 var firstOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
9390 firstOption . Click ( ) ;
94-
91+
9592 firstOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
9693 var checkbox = firstOption . QuerySelector < IHtmlInputElement > ( "input[type='checkbox']" ) ;
9794 checkbox . Should ( ) . NotBeNull ( ) ;
@@ -106,19 +103,16 @@ public void Component_DeselectsOption_WhenSelectedOptionClicked()
106103 var component = RenderComponent < SimpleMultiselect < string > > ( parameters => parameters
107104 . Add ( p => p . Options , _testOptions )
108105 . Add ( p => p . SelectedOptions , selectedOptions )
109- . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < string > > ( this , newSelection =>
110- {
111- selectedOptions = newSelection ;
112- } ) ) ) ;
106+ . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < string > > ( this , newSelection => { selectedOptions = newSelection ; } ) ) ) ;
113107
114108 var button = component . Find ( "button" ) ;
115109 button . Click ( ) ;
116-
110+
117111 var firstOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
118112 firstOption . Click ( ) ;
119113
120114 selectedOptions . Should ( ) . NotContain ( "Apple" ) ;
121- foreach ( var option in component . FindAll ( ".dropdown-item" ) )
115+ foreach ( var option in component . FindAll ( ".dropdown-item" ) )
122116 {
123117 var cb = option . QuerySelector < IHtmlInputElement > ( "input[type='checkbox']" ) ;
124118 cb ! . IsChecked . Should ( ) . BeFalse ( ) ;
@@ -149,7 +143,7 @@ public void Component_FiltersOptions_WhenFilterTextEntered()
149143
150144 var button = component . Find ( "button" ) ;
151145 button . Click ( ) ;
152-
146+
153147 var filterInput = component . Find ( ".simple-filter-input" ) ;
154148 filterInput . Input ( "App" ) ;
155149
@@ -161,13 +155,13 @@ public void Component_FiltersOptions_WhenFilterTextEntered()
161155 [ Fact ]
162156 public void Component_UsesCustomStringSelector_WhenProvided ( )
163157 {
164- var complexOptions = new List < TestItem >
158+ var complexOptions = new List < TestValueItem >
165159 {
166160 new ( "1" , "Apple" ) ,
167161 new ( "2" , "Banana" )
168162 } ;
169163
170- var component = RenderComponent < SimpleMultiselect < TestItem > > ( parameters => parameters
164+ var component = RenderComponent < SimpleMultiselect < TestValueItem > > ( parameters => parameters
171165 . Add ( p => p . Options , complexOptions )
172166 . Add ( p => p . StringSelector , item => item . Name ) ) ;
173167
@@ -188,7 +182,7 @@ public void Component_UsesCustomFilterPredicate_WhenProvided()
188182
189183 var button = component . Find ( "button" ) ;
190184 button . Click ( ) ;
191-
185+
192186 var filterInput = component . Find ( ".simple-filter-input" ) ;
193187 filterInput . Input ( "B" ) ;
194188
@@ -218,22 +212,19 @@ public void Component_SingleSelectMode_SelectsOnlyOneOption()
218212 . Add ( p => p . Options , _testOptions )
219213 . Add ( p => p . SelectedOptions , selectedOptions )
220214 . Add ( p => p . IsMultiSelect , false )
221- . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < string > > ( this , newSelection =>
222- {
223- selectedOptions = newSelection ;
224- } ) ) ) ;
215+ . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < string > > ( this , newSelection => { selectedOptions = newSelection ; } ) ) ) ;
225216
226217 var button = component . Find ( "button" ) ;
227218 button . Click ( ) ;
228-
219+
229220 var firstOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
230221 firstOption . Click ( ) ;
231222 component . Render ( ) ;
232-
223+
233224 JSInterop . VerifyInvoke ( "dispose" ) ;
234225 component . Instance . IsDropdownOpen . Should ( ) . BeFalse ( ) ;
235-
236- button . Click ( ) ;
226+
227+ button . Click ( ) ;
237228 var secondOption = component . FindAll ( ".dropdown-item" ) [ 1 ] ;
238229 secondOption . Click ( ) ;
239230
@@ -295,10 +286,10 @@ public void Component_CachesFilteredOptions_ForPerformance()
295286
296287 var button = component . Find ( "button" ) ;
297288 button . Click ( ) ;
298-
289+
299290 var filterInput = component . Find ( ".simple-filter-input" ) ;
300291 filterInput . Input ( "Appl" ) ;
301-
292+
302293 // Trigger multiple renders without changing filter
303294 component . Render ( ) ;
304295 component . Render ( ) ;
@@ -307,6 +298,171 @@ public void Component_CachesFilteredOptions_ForPerformance()
307298 dropdownItems . Should ( ) . HaveCount ( 1 ) ;
308299 dropdownItems [ 0 ] . TextContent . Should ( ) . Contain ( "Apple" ) ;
309300 }
310-
311- private record TestItem ( string Id , string Name ) ;
301+
302+ [ Fact ]
303+ public void Component_CanDeselect_WhenPrefilledValueItems ( )
304+ {
305+ var options = new List < TestValueItem >
306+ {
307+ new ( "1" , "Apple" ) ,
308+ new ( "2" , "Banana" ) ,
309+ new ( "3" , "Cherry" )
310+ } ;
311+ var selectedItems = new HashSet < TestValueItem >
312+ {
313+ new ( "1" , "Apple" )
314+ } ;
315+
316+ var component = RenderComponent < SimpleMultiselect < TestValueItem > > ( parameters => parameters
317+ . Add ( p => p . Options , options )
318+ . Add ( p => p . SelectedOptions , selectedItems )
319+ . Add ( p => p . StringSelector , item => item . Name )
320+ . Add ( p => p . DefaultText , "Select fruits" )
321+ . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < TestValueItem > > ( this , newSelection => { selectedItems = newSelection ; } ) ) ) ;
322+
323+ var button = component . Find ( "button" ) ;
324+ button . TextContent . Should ( ) . Contain ( "Apple" ) ;
325+ button . Click ( ) ;
326+
327+ // Now only apple should be checked
328+ var appleOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
329+ var appleCheckbox = appleOption . QuerySelector < IHtmlInputElement > ( "input[type='checkbox']" ) ;
330+ appleCheckbox . Should ( ) . NotBeNull ( ) ;
331+ appleCheckbox . IsChecked . Should ( ) . BeTrue ( ) ;
332+
333+ appleOption . Click ( ) ;
334+
335+ // After clicking, apple should be deselected
336+ selectedItems . Should ( ) . BeEmpty ( ) ;
337+ button = component . Find ( "button" ) ;
338+ button . TextContent . Should ( ) . Be ( "Select fruits" ) ;
339+ }
340+
341+ [ Fact ]
342+ public void Component_CanDeselect_WhenPrefilledReferenceItems ( )
343+ {
344+ var options = new List < TestReferenceItem >
345+ {
346+ new ( "1" , "Apple" ) ,
347+ new ( "2" , "Banana" ) ,
348+ new ( "3" , "Cherry" )
349+ } ;
350+ var selectedItems = new HashSet < TestReferenceItem >
351+ {
352+ new ( "1" , "Apple" )
353+ } ;
354+
355+ var component = RenderComponent < SimpleMultiselect < TestReferenceItem > > ( parameters => parameters
356+ . Add ( p => p . Options , options )
357+ . Add ( p => p . SelectedOptions , selectedItems )
358+ . Add ( p => p . StringSelector , item => item . Name )
359+ . Add ( p => p . DefaultText , "Select fruits" )
360+ . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < TestReferenceItem > > ( this , newSelection => { selectedItems = newSelection ; } ) ) ) ;
361+
362+ var button = component . Find ( "button" ) ;
363+ button . TextContent . Should ( ) . Contain ( "Apple" ) ;
364+ button . Click ( ) ;
365+
366+ // Now only apple should be checked
367+ var appleOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
368+ var appleCheckbox = appleOption . QuerySelector < IHtmlInputElement > ( "input[type='checkbox']" ) ;
369+ appleCheckbox . Should ( ) . NotBeNull ( ) ;
370+ appleCheckbox . IsChecked . Should ( ) . BeTrue ( ) ;
371+
372+ appleOption . Click ( ) ;
373+
374+ // After clicking, apple should be deselected
375+ selectedItems . Should ( ) . BeEmpty ( ) ;
376+ button = component . Find ( "button" ) ;
377+ button . TextContent . Should ( ) . Be ( "Select fruits" ) ;
378+ }
379+
380+ [ Fact ]
381+ public void Component_CanDeselectValueItem_WhenMatchByReference ( )
382+ {
383+ var options = new List < TestValueItem >
384+ {
385+ new ( "1" , "Apple" ) ,
386+ new ( "2" , "Banana" ) ,
387+ new ( "3" , "Cherry" )
388+ } ;
389+ var selectedItems = new HashSet < TestValueItem >
390+ {
391+ new ( "1" , "Apple" )
392+ } ;
393+
394+ var component = RenderComponent < SimpleMultiselect < TestValueItem > > ( parameters => parameters
395+ . Add ( p => p . Options , options )
396+ . Add ( p => p . SelectedOptions , selectedItems )
397+ . Add ( p => p . StringSelector , item => item . Name )
398+ . Add ( p => p . DefaultText , "Select fruits" )
399+ . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < TestValueItem > > ( this , newSelection => { selectedItems = newSelection ; } ) )
400+ . Add ( p => p . MatchByReference , true ) ) ; // Should not matter for value types
401+
402+ var button = component . Find ( "button" ) ;
403+ button . TextContent . Should ( ) . Contain ( "Apple" ) ;
404+ button . Click ( ) ;
405+
406+ // Now only apple should be checked
407+ var appleOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
408+ var appleCheckbox = appleOption . QuerySelector < IHtmlInputElement > ( "input[type='checkbox']" ) ;
409+ appleCheckbox . Should ( ) . NotBeNull ( ) ;
410+ appleCheckbox . IsChecked . Should ( ) . BeTrue ( ) ;
411+
412+ appleOption . Click ( ) ;
413+
414+ // After clicking, apple should be deselected
415+ selectedItems . Should ( ) . BeEmpty ( ) ;
416+ button = component . Find ( "button" ) ;
417+ button . TextContent . Should ( ) . Be ( "Select fruits" ) ;
418+ }
419+
420+ [ Fact ]
421+ public void Component_CannotDeselectIdenticalInstance_WhenMatchByReference ( )
422+ {
423+ var options = new List < TestReferenceItem >
424+ {
425+ new ( "1" , "Apple" ) ,
426+ new ( "2" , "Banana" ) ,
427+ new ( "3" , "Cherry" )
428+ } ;
429+ var selectedItems = new HashSet < TestReferenceItem >
430+ {
431+ new ( "1" , "Apple" )
432+ } ;
433+
434+ var component = RenderComponent < SimpleMultiselect < TestReferenceItem > > ( parameters => parameters
435+ . Add ( p => p . Options , options )
436+ . Add ( p => p . SelectedOptions , selectedItems )
437+ . Add ( p => p . StringSelector , item => item . Name )
438+ . Add ( p => p . DefaultText , "Select fruits" )
439+ . Add ( p => p . SelectedOptionsChanged , EventCallback . Factory . Create < HashSet < TestReferenceItem > > ( this , newSelection => { selectedItems = newSelection ; } ) )
440+ . Add ( p => p . MatchByReference , true ) ) ; // This will break the deselection
441+
442+ var button = component . Find ( "button" ) ;
443+ button . TextContent . Should ( ) . Contain ( "Apple" ) ;
444+ button . Click ( ) ;
445+
446+ // Apple should not be checked because the instance is different
447+ // So clicking it will add another apple instead of removing the existing one
448+ var appleOption = component . FindAll ( ".dropdown-item" ) [ 0 ] ;
449+ var appleCheckbox = appleOption . QuerySelector < IHtmlInputElement > ( "input[type='checkbox']" ) ;
450+ appleCheckbox . Should ( ) . NotBeNull ( ) ;
451+ appleCheckbox . IsChecked . Should ( ) . BeFalse ( ) ;
452+
453+ appleOption . Click ( ) ;
454+
455+ // After clicking, we should have two apples
456+ selectedItems . Should ( ) . HaveCount ( 2 ) ;
457+ button = component . Find ( "button" ) ;
458+ button . TextContent . Should ( ) . Be ( "Apple, Apple" ) ;
459+ }
460+
461+ private record TestValueItem ( string Id , string Name ) ;
462+
463+ private class TestReferenceItem ( string id , string name )
464+ {
465+ public string Id { get ; set ; } = id ;
466+ public string Name { get ; set ; } = name ;
467+ }
312468}
0 commit comments