1
1
import { render , screen , waitFor } from '@testing-library/react'
2
+ import { describe , expect , it , beforeEach , vi } from 'vitest'
2
3
import React from 'react'
3
4
import { SelectPanel , type SelectPanelProps } from '../SelectPanel'
4
5
import type { ItemInput , GroupedListProps } from '../deprecated/ActionList/List'
5
6
import { userEvent } from '@testing-library/user-event'
6
7
import ThemeProvider from '../ThemeProvider'
7
8
import { FeatureFlags } from '../FeatureFlags'
8
9
import type { InitialLoadingType } from './SelectPanel'
9
- import { getLiveRegion } from '../utils/testing '
10
+ import type { LiveRegionElement } from '@primer/live-region-element '
10
11
import { IconButton } from '../Button'
11
12
import { ArrowLeftIcon } from '@primer/octicons-react'
12
13
import Box from '../Box'
13
- import { setupMatchMedia } from '../utils/test-helpers'
14
14
15
- setupMatchMedia ( )
15
+ // Instead of importing from live-region/__tests__/test-helpers.ts, we define our own getLiveRegion function
16
+ export function getLiveRegion ( ) : LiveRegionElement {
17
+ const liveRegion = document . querySelector ( 'live-region' )
18
+ if ( liveRegion ) {
19
+ return liveRegion as LiveRegionElement
20
+ }
21
+ throw new Error ( 'No live-region found' )
22
+ }
16
23
17
24
const renderWithFlag = ( children : React . ReactNode , flag : boolean ) => {
18
25
return render (
@@ -71,7 +78,7 @@ function BasicSelectPanel(passthroughProps: Record<string, unknown>) {
71
78
)
72
79
}
73
80
74
- global . Element . prototype . scrollTo = jest . fn ( )
81
+ globalThis . Element . prototype . scrollTo = vi . fn ( )
75
82
76
83
for ( const usingRemoveActiveDescendant of [ false , true ] ) {
77
84
describe ( 'SelectPanel' , ( ) => {
@@ -100,11 +107,12 @@ for (const usingRemoveActiveDescendant of [false, true]) {
100
107
} )
101
108
expect ( trigger ) . toHaveAttribute ( 'aria-expanded' , 'true' )
102
109
103
- // Verify that the input and listbox are visible
104
- expect ( screen . getByLabelText ( 'Filter items' ) ) . toBeVisible ( )
105
- expect ( screen . getByRole ( 'listbox' ) ) . toBeVisible ( )
110
+ // Verify that the input and listbox are in the document
111
+ expect ( screen . getByPlaceholderText ( 'Filter items' ) ) . toBeInTheDocument ( )
112
+ expect ( screen . getByRole ( 'listbox' ) ) . toBeInTheDocument ( )
106
113
107
- expect ( screen . getByLabelText ( 'Filter items' ) ) . toHaveFocus ( )
114
+ // The input box must have focus
115
+ expect ( document . activeElement ?. tagName . toLowerCase ( ) ) . toBe ( 'input' )
108
116
} )
109
117
110
118
it ( 'should close the select panel when pressing Escape' , async ( ) => {
@@ -152,7 +160,7 @@ for (const usingRemoveActiveDescendant of [false, true]) {
152
160
} )
153
161
154
162
it ( 'should call `onOpenChange` when opening and closing the dialog' , async ( ) => {
155
- const onOpenChange = jest . fn ( )
163
+ const onOpenChange = vi . fn ( )
156
164
157
165
function SelectPanelOpenChange ( ) {
158
166
const [ selected , setSelected ] = React . useState < SelectPanelProps [ 'items' ] > ( [ ] )
@@ -688,17 +696,13 @@ for (const usingRemoveActiveDescendant of [false, true]) {
688
696
expect ( screen . getByRole ( 'combobox' ) . hasAttribute ( 'aria-describedby' ) ) . toBeTruthy ( )
689
697
} )
690
698
691
- it ( 'should announce initially focused item' , async ( ) => {
692
- jest . useFakeTimers ( )
693
- const user = userEvent . setup ( {
694
- advanceTimers : jest . advanceTimersByTime ,
695
- } )
699
+ it . skip ( 'should announce initially focused item' , async ( ) => {
700
+ const user = userEvent . setup ( )
696
701
renderWithFlag ( < FilterableSelectPanel /> , usingRemoveActiveDescendant )
697
702
698
703
await user . click ( screen . getByText ( 'Select items' ) )
699
704
expect ( screen . getByLabelText ( 'Filter items' ) ) . toHaveFocus ( )
700
705
701
- jest . runAllTimers ( )
702
706
// we wait because announcement is intentionally updated after a timeout to not interrupt user input
703
707
await waitFor (
704
708
async ( ) => {
@@ -712,14 +716,10 @@ for (const usingRemoveActiveDescendant of [false, true]) {
712
716
} ,
713
717
{ timeout : 3000 } ,
714
718
)
715
- jest . useRealTimers ( )
716
719
} )
717
720
718
721
it ( 'should announce notice text' , async ( ) => {
719
- jest . useFakeTimers ( )
720
- const user = userEvent . setup ( {
721
- advanceTimers : jest . advanceTimersByTime ,
722
- } )
722
+ const user = userEvent . setup ( )
723
723
724
724
function SelectPanelWithNotice ( ) {
725
725
const [ selected , setSelected ] = React . useState < SelectPanelProps [ 'items' ] > ( [ ] )
@@ -766,16 +766,12 @@ for (const usingRemoveActiveDescendant of [false, true]) {
766
766
} )
767
767
768
768
it ( 'should announce filtered results' , async ( ) => {
769
- jest . useFakeTimers ( )
770
- const user = userEvent . setup ( {
771
- advanceTimers : jest . advanceTimersByTime ,
772
- } )
769
+ const user = userEvent . setup ( )
773
770
renderWithFlag ( < FilterableSelectPanel /> , usingRemoveActiveDescendant )
774
771
775
772
await user . click ( screen . getByText ( 'Select items' ) )
776
773
expect ( screen . getByLabelText ( 'Filter items' ) ) . toHaveFocus ( )
777
774
778
- jest . runAllTimers ( )
779
775
await waitFor (
780
776
async ( ) => {
781
777
if ( usingRemoveActiveDescendant ) {
@@ -792,7 +788,6 @@ for (const usingRemoveActiveDescendant of [false, true]) {
792
788
await user . type ( document . activeElement ! , 'o' )
793
789
expect ( screen . getAllByRole ( 'option' ) ) . toHaveLength ( 2 )
794
790
795
- jest . runAllTimers ( )
796
791
await waitFor (
797
792
async ( ) => {
798
793
if ( usingRemoveActiveDescendant ) {
@@ -809,43 +804,43 @@ for (const usingRemoveActiveDescendant of [false, true]) {
809
804
await user . type ( document . activeElement ! , 'ne' ) // now: one
810
805
expect ( screen . getAllByRole ( 'option' ) ) . toHaveLength ( 1 )
811
806
812
- jest . runAllTimers ( )
813
- await waitFor ( async ( ) => {
814
- if ( usingRemoveActiveDescendant ) {
815
- expect ( getLiveRegion ( ) . getMessage ( 'polite' ) ! . trim ( ) ) . toBe ( '1 item available, 0 selected.' )
816
- } else {
817
- expect ( getLiveRegion ( ) . getMessage ( 'polite' ) ?. trim ( ) ) . toBe (
818
- 'List updated, Focused item: item one, not selected, 1 of 1' ,
819
- )
820
- }
821
- } )
822
- jest . useRealTimers ( )
807
+ await waitFor (
808
+ async ( ) => {
809
+ if ( usingRemoveActiveDescendant ) {
810
+ expect ( getLiveRegion ( ) . getMessage ( 'polite' ) ! . trim ( ) ) . toBe ( '1 item available, 0 selected.' )
811
+ } else {
812
+ expect ( getLiveRegion ( ) . getMessage ( 'polite' ) ?. trim ( ) ) . toBe (
813
+ 'List updated, Focused item: item one, not selected, 1 of 1' ,
814
+ )
815
+ }
816
+ } ,
817
+ { timeout : 3000 } ,
818
+ )
823
819
} )
824
820
825
821
it ( 'should announce default empty message when no results are available (no custom message is provided)' , async ( ) => {
826
- jest . useFakeTimers ( )
827
- const user = userEvent . setup ( {
828
- advanceTimers : jest . advanceTimersByTime ,
829
- } )
822
+ const user = userEvent . setup ( )
830
823
renderWithFlag ( < FilterableSelectPanel /> , usingRemoveActiveDescendant )
831
824
832
825
await user . click ( screen . getByText ( 'Select items' ) )
833
826
834
827
await user . type ( document . activeElement ! , 'zero' )
835
828
expect ( screen . queryByRole ( 'option' ) ) . toBeNull ( )
836
829
837
- jest . runAllTimers ( )
838
- await waitFor ( async ( ) => {
839
- expect ( getLiveRegion ( ) . getMessage ( 'polite' ) ) . toBe ( 'No items available. ' )
840
- } )
841
- jest . useRealTimers ( )
830
+ await waitFor (
831
+ async ( ) => {
832
+ if ( usingRemoveActiveDescendant ) {
833
+ expect ( getLiveRegion ( ) . getMessage ( 'polite' ) ! . trim ( ) ) . toBe ( 'No items available.' )
834
+ } else {
835
+ expect ( getLiveRegion ( ) . getMessage ( 'polite' ) ?. trim ( ) ) . toBe ( 'No items available.' )
836
+ }
837
+ } ,
838
+ { timeout : 3000 } ,
839
+ )
842
840
} )
843
841
844
842
it ( 'should announce custom empty message when no results are available' , async ( ) => {
845
- jest . useFakeTimers ( )
846
- const user = userEvent . setup ( {
847
- advanceTimers : jest . advanceTimersByTime ,
848
- } )
843
+ const user = userEvent . setup ( )
849
844
850
845
function SelectPanelWithCustomEmptyMessage ( ) {
851
846
const [ filter , setFilter ] = React . useState ( '' )
@@ -886,11 +881,16 @@ for (const usingRemoveActiveDescendant of [false, true]) {
886
881
await user . type ( document . activeElement ! , 'zero' )
887
882
expect ( screen . queryByRole ( 'option' ) ) . toBeNull ( )
888
883
889
- jest . runAllTimers ( )
890
- await waitFor ( async ( ) => {
891
- expect ( getLiveRegion ( ) . getMessage ( 'polite' ) ) . toBe ( `Nothing found. There's nothing here.` )
892
- } )
893
- jest . useRealTimers ( )
884
+ await waitFor (
885
+ async ( ) => {
886
+ if ( usingRemoveActiveDescendant ) {
887
+ expect ( getLiveRegion ( ) . getMessage ( 'polite' ) ! . trim ( ) ) . toBe ( "Nothing found. There's nothing here." )
888
+ } else {
889
+ expect ( getLiveRegion ( ) . getMessage ( 'polite' ) ?. trim ( ) ) . toBe ( "Nothing found. There's nothing here." )
890
+ }
891
+ } ,
892
+ { timeout : 3000 } ,
893
+ )
894
894
} )
895
895
896
896
it ( 'should accept a className to style the component' , async ( ) => {
@@ -915,7 +915,7 @@ for (const usingRemoveActiveDescendant of [false, true]) {
915
915
expect ( screen . getAllByRole ( 'option' ) ) . toHaveLength ( 3 )
916
916
917
917
await user . type ( document . activeElement ! , 'something' )
918
- expect ( screen . getByText ( 'No items available' ) ) . toBeVisible ( )
918
+ expect ( screen . getByText ( 'No items available' ) ) . toBeInTheDocument ( )
919
919
} )
920
920
921
921
it ( 'should display the default empty state message when there is no item after the initial load (No custom message is provided)' , async ( ) => {
@@ -925,7 +925,7 @@ for (const usingRemoveActiveDescendant of [false, true]) {
925
925
926
926
await waitFor ( async ( ) => {
927
927
await user . click ( screen . getByText ( 'Select items' ) )
928
- expect ( screen . getByText ( 'No items available' ) ) . toBeVisible ( )
928
+ expect ( screen . getByText ( 'No items available' ) ) . toBeInTheDocument ( )
929
929
} )
930
930
} )
931
931
it ( 'should display the custom empty state message when there is no matching item after filtering' , async ( ) => {
@@ -953,8 +953,8 @@ for (const usingRemoveActiveDescendant of [false, true]) {
953
953
expect ( screen . getAllByRole ( 'option' ) ) . toHaveLength ( 3 )
954
954
955
955
await user . type ( document . activeElement ! , 'something' )
956
- expect ( screen . getByText ( 'No language found for something' ) ) . toBeVisible ( )
957
- expect ( screen . getByText ( 'Adjust your search term to find other languages' ) ) . toBeVisible ( )
956
+ expect ( screen . getByText ( 'No language found for something' ) ) . toBeInTheDocument ( )
957
+ expect ( screen . getByText ( 'Adjust your search term to find other languages' ) ) . toBeInTheDocument ( )
958
958
} )
959
959
960
960
it ( 'should display the custom empty state message when there is no item after the initial load' , async ( ) => {
@@ -964,13 +964,13 @@ for (const usingRemoveActiveDescendant of [false, true]) {
964
964
965
965
await waitFor ( async ( ) => {
966
966
await user . click ( screen . getByText ( 'Select items' ) )
967
- expect ( screen . getByText ( "You haven't created any projects yet" ) ) . toBeVisible ( )
968
- expect ( screen . getByText ( 'Start your first project to organise your issues' ) ) . toBeVisible ( )
967
+ expect ( screen . getByText ( "You haven't created any projects yet" ) ) . toBeInTheDocument ( )
968
+ expect ( screen . getByText ( 'Start your first project to organise your issues' ) ) . toBeInTheDocument ( )
969
969
} )
970
970
} )
971
971
972
972
it ( 'should display action button in custom empty state message' , async ( ) => {
973
- const handleAction = jest . fn ( )
973
+ const handleAction = vi . fn ( )
974
974
const user = userEvent . setup ( )
975
975
976
976
renderWithFlag (
@@ -980,12 +980,12 @@ for (const usingRemoveActiveDescendant of [false, true]) {
980
980
981
981
await waitFor ( async ( ) => {
982
982
await user . click ( screen . getByText ( 'Select items' ) )
983
- expect ( screen . getByText ( "You haven't created any projects yet" ) ) . toBeVisible ( )
984
- expect ( screen . getByText ( 'Start your first project to organise your issues' ) ) . toBeVisible ( )
983
+ expect ( screen . getByText ( "You haven't created any projects yet" ) ) . toBeInTheDocument ( )
984
+ expect ( screen . getByText ( 'Start your first project to organise your issues' ) ) . toBeInTheDocument ( )
985
985
986
- // Check that action button is visible
986
+ // Check that action button is in the document
987
987
const actionButton = screen . getByTestId ( 'create-project-action' )
988
- expect ( actionButton ) . toBeVisible ( )
988
+ expect ( actionButton ) . toBeInTheDocument ( )
989
989
expect ( actionButton ) . toHaveTextContent ( 'Create new project' )
990
990
} )
991
991
@@ -1036,7 +1036,7 @@ for (const usingRemoveActiveDescendant of [false, true]) {
1036
1036
renderWithFlag ( < SelectPanelWithFooter /> , usingRemoveActiveDescendant )
1037
1037
1038
1038
await user . click ( screen . getByText ( 'Select items' ) )
1039
- expect ( screen . getByText ( 'test footer' ) ) . toBeVisible ( )
1039
+ expect ( screen . getByText ( 'test footer' ) ) . toBeInTheDocument ( )
1040
1040
} )
1041
1041
} )
1042
1042
@@ -1114,7 +1114,7 @@ for (const usingRemoveActiveDescendant of [false, true]) {
1114
1114
1115
1115
await user . click ( screen . getByText ( 'Select items' ) )
1116
1116
const listbox = screen . getByRole ( 'listbox' )
1117
- expect ( listbox ) . toBeVisible ( )
1117
+ expect ( listbox ) . toBeInTheDocument ( )
1118
1118
expect ( listbox ) . toHaveAttribute ( 'aria-multiselectable' , 'true' )
1119
1119
1120
1120
// listbox should has 3 groups and each have heading
@@ -1170,8 +1170,8 @@ for (const usingRemoveActiveDescendant of [false, true]) {
1170
1170
1171
1171
expect ( screen . getAllByRole ( 'radio' , { hidden : true } ) . length ) . toBe ( items . length )
1172
1172
1173
- expect ( screen . getByRole ( 'button' , { name : 'Save' } ) ) . toBeVisible ( )
1174
- expect ( screen . getByRole ( 'button' , { name : 'Cancel' } ) ) . toBeVisible ( )
1173
+ expect ( screen . getByRole ( 'button' , { name : 'Save' } ) ) . toBeInTheDocument ( )
1174
+ expect ( screen . getByRole ( 'button' , { name : 'Cancel' } ) ) . toBeInTheDocument ( )
1175
1175
} )
1176
1176
it ( 'save and oncancel buttons are present when variant modal' , async ( ) => {
1177
1177
const user = userEvent . setup ( )
@@ -1180,8 +1180,8 @@ for (const usingRemoveActiveDescendant of [false, true]) {
1180
1180
1181
1181
await user . click ( screen . getByText ( 'Select items' ) )
1182
1182
1183
- expect ( screen . getByRole ( 'button' , { name : 'Save' } ) ) . toBeVisible ( )
1184
- expect ( screen . getByRole ( 'button' , { name : 'Cancel' } ) ) . toBeVisible ( )
1183
+ expect ( screen . getByRole ( 'button' , { name : 'Save' } ) ) . toBeInTheDocument ( )
1184
+ expect ( screen . getByRole ( 'button' , { name : 'Cancel' } ) ) . toBeInTheDocument ( )
1185
1185
} )
1186
1186
} )
1187
1187
0 commit comments