@@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({
1212 remove : vi . fn ( ) ,
1313 install : vi . fn ( ) ,
1414 refetch : vi . fn ( ) ,
15+ copy : vi . fn ( ) ,
1516 removeError : null as Error | null ,
1617} ) )
1718vi . mock ( 'nuqs' , ( ) => ( { useQueryState : ( ) => [ null , vi . fn ( ) ] } ) )
@@ -53,6 +54,8 @@ let container: HTMLDivElement
5354beforeEach ( ( ) => {
5455 vi . clearAllMocks ( )
5556 vi . stubGlobal ( 'IS_REACT_ACT_ENVIRONMENT' , true )
57+ vi . stubGlobal ( 'navigator' , { clipboard : { writeText : mocks . copy } } )
58+ mocks . copy . mockReset ( ) . mockResolvedValue ( undefined )
5659 mocks . context . mockReturnValue ( { organization : { id : 'org-1' } , viewer : { isAdmin : true } } )
5760 mocks . list . mockReturnValue ( { data : { installations : [ ] , bots : [ ] } } )
5861 mocks . manifest . mockReturnValue ( {
@@ -138,7 +141,11 @@ describe('Slack Search settings and shared wizard', () => {
138141 expect ( container . textContent ) . toContain ( 'Enabled' )
139142 await action ( 'Reconnect' )
140143 expect ( document . querySelector ( '[role="dialog"]' ) ) . toHaveTextContent ( 'Reconnect Slack Search' )
144+ await click ( 'Copy app configuration' )
145+ expect ( mocks . copy ) . toHaveBeenCalledExactlyOnceWith ( '{}' )
141146 expect ( document . querySelector ( 'a[href="https://api.slack.com/apps/A1"]' ) ) . not . toBeNull ( )
147+ expect ( document . querySelector ( '[role="dialog"]' ) ) . toHaveTextContent ( 'Configuration copied' )
148+ expect ( document . querySelector ( 'pre' ) ) . toBeNull ( )
142149 await click ( 'Continue' )
143150 expect ( document . querySelector ( '[role="dialog"]' ) ) . toHaveTextContent ( 'Leave fields blank' )
144151 await click ( 'Continue' )
@@ -154,6 +161,40 @@ describe('Slack Search settings and shared wizard', () => {
154161 expect ( mocks . install . mock . calls [ 0 ] [ 0 ] ) . not . toHaveProperty ( 'clientSecret' )
155162 } )
156163
164+ it ( 'keeps the update action available when clipboard access fails' , async ( ) => {
165+ mocks . copy . mockRejectedValueOnce ( new Error ( 'Clipboard access denied' ) )
166+ await render ( true )
167+ await action ( 'Reconnect' )
168+ await click ( 'Copy app configuration' )
169+ expect ( document . querySelector ( '[role="alert"]' ) ) . toHaveTextContent ( 'Allow clipboard access' )
170+ expect ( document . querySelector ( 'a[href="https://api.slack.com/apps/A1"]' ) ) . toBeNull ( )
171+ expect ( button ( 'Copy app configuration' ) ) . toBeDefined ( )
172+ await click ( 'Copy app configuration' )
173+ expect ( document . querySelector ( '[role="alert"]' ) ) . toBeNull ( )
174+ expect ( document . querySelector ( 'a[href="https://api.slack.com/apps/A1"]' ) ) . not . toBeNull ( )
175+ } )
176+
177+ it ( 'offers the same configuration update for an app shared with Slack sources' , async ( ) => {
178+ mocks . manifest . mockReturnValue ( {
179+ data : {
180+ manifest : '{"display_information":{"name":"Shared Slack app"}}' ,
181+ existingApp : { appId : 'A2' } ,
182+ createAppUrl : 'https://api.slack.com/apps' ,
183+ } ,
184+ isPending : false ,
185+ refetch : mocks . refetch ,
186+ } )
187+ await render ( )
188+ await click ( 'Set up' )
189+ expect ( document . querySelector ( '[role="dialog"]' ) ) . toHaveTextContent ( 'Update your Slack app' )
190+ await click ( 'Copy app configuration' )
191+ expect ( mocks . copy ) . toHaveBeenCalledExactlyOnceWith (
192+ '{"display_information":{"name":"Shared Slack app"}}'
193+ )
194+ expect ( document . querySelector ( 'a[href="https://api.slack.com/apps/A2"]' ) ) . not . toBeNull ( )
195+ expect ( document . querySelector ( 'pre' ) ) . toBeNull ( )
196+ } )
197+
157198 it ( 'disables the selected connection from the actions menu' , async ( ) => {
158199 await render ( true )
159200 await action ( 'Disable' )
0 commit comments