11import { describe , it , expect , vi , beforeEach , afterEach } from 'vitest'
2- import { mountSuspended } from '@nuxt/test-utils/runtime'
2+ import { mockNuxtImport , mountSuspended } from '@nuxt/test-utils/runtime'
33import { ref , computed , readonly , nextTick } from 'vue'
44import type { VueWrapper } from '@vue/test-utils'
55import type { PendingOperation } from '../../../cli/src/types'
@@ -110,11 +110,6 @@ function simulateConnect() {
110110 mockState . value . avatar = 'https://example.com/avatar.png'
111111}
112112
113- // Mock the composables at module level (vi.mock is hoisted)
114- vi . mock ( '~/composables/useConnector' , ( ) => ( {
115- useConnector : createMockUseConnector ,
116- } ) )
117-
118113const mockSettings = ref ( {
119114 relativeDates : false ,
120115 includeTypesInInstall : true ,
@@ -131,13 +126,17 @@ const mockSettings = ref({
131126 } ,
132127} )
133128
134- vi . mock ( '~/composables/useSettings ', ( ) => ( {
135- useSettings : ( ) => ( { settings : mockSettings } ) ,
136- } ) )
129+ mockNuxtImport ( 'useConnector ', ( ) => {
130+ return createMockUseConnector
131+ } )
137132
138- vi . mock ( '~/composables/useSelectedPackageManager' , ( ) => ( {
139- useSelectedPackageManager : ( ) => ref ( 'npm' ) ,
140- } ) )
133+ mockNuxtImport ( 'useSettings' , ( ) => {
134+ return ( ) => ( { settings : mockSettings } )
135+ } )
136+
137+ mockNuxtImport ( 'useSelectedPackageManager' , ( ) => {
138+ return ( ) => ref ( 'npm' )
139+ } )
141140
142141vi . mock ( '~/utils/npm' , ( ) => ( {
143142 getExecuteCommand : ( ) => 'npx npmx-connector' ,
@@ -199,6 +198,181 @@ afterEach(() => {
199198} )
200199
201200describe ( 'HeaderConnectorModal' , ( ) => {
201+ describe ( 'Web auth settings (connected)' , ( ) => {
202+ it ( 'shows web auth toggle when connected' , async ( ) => {
203+ const dialog = await mountAndOpen ( 'connected' )
204+ const labels = Array . from ( dialog ?. querySelectorAll ( 'label, span' ) ?? [ ] )
205+ const webAuthLabel = labels . find ( el => el . textContent ?. includes ( 'web authentication' ) )
206+ expect ( webAuthLabel ) . toBeTruthy ( )
207+ } )
208+
209+ it ( 'shows auto-open URL toggle when connected' , async ( ) => {
210+ const dialog = await mountAndOpen ( 'connected' )
211+ const labels = Array . from ( dialog ?. querySelectorAll ( 'label, span' ) ?? [ ] )
212+ const autoOpenLabel = labels . find ( el => el . textContent ?. includes ( 'open auth page' ) )
213+ expect ( autoOpenLabel ) . toBeTruthy ( )
214+ } )
215+
216+ it ( 'auto-open URL toggle is disabled when webAuth is off' , async ( ) => {
217+ mockSettings . value . connector . webAuth = false
218+ const dialog = await mountAndOpen ( 'connected' )
219+
220+ // Find the auto-open toggle container - it should have opacity-50 class
221+ const toggleContainers = Array . from ( dialog ?. querySelectorAll ( '[class*="opacity-50"]' ) ?? [ ] )
222+ expect ( toggleContainers . length ) . toBeGreaterThan ( 0 )
223+ } )
224+
225+ it ( 'auto-open URL toggle is not disabled when webAuth is on' , async ( ) => {
226+ mockSettings . value . connector . webAuth = true
227+ const dialog = await mountAndOpen ( 'connected' )
228+
229+ // When webAuth is ON, the auto-open toggle should not have opacity-50
230+ // Verify by checking that we can find the toggle with the "open auth page" label
231+ // and it does NOT have opacity-50 in its parent
232+ const autoOpenLabels = Array . from ( dialog ?. querySelectorAll ( '*' ) ?? [ ] ) . filter ( el =>
233+ el . textContent ?. includes ( 'open auth page' ) ,
234+ )
235+ expect ( autoOpenLabels . length ) . toBeGreaterThan ( 0 )
236+ } )
237+ } )
238+
239+ describe ( 'Web auth settings (disconnected advanced)' , ( ) => {
240+ it ( 'shows web auth toggles in advanced details section' , async ( ) => {
241+ const dialog = await mountAndOpen ( )
242+
243+ // Open the advanced details section
244+ const details = dialog ?. querySelector ( 'details' )
245+ expect ( details ) . not . toBeNull ( )
246+
247+ // Programmatically open it
248+ details ?. setAttribute ( 'open' , '' )
249+ await nextTick ( )
250+
251+ const labels = Array . from ( details ?. querySelectorAll ( 'label, span' ) ?? [ ] )
252+ const webAuthLabel = labels . find ( el => el . textContent ?. includes ( 'web authentication' ) )
253+ const autoOpenLabel = labels . find ( el => el . textContent ?. includes ( 'open auth page' ) )
254+ expect ( webAuthLabel ) . toBeTruthy ( )
255+ expect ( autoOpenLabel ) . toBeTruthy ( )
256+ } )
257+ } )
258+
259+ describe ( 'Auth URL button' , ( ) => {
260+ it ( 'does not show auth URL button when no running operations have an authUrl' , async ( ) => {
261+ const dialog = await mountAndOpen ( 'connected' )
262+
263+ const buttons = Array . from ( dialog ?. querySelectorAll ( 'button' ) ?? [ ] )
264+ const authUrlBtn = buttons . find ( b => b . textContent ?. includes ( 'web auth link' ) )
265+ expect ( authUrlBtn ) . toBeUndefined ( )
266+ } )
267+
268+ it ( 'shows auth URL button when a running operation has an authUrl' , async ( ) => {
269+ mockState . value . operations = [
270+ {
271+ id : '0000000000000001' ,
272+ type : 'org:add-user' ,
273+ params : { org : 'myorg' , user : 'alice' , role : 'developer' } ,
274+ description : 'Add alice' ,
275+ command : 'npm org set myorg alice developer' ,
276+ status : 'running' ,
277+ createdAt : Date . now ( ) ,
278+ authUrl : 'https://www.npmjs.com/login?next=/login/cli/abc123' ,
279+ } ,
280+ ]
281+ const dialog = await mountAndOpen ( 'connected' )
282+
283+ const buttons = Array . from ( dialog ?. querySelectorAll ( 'button' ) ?? [ ] )
284+ const authUrlBtn = buttons . find ( b => b . textContent ?. includes ( 'web auth link' ) )
285+ expect ( authUrlBtn ) . toBeTruthy ( )
286+ } )
287+
288+ it ( 'opens auth URL in new tab when button is clicked' , async ( ) => {
289+ const mockOpen = vi . fn ( )
290+ vi . stubGlobal ( 'open' , mockOpen )
291+
292+ mockState . value . operations = [
293+ {
294+ id : '0000000000000001' ,
295+ type : 'org:add-user' ,
296+ params : { org : 'myorg' , user : 'alice' , role : 'developer' } ,
297+ description : 'Add alice' ,
298+ command : 'npm org set myorg alice developer' ,
299+ status : 'running' ,
300+ createdAt : Date . now ( ) ,
301+ authUrl : 'https://www.npmjs.com/login?next=/login/cli/abc123' ,
302+ } ,
303+ ]
304+ const dialog = await mountAndOpen ( 'connected' )
305+
306+ const buttons = Array . from ( dialog ?. querySelectorAll ( 'button' ) ?? [ ] )
307+ const authUrlBtn = buttons . find ( b =>
308+ b . textContent ?. includes ( 'web auth link' ) ,
309+ ) as HTMLButtonElement
310+ authUrlBtn ?. click ( )
311+ await nextTick ( )
312+
313+ expect ( mockOpen ) . toHaveBeenCalledWith (
314+ 'https://www.npmjs.com/login?next=/login/cli/abc123' ,
315+ '_blank' ,
316+ 'noopener,noreferrer' ,
317+ )
318+
319+ vi . unstubAllGlobals ( )
320+ // Re-stub navigator.clipboard which was unstubbed
321+ vi . stubGlobal ( 'navigator' , {
322+ ...navigator ,
323+ clipboard : {
324+ writeText : mockWriteText ,
325+ readText : vi . fn ( ) . mockResolvedValue ( '' ) ,
326+ } ,
327+ } )
328+ } )
329+ } )
330+
331+ describe ( 'Operations queue in connected state' , ( ) => {
332+ it ( 'renders OTP prompt when operations have OTP failures' , async ( ) => {
333+ mockState . value . operations = [
334+ {
335+ id : '0000000000000001' ,
336+ type : 'org:add-user' ,
337+ params : { org : 'myorg' , user : 'alice' , role : 'developer' } ,
338+ description : 'Add alice' ,
339+ command : 'npm org set myorg alice developer' ,
340+ status : 'failed' ,
341+ createdAt : Date . now ( ) ,
342+ result : { stdout : '' , stderr : 'otp required' , exitCode : 1 , requiresOtp : true } ,
343+ } ,
344+ ]
345+ const dialog = await mountAndOpen ( 'connected' )
346+
347+ // The OrgOperationsQueue child should render with the OTP alert
348+ const otpAlert = dialog ?. querySelector ( '[role="alert"]' )
349+ expect ( otpAlert ) . not . toBeNull ( )
350+ expect ( dialog ?. innerHTML ) . toContain ( 'otp-input' )
351+ } )
352+
353+ it ( 'does not show retry with web auth button when webAuth setting is off' , async ( ) => {
354+ mockSettings . value . connector . webAuth = false
355+ mockState . value . operations = [
356+ {
357+ id : '0000000000000001' ,
358+ type : 'org:add-user' ,
359+ params : { org : 'myorg' , user : 'alice' , role : 'developer' } ,
360+ description : 'Add alice' ,
361+ command : 'npm org set myorg alice developer' ,
362+ status : 'failed' ,
363+ createdAt : Date . now ( ) ,
364+ result : { stdout : '' , stderr : 'otp required' , exitCode : 1 , requiresOtp : true } ,
365+ } ,
366+ ]
367+ const dialog = await mountAndOpen ( 'connected' )
368+
369+ const html = dialog ?. innerHTML ?? ''
370+ const hasWebAuthButton =
371+ html . includes ( 'Retry with web auth' ) || html . includes ( 'retry_web_auth' )
372+ expect ( hasWebAuthButton ) . toBe ( false )
373+ } )
374+ } )
375+
202376 describe ( 'Disconnected state' , ( ) => {
203377 it ( 'shows connection form when not connected' , async ( ) => {
204378 const dialog = await mountAndOpen ( )
0 commit comments