11import React from "react"
22import { render , screen , fireEvent } from "@testing-library/react"
3- import { describe , it , expect , vi , beforeEach } from "vitest"
3+ import { describe , it , expect , vi } from "vitest"
44import { CommandPatternSelector } from "../CommandPatternSelector"
55import { CommandPattern } from "../../../utils/commandPatterns"
6+ import { TooltipProvider } from "../../../components/ui/tooltip"
67
78// Mock react-i18next
89vi . mock ( "react-i18next" , ( ) => ( {
910 useTranslation : ( ) => ( {
1011 t : ( key : string ) => key ,
1112 } ) ,
12- Trans : ( { i18nKey, components } : any ) => {
13- if ( i18nKey === "chat:commandExecution.commandManagementDescription" ) {
14- return (
15- < span >
16- Manage command permissions: Click ✓ to allow auto-execution, ✗ to deny execution. Patterns can be
17- toggled on/off or removed from lists. { components . settingsLink }
18- </ span >
19- )
20- }
21- return < span > { i18nKey } </ span >
22- } ,
13+ Trans : ( { i18nKey, children } : any ) => < span > { i18nKey || children } </ span > ,
2314} ) )
2415
2516// Mock VSCodeLink
2617vi . mock ( "@vscode/webview-ui-toolkit/react" , ( ) => ( {
2718 VSCodeLink : ( { children, onClick } : any ) => (
2819 < a href = "#" onClick = { onClick } >
29- { children || "View all settings" }
30- </ a >
31- ) ,
32- } ) )
33-
34- // Mock StandardTooltip
35- vi . mock ( "../../ui/standard-tooltip" , ( ) => ( {
36- StandardTooltip : ( { children, content } : any ) => (
37- < div title = { typeof content === "string" ? content : "tooltip" } >
3820 { children }
39- { /* Render the content to make it testable */ }
40- < div style = { { display : "none" } } > { content } </ div >
41- </ div >
21+ </ a >
4222 ) ,
4323} ) )
4424
45- // Mock window.postMessage
46- const mockPostMessage = vi . fn ( )
47- window . postMessage = mockPostMessage
25+ // Wrapper component with TooltipProvider
26+ const TestWrapper = ( { children } : { children : React . ReactNode } ) => < TooltipProvider > { children } </ TooltipProvider >
4827
4928describe ( "CommandPatternSelector" , ( ) => {
5029 const mockPatterns : CommandPattern [ ] = [
@@ -61,192 +40,53 @@ describe("CommandPatternSelector", () => {
6140 onDenyPatternChange : vi . fn ( ) ,
6241 }
6342
64- beforeEach ( ( ) => {
65- vi . clearAllMocks ( )
66- } )
67-
68- it ( "should render collapsed by default" , ( ) => {
69- render ( < CommandPatternSelector { ...defaultProps } /> )
70-
71- expect ( screen . getByText ( "chat:commandExecution.manageCommands" ) ) . toBeInTheDocument ( )
72- expect ( screen . queryByText ( "npm commands" ) ) . not . toBeInTheDocument ( )
73- } )
43+ it ( "should render with unique pattern keys" , ( ) => {
44+ const { container } = render (
45+ < TestWrapper >
46+ < CommandPatternSelector { ...defaultProps } />
47+ </ TestWrapper > ,
48+ )
7449
75- it ( "should expand when clicked" , ( ) => {
76- render ( < CommandPatternSelector { ... defaultProps } /> )
50+ // The component should render without errors
51+ expect ( container ) . toBeTruthy ( )
7752
78- const expandButton = screen . getByRole ( "button" , { name : "chat:commandExecution.expandManagement" } )
53+ // Click to expand the component
54+ const expandButton = screen . getByRole ( "button" , { name : / c h a t : c o m m a n d E x e c u t i o n .e x p a n d M a n a g e m e n t / i } )
7955 fireEvent . click ( expandButton )
8056
81- // Check for the patterns themselves
57+ // Check that patterns are rendered
8258 expect ( screen . getByText ( "npm" ) ) . toBeInTheDocument ( )
8359 expect ( screen . getByText ( "npm install" ) ) . toBeInTheDocument ( )
8460 expect ( screen . getByText ( "git" ) ) . toBeInTheDocument ( )
85-
86- // Check for the descriptions
87- expect ( screen . getByText ( "- npm commands" ) ) . toBeInTheDocument ( )
88- expect ( screen . getByText ( "- npm install commands" ) ) . toBeInTheDocument ( )
89- expect ( screen . getByText ( "- git commands" ) ) . toBeInTheDocument ( )
9061 } )
9162
92- it ( "should collapse when clicked again" , ( ) => {
93- render ( < CommandPatternSelector { ...defaultProps } /> )
94-
95- const expandButton = screen . getByRole ( "button" , { name : "chat:commandExecution.expandManagement" } )
96- fireEvent . click ( expandButton )
97-
98- const collapseButton = screen . getByRole ( "button" , { name : "chat:commandExecution.collapseManagement" } )
99- fireEvent . click ( collapseButton )
100-
101- expect ( screen . queryByText ( "npm commands" ) ) . not . toBeInTheDocument ( )
102- } )
103-
104- it ( "should show correct status for patterns" , ( ) => {
105- render ( < CommandPatternSelector { ...defaultProps } /> )
106-
107- const expandButton = screen . getByRole ( "button" , { name : "chat:commandExecution.expandManagement" } )
108- fireEvent . click ( expandButton )
109-
110- // Check that npm has allowed styling (green)
111- const npmAllowButton = screen . getAllByRole ( "button" , { name : "chat:commandExecution.removeFromAllowed" } ) [ 0 ]
112- expect ( npmAllowButton ) . toHaveClass ( "bg-green-500/20" )
113-
114- // Check that git has denied styling (red)
115- const gitDenyButton = screen . getAllByRole ( "button" , { name : "chat:commandExecution.removeFromDenied" } ) [ 0 ]
116- expect ( gitDenyButton ) . toHaveClass ( "bg-red-500/20" )
117- } )
118-
119- it ( "should call onAllowPatternChange when allow button is clicked" , ( ) => {
120- render ( < CommandPatternSelector { ...defaultProps } /> )
121-
122- const expandButton = screen . getByRole ( "button" , { name : "chat:commandExecution.expandManagement" } )
123- fireEvent . click ( expandButton )
124-
125- // Find all allow buttons with the "add to allowed" label
126- const allowButtons = screen . getAllByRole ( "button" , { name : "chat:commandExecution.addToAllowed" } )
127-
128- // The second one should be for npm install (first is npm which is already allowed)
129- fireEvent . click ( allowButtons [ 0 ] )
130-
131- expect ( defaultProps . onAllowPatternChange ) . toHaveBeenCalledWith ( "npm install" )
132- } )
133-
134- it ( "should call onDenyPatternChange when deny button is clicked" , ( ) => {
135- render ( < CommandPatternSelector { ...defaultProps } /> )
136-
137- const expandButton = screen . getByRole ( "button" , { name : "chat:commandExecution.expandManagement" } )
138- fireEvent . click ( expandButton )
139-
140- // Find all deny buttons with the "add to denied" label
141- const denyButtons = screen . getAllByRole ( "button" , { name : "chat:commandExecution.addToDenied" } )
142-
143- // The second one should be for npm install (first is npm, third is git which is already denied)
144- fireEvent . click ( denyButtons [ 1 ] )
145-
146- expect ( defaultProps . onDenyPatternChange ) . toHaveBeenCalledWith ( "npm install" )
147- } )
148-
149- it ( "should toggle allowed pattern when clicked" , ( ) => {
150- render ( < CommandPatternSelector { ...defaultProps } /> )
151-
152- const expandButton = screen . getByRole ( "button" , { name : "chat:commandExecution.expandManagement" } )
153- fireEvent . click ( expandButton )
154-
155- // Find the allow button for npm (which is already allowed)
156- const npmAllowButton = screen . getAllByRole ( "button" , { name : "chat:commandExecution.removeFromAllowed" } ) [ 0 ]
157- fireEvent . click ( npmAllowButton )
158-
159- expect ( defaultProps . onAllowPatternChange ) . toHaveBeenCalledWith ( "npm" )
160- } )
161-
162- it ( "should toggle denied pattern when clicked" , ( ) => {
163- render ( < CommandPatternSelector { ...defaultProps } /> )
164-
165- const expandButton = screen . getByRole ( "button" , { name : "chat:commandExecution.expandManagement" } )
166- fireEvent . click ( expandButton )
167-
168- // Find the deny button for git (which is already denied)
169- const gitDenyButton = screen . getAllByRole ( "button" , { name : "chat:commandExecution.removeFromDenied" } ) [ 0 ]
170- fireEvent . click ( gitDenyButton )
171-
172- expect ( defaultProps . onDenyPatternChange ) . toHaveBeenCalledWith ( "git" )
173- } )
174-
175- it ( "should have tooltip with settings link" , ( ) => {
176- const { container } = render ( < CommandPatternSelector { ...defaultProps } /> )
177-
178- // The info icon should have a tooltip
179- const tooltipWrapper = container . querySelector ( '[title="tooltip"]' )
180- expect ( tooltipWrapper ) . toBeTruthy ( )
181-
182- // The tooltip content includes a settings link (mocked as VSCodeLink)
183- // It's rendered in a hidden div for testing purposes
184- const settingsLink = container . querySelector ( 'a[href="#"]' )
185- expect ( settingsLink ) . toBeTruthy ( )
186- expect ( settingsLink ?. textContent ) . toBe ( "View all settings" )
187-
188- // Test that clicking the link posts the correct message
189- if ( settingsLink ) {
190- fireEvent . click ( settingsLink )
191-
192- expect ( mockPostMessage ) . toHaveBeenCalledWith (
193- {
194- type : "action" ,
195- action : "settingsButtonClicked" ,
196- values : { section : "autoApprove" } ,
197- } ,
198- "*" ,
199- )
63+ it ( "should handle duplicate patterns gracefully" , ( ) => {
64+ // Test with duplicate patterns to ensure keys are still unique
65+ const duplicatePatterns : CommandPattern [ ] = [
66+ { pattern : "npm" , description : "npm commands" } ,
67+ { pattern : "npm" , description : "duplicate npm commands" } , // Duplicate pattern
68+ { pattern : "git" , description : "git commands" } ,
69+ ]
70+
71+ const props = {
72+ ...defaultProps ,
73+ patterns : duplicatePatterns ,
20074 }
201- } )
202-
203- it ( "should render with empty patterns" , ( ) => {
204- render ( < CommandPatternSelector { ...defaultProps } patterns = { [ ] } /> )
205-
206- const expandButton = screen . getByRole ( "button" , { name : "chat:commandExecution.expandManagement" } )
207- fireEvent . click ( expandButton )
208-
209- // The expanded view should exist but be empty since there are no patterns
210- const expandedContent = screen
211- . getByRole ( "button" , { name : "chat:commandExecution.collapseManagement" } )
212- . parentElement ?. querySelector ( ".px-3.pb-3" )
213- expect ( expandedContent ) . toBeInTheDocument ( )
214- expect ( expandedContent ?. children . length ) . toBe ( 0 )
215- } )
216-
217- it ( "should render patterns without descriptions" , ( ) => {
218- const patternsWithoutDesc : CommandPattern [ ] = [ { pattern : "custom-command" } ]
219-
220- render ( < CommandPatternSelector { ...defaultProps } patterns = { patternsWithoutDesc } /> )
221-
222- const expandButton = screen . getByRole ( "button" , { name : "chat:commandExecution.expandManagement" } )
223- fireEvent . click ( expandButton )
224-
225- expect ( screen . getByText ( "custom-command" ) ) . toBeInTheDocument ( )
226- } )
227-
228- it ( "should always show info icon with tooltip" , ( ) => {
229- const { container } = render ( < CommandPatternSelector { ...defaultProps } /> )
230-
231- // Info icon should always be visible (not just when expanded)
232- // Look for the Info icon which is wrapped in StandardTooltip
233- const infoIcon = container . querySelector ( ".ml-1" )
234- expect ( infoIcon ) . toBeTruthy ( )
235- } )
236-
237- it ( "should apply correct classes for chevron rotation" , ( ) => {
238- const { container } = render ( < CommandPatternSelector { ...defaultProps } /> )
23975
240- // Initially collapsed - chevron should be rotated
241- let chevron = container . querySelector ( ".size-3.transition-transform" )
242- expect ( chevron ) . toHaveClass ( "-rotate-90" )
76+ // This should not throw an error even with duplicate patterns
77+ const { container } = render (
78+ < TestWrapper >
79+ < CommandPatternSelector { ...props } />
80+ </ TestWrapper > ,
81+ )
82+ expect ( container ) . toBeTruthy ( )
24383
244- // Click to expand
245- const expandButton = screen . getByRole ( "button" , { name : " chat:commandExecution.expandManagement" } )
84+ // Click to expand the component
85+ const expandButton = screen . getByRole ( "button" , { name : / c h a t : c o m m a n d E x e c u t i o n .e x p a n d M a n a g e m e n t / i } )
24686 fireEvent . click ( expandButton )
24787
248- // When expanded - chevron should not be rotated
249- chevron = container . querySelector ( ".size-3.transition-transform ")
250- expect ( chevron ) . toHaveClass ( "rotate-0" )
88+ // Both instances of "npm" should be rendered
89+ const npmElements = screen . getAllByText ( "npm ")
90+ expect ( npmElements ) . toHaveLength ( 2 )
25191 } )
25292} )
0 commit comments