1- import { act , fireEvent , render , screen } from "@testing-library/react" ;
1+ import { fireEvent , render , screen } from "@testing-library/react" ;
22import { describe , expect , it , vi } from "vitest" ;
3- import { useMessageEditorStore } from "../stores/messageEditorStore" ;
4- import { setupSuggestionTests } from "../test/helpers" ;
5- import type { SuggestionItem , SuggestionType } from "../types" ;
3+ import { useSuggestionStore } from "../stores/suggestionStore" ;
4+ import { ARIA , CSS , EMPTY_MESSAGES , LOADING_TEXT } from "../test/constants" ;
5+ import { SUGGESTION_ITEMS } from "../test/fixtures" ;
6+ import {
7+ enableMouseInteraction ,
8+ getListbox ,
9+ getOptions ,
10+ openSuggestion ,
11+ setupSuggestionTests ,
12+ } from "../test/helpers" ;
613import { SuggestionList } from "./SuggestionList" ;
714
8- const SESSION_ID = "session-1" ;
9- const SELECTED_CLASS = "suggestion-item-selected" ;
10-
11- const ARIA_LABELS = {
12- file : "File suggestions" ,
13- command : "Available commands" ,
14- } as const ;
15-
16- const EMPTY_MESSAGES = {
17- file : "No files found" ,
18- command : "No commands available" ,
19- } as const ;
20-
21- const MOCK_ITEMS : SuggestionItem [ ] = [
22- { id : "1" , label : "file1.ts" , description : "src/file1.ts" } ,
23- { id : "2" , label : "file2.ts" , description : "src/file2.ts" } ,
24- { id : "3" , label : "file3.ts" } ,
25- ] ;
26-
27- const getActions = ( ) => useMessageEditorStore . getState ( ) . actions ;
28- const getSelectedIndex = ( ) =>
29- useMessageEditorStore . getState ( ) . suggestion . selectedIndex ;
30-
31- const getListbox = ( ) => screen . getByRole ( "listbox" ) ;
32- const getOptions = ( ) => screen . getAllByRole ( "option" ) ;
33-
34- interface SuggestionSetup {
35- items ?: SuggestionItem [ ] ;
36- selectedIndex ?: number ;
37- type ?: SuggestionType ;
38- loadingState ?: "idle" | "loading" | "success" | "error" ;
39- error ?: string ;
40- onSelectItem ?: ( item : SuggestionItem ) => void ;
41- }
42-
43- function renderSuggestionList ( overrides : SuggestionSetup = { } ) {
44- act ( ( ) => {
45- const actions = getActions ( ) ;
46- actions . openSuggestion ( SESSION_ID , overrides . type ?? "file" , {
47- x : 0 ,
48- y : 0 ,
49- } ) ;
50- actions . setSuggestionItems ( overrides . items ?? MOCK_ITEMS ) ;
51- if ( overrides . selectedIndex !== undefined ) {
52- actions . setSelectedIndex ( overrides . selectedIndex ) ;
53- }
54- actions . setSuggestionLoadingState (
55- overrides . loadingState ?? "success" ,
56- overrides . error ,
57- ) ;
58- if ( overrides . onSelectItem ) {
59- actions . setOnSelectItem ( overrides . onSelectItem ) ;
60- }
61- } ) ;
62- return render ( < SuggestionList /> ) ;
63- }
64-
65- function enableMouseInteraction ( ) {
66- fireEvent . mouseMove ( getListbox ( ) ) ;
67- }
15+ const getSelectedIndex = ( ) => useSuggestionStore . getState ( ) . selectedIndex ;
6816
6917describe ( "SuggestionList" , ( ) => {
7018 setupSuggestionTests ( ) ;
7119
7220 describe ( "rendering items" , ( ) => {
7321 it ( "renders all item labels and descriptions" , ( ) => {
74- renderSuggestionList ( ) ;
75-
76- expect ( screen . getByText ( "file1.ts" ) ) . toBeInTheDocument ( ) ;
77- expect ( screen . getByText ( "file2.ts" ) ) . toBeInTheDocument ( ) ;
78- expect ( screen . getByText ( "file3.ts" ) ) . toBeInTheDocument ( ) ;
79- expect ( screen . getByText ( "src/file1.ts" ) ) . toBeInTheDocument ( ) ;
80- expect ( screen . getByText ( "src/file2.ts" ) ) . toBeInTheDocument ( ) ;
22+ openSuggestion ( ) ;
23+ render ( < SuggestionList /> ) ;
24+
25+ for ( const item of SUGGESTION_ITEMS ) {
26+ expect ( screen . getByText ( item . label ) ) . toBeInTheDocument ( ) ;
27+ if ( item . description ) {
28+ expect ( screen . getByText ( item . description ) ) . toBeInTheDocument ( ) ;
29+ }
30+ }
8131 } ) ;
8232
8333 it ( "renders keyboard hints footer" , ( ) => {
84- renderSuggestionList ( ) ;
34+ openSuggestion ( ) ;
35+ render ( < SuggestionList /> ) ;
8536
8637 expect ( screen . getByText ( / n a v i g a t e / ) ) . toBeInTheDocument ( ) ;
8738 expect ( screen . getByText ( / s e l e c t / ) ) . toBeInTheDocument ( ) ;
@@ -91,14 +42,15 @@ describe("SuggestionList", () => {
9142
9243 describe ( "selected item highlighting" , ( ) => {
9344 it ( "applies selected class and aria-selected to correct item" , ( ) => {
94- renderSuggestionList ( { selectedIndex : 1 } ) ;
45+ openSuggestion ( { selectedIndex : 1 } ) ;
46+ render ( < SuggestionList /> ) ;
9547
9648 const options = getOptions ( ) ;
97- expect ( options [ 0 ] ) . not . toHaveClass ( SELECTED_CLASS ) ;
49+ expect ( options [ 0 ] ) . not . toHaveClass ( CSS . SELECTED ) ;
9850 expect ( options [ 0 ] ) . toHaveAttribute ( "aria-selected" , "false" ) ;
99- expect ( options [ 1 ] ) . toHaveClass ( SELECTED_CLASS ) ;
51+ expect ( options [ 1 ] ) . toHaveClass ( CSS . SELECTED ) ;
10052 expect ( options [ 1 ] ) . toHaveAttribute ( "aria-selected" , "true" ) ;
101- expect ( options [ 2 ] ) . not . toHaveClass ( SELECTED_CLASS ) ;
53+ expect ( options [ 2 ] ) . not . toHaveClass ( CSS . SELECTED ) ;
10254 expect ( options [ 2 ] ) . toHaveAttribute ( "aria-selected" , "false" ) ;
10355 } ) ;
10456 } ) ;
@@ -107,7 +59,8 @@ describe("SuggestionList", () => {
10759 it . each ( [ "file" , "command" ] as const ) (
10860 "shows correct empty message for %s type" ,
10961 ( type ) => {
110- renderSuggestionList ( { items : [ ] , type, loadingState : "idle" } ) ;
62+ openSuggestion ( { type, items : [ ] , loadingState : "idle" } ) ;
63+ render ( < SuggestionList /> ) ;
11164
11265 expect ( screen . getByText ( EMPTY_MESSAGES [ type ] ) ) . toBeInTheDocument ( ) ;
11366 } ,
@@ -116,39 +69,47 @@ describe("SuggestionList", () => {
11669
11770 describe ( "loading and error states" , ( ) => {
11871 it ( "shows loading indicator and hides items" , ( ) => {
119- renderSuggestionList ( { loadingState : "loading" } ) ;
120-
121- expect ( screen . getByText ( "Searching..." ) ) . toBeInTheDocument ( ) ;
122- expect ( screen . getByLabelText ( "Loading suggestions" ) ) . toBeInTheDocument ( ) ;
123- expect ( screen . queryByText ( "file1.ts" ) ) . not . toBeInTheDocument ( ) ;
72+ openSuggestion ( { loadingState : "loading" } ) ;
73+ render ( < SuggestionList /> ) ;
74+
75+ expect ( screen . getByText ( LOADING_TEXT ) ) . toBeInTheDocument ( ) ;
76+ expect ( screen . getByLabelText ( ARIA . LOADING ) ) . toBeInTheDocument ( ) ;
77+ expect (
78+ screen . queryByText ( SUGGESTION_ITEMS [ 0 ] . label ) ,
79+ ) . not . toBeInTheDocument ( ) ;
12480 } ) ;
12581
12682 it ( "shows error message and hides items" , ( ) => {
12783 const errorMessage = "Failed to load files" ;
128- renderSuggestionList ( { loadingState : "error" , error : errorMessage } ) ;
84+ openSuggestion ( { loadingState : "error" , error : errorMessage } ) ;
85+ render ( < SuggestionList /> ) ;
12986
13087 expect ( screen . getByText ( errorMessage ) ) . toBeInTheDocument ( ) ;
13188 expect ( screen . getByRole ( "alert" ) ) . toHaveAttribute (
13289 "aria-label" ,
133- "Error loading suggestions" ,
90+ ARIA . ERROR ,
13491 ) ;
135- expect ( screen . queryByText ( "file1.ts" ) ) . not . toBeInTheDocument ( ) ;
92+ expect (
93+ screen . queryByText ( SUGGESTION_ITEMS [ 0 ] . label ) ,
94+ ) . not . toBeInTheDocument ( ) ;
13695 } ) ;
13796 } ) ;
13897
13998 describe ( "mouse interactions" , ( ) => {
14099 it ( "calls onSelectItem when clicking an item" , ( ) => {
141100 const onSelectItem = vi . fn ( ) ;
142- renderSuggestionList ( { onSelectItem } ) ;
101+ openSuggestion ( { onSelectItem } ) ;
102+ render ( < SuggestionList /> ) ;
143103
144104 enableMouseInteraction ( ) ;
145- fireEvent . click ( screen . getByText ( "file2.ts" ) ) ;
105+ fireEvent . click ( screen . getByText ( SUGGESTION_ITEMS [ 1 ] . label ) ) ;
146106
147- expect ( onSelectItem ) . toHaveBeenCalledWith ( MOCK_ITEMS [ 1 ] ) ;
107+ expect ( onSelectItem ) . toHaveBeenCalledWith ( 1 ) ;
148108 } ) ;
149109
150110 it ( "updates selectedIndex on hover after mouse movement" , ( ) => {
151- renderSuggestionList ( ) ;
111+ openSuggestion ( ) ;
112+ render ( < SuggestionList /> ) ;
152113
153114 enableMouseInteraction ( ) ;
154115 fireEvent . mouseEnter ( getOptions ( ) [ 1 ] ) ;
@@ -157,7 +118,8 @@ describe("SuggestionList", () => {
157118 } ) ;
158119
159120 it ( "ignores hover before any mouse movement" , ( ) => {
160- renderSuggestionList ( ) ;
121+ openSuggestion ( ) ;
122+ render ( < SuggestionList /> ) ;
161123
162124 fireEvent . mouseEnter ( getOptions ( ) [ 1 ] ) ;
163125
@@ -167,27 +129,34 @@ describe("SuggestionList", () => {
167129
168130 describe ( "accessibility" , ( ) => {
169131 it ( "has listbox role with option children" , ( ) => {
170- renderSuggestionList ( ) ;
132+ openSuggestion ( ) ;
133+ render ( < SuggestionList /> ) ;
171134
172135 expect ( getListbox ( ) ) . toBeInTheDocument ( ) ;
173- expect ( getOptions ( ) ) . toHaveLength ( 3 ) ;
136+ expect ( getOptions ( ) ) . toHaveLength ( SUGGESTION_ITEMS . length ) ;
174137 } ) ;
175138
176139 it . each ( [ "file" , "command" ] as const ) (
177140 "sets correct aria-label for %s type" ,
178141 ( type ) => {
179- renderSuggestionList ( { type } ) ;
180-
181- expect ( getListbox ( ) ) . toHaveAttribute ( "aria-label" , ARIA_LABELS [ type ] ) ;
142+ const ariaLabels = {
143+ file : ARIA . FILE_SUGGESTIONS ,
144+ command : ARIA . COMMAND_SUGGESTIONS ,
145+ } ;
146+ openSuggestion ( { type } ) ;
147+ render ( < SuggestionList /> ) ;
148+
149+ expect ( getListbox ( ) ) . toHaveAttribute ( "aria-label" , ariaLabels [ type ] ) ;
182150 } ,
183151 ) ;
184152
185153 it ( "sets aria-activedescendant to selected item id" , ( ) => {
186- renderSuggestionList ( { selectedIndex : 1 } ) ;
154+ openSuggestion ( { selectedIndex : 1 } ) ;
155+ render ( < SuggestionList /> ) ;
187156
188157 expect ( getListbox ( ) ) . toHaveAttribute (
189158 "aria-activedescendant" ,
190- " suggestion-2" ,
159+ ` suggestion-${ SUGGESTION_ITEMS [ 1 ] . id } ` ,
191160 ) ;
192161 } ) ;
193162 } ) ;
0 commit comments