@@ -9,10 +9,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
99import { mountSuspended } from '@nuxt/test-utils/runtime'
1010import { ref , computed , readonly , nextTick } from 'vue'
1111import type { VueWrapper } from '@vue/test-utils'
12- import type { MockConnectorTestControls } from '../../test-utils'
13-
14- /** Subset of MockConnectorTestControls for unit tests that don't need stateManager */
15- type UnitTestConnectorControls = Omit < MockConnectorTestControls , 'stateManager' >
1612import type { PendingOperation } from '../../../cli/src/types'
1713import { HeaderConnectorModal } from '#components'
1814
@@ -84,39 +80,22 @@ function createMockUseConnector() {
8480 }
8581}
8682
87- // Test controls for manipulating mock state
88- const mockControls : UnitTestConnectorControls = {
89- setOrgData : vi . fn ( ) ,
90- setUserOrgs : vi . fn ( ) ,
91- setUserPackages : vi . fn ( ) ,
92- setPackageData : vi . fn ( ) ,
93- reset ( ) {
94- mockState . value = {
95- connected : false ,
96- connecting : false ,
97- npmUser : null ,
98- avatar : null ,
99- operations : [ ] ,
100- error : null ,
101- lastExecutionTime : null ,
102- }
103- } ,
104- simulateConnect ( ) {
105- mockState . value . connected = true
106- mockState . value . npmUser = 'testuser'
107- mockState . value . avatar = 'https://example.com/avatar.png'
108- } ,
109- simulateDisconnect ( ) {
110- mockState . value . connected = false
111- mockState . value . npmUser = null
112- mockState . value . avatar = null
113- } ,
114- simulateError ( message : string ) {
115- mockState . value . error = message
116- } ,
117- clearError ( ) {
118- mockState . value . error = null
119- } ,
83+ function resetMockState ( ) {
84+ mockState . value = {
85+ connected : false ,
86+ connecting : false ,
87+ npmUser : null ,
88+ avatar : null ,
89+ operations : [ ] ,
90+ error : null ,
91+ lastExecutionTime : null ,
92+ }
93+ }
94+
95+ function simulateConnect ( ) {
96+ mockState . value . connected = true
97+ mockState . value . npmUser = 'testuser'
98+ mockState . value . avatar = 'https://example.com/avatar.png'
12099}
121100
122101// Mock the composables at module level (vi.mock is hoisted)
@@ -146,21 +125,41 @@ vi.stubGlobal('navigator', {
146125let currentWrapper : VueWrapper | null = null
147126
148127/**
149- * Get the modal dialog element from the document body (where Teleport sends it)
128+ * Get the modal dialog element from the document body (where Teleport sends it).
129+ */
130+ function getModalDialog ( ) : HTMLDialogElement | null {
131+ return document . body . querySelector ( 'dialog#connector-modal' )
132+ }
133+
134+ /**
135+ * Mount the component and open the dialog via showModal().
150136 */
151- function getModalDialog ( ) : HTMLElement | null {
152- return document . body . querySelector ( '[role="dialog"]' )
137+ async function mountAndOpen ( state ?: 'connected' | 'error' ) {
138+ if ( state === 'connected' ) simulateConnect ( )
139+ if ( state === 'error' ) {
140+ mockState . value . error = 'Could not reach connector. Is it running?'
141+ }
142+
143+ currentWrapper = await mountSuspended ( HeaderConnectorModal , {
144+ attachTo : document . body ,
145+ } )
146+ await nextTick ( )
147+
148+ const dialog = getModalDialog ( )
149+ dialog ?. showModal ( )
150+ await nextTick ( )
151+
152+ return dialog
153153}
154154
155155// Reset state before each test
156156beforeEach ( ( ) => {
157- mockControls . reset ( )
157+ resetMockState ( )
158158 mockWriteText . mockClear ( )
159159} )
160160
161161afterEach ( ( ) => {
162162 vi . clearAllMocks ( )
163- // Clean up Vue wrapper to remove teleported content
164163 if ( currentWrapper ) {
165164 currentWrapper . unmount ( )
166165 currentWrapper = null
@@ -170,13 +169,7 @@ afterEach(() => {
170169describe ( 'HeaderConnectorModal' , ( ) => {
171170 describe ( 'Disconnected state' , ( ) => {
172171 it ( 'shows connection form when not connected' , async ( ) => {
173- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
174- props : { open : true } ,
175- attachTo : document . body ,
176- } )
177- await nextTick ( )
178-
179- const dialog = getModalDialog ( )
172+ const dialog = await mountAndOpen ( )
180173 expect ( dialog ) . not . toBeNull ( )
181174
182175 // Should show the form (disconnected state)
@@ -193,55 +186,29 @@ describe('HeaderConnectorModal', () => {
193186 } )
194187
195188 it ( 'shows the CLI command to run' , async ( ) => {
196- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
197- props : { open : true } ,
198- attachTo : document . body ,
199- } )
200- await nextTick ( )
201-
202- const dialog = getModalDialog ( )
203- expect ( dialog ?. textContent ) . toContain ( 'npx npmx-connector' )
189+ const dialog = await mountAndOpen ( )
190+ // The command is now "pnpm npmx-connector"
191+ expect ( dialog ?. textContent ) . toContain ( 'npmx-connector' )
204192 } )
205193
206- it ( 'can copy command to clipboard' , async ( ) => {
207- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
208- props : { open : true } ,
209- attachTo : document . body ,
210- } )
211- await nextTick ( )
212-
213- const dialog = getModalDialog ( )
214- const copyButton = dialog ?. querySelector (
215- 'button[aria-label="Copy command"]' ,
216- ) as HTMLButtonElement
217- expect ( copyButton ) . not . toBeNull ( )
218-
219- copyButton ?. click ( )
220- await nextTick ( )
221-
222- expect ( mockWriteText ) . toHaveBeenCalled ( )
194+ it ( 'has a copy button for the command' , async ( ) => {
195+ const dialog = await mountAndOpen ( )
196+ // The copy button is inside the command block (dir="ltr" div)
197+ const commandBlock = dialog ?. querySelector ( 'div[dir="ltr"]' )
198+ const copyBtn = commandBlock ?. querySelector ( 'button' ) as HTMLButtonElement
199+ expect ( copyBtn ) . toBeTruthy ( )
200+ // The button should have a copy-related aria-label
201+ expect ( copyBtn ?. getAttribute ( 'aria-label' ) ) . toBeTruthy ( )
223202 } )
224203
225204 it ( 'disables connect button when token is empty' , async ( ) => {
226- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
227- props : { open : true } ,
228- attachTo : document . body ,
229- } )
230- await nextTick ( )
231-
232- const dialog = getModalDialog ( )
205+ const dialog = await mountAndOpen ( )
233206 const connectButton = dialog ?. querySelector ( 'button[type="submit"]' ) as HTMLButtonElement
234207 expect ( connectButton ?. disabled ) . toBe ( true )
235208 } )
236209
237210 it ( 'enables connect button when token is entered' , async ( ) => {
238- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
239- props : { open : true } ,
240- attachTo : document . body ,
241- } )
242- await nextTick ( )
243-
244- const dialog = getModalDialog ( )
211+ const dialog = await mountAndOpen ( )
245212 const tokenInput = dialog ?. querySelector ( 'input[name="connector-token"]' ) as HTMLInputElement
246213 expect ( tokenInput ) . not . toBeNull ( )
247214
@@ -255,18 +222,18 @@ describe('HeaderConnectorModal', () => {
255222 } )
256223
257224 it ( 'shows error message when connection fails' , async ( ) => {
258- // Simulate an error before mounting
259- mockControls . simulateError ( 'Could not reach connector. Is it running?' )
225+ const dialog = await mountAndOpen ( 'error' )
226+ // Error needs hasAttemptedConnect=true to show. Simulate a connect attempt first.
227+ const tokenInput = dialog ?. querySelector ( 'input[name="connector-token"]' ) as HTMLInputElement
228+ tokenInput . value = 'bad-token'
229+ tokenInput . dispatchEvent ( new Event ( 'input' , { bubbles : true } ) )
230+ await nextTick ( )
260231
261- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
262- props : { open : true } ,
263- attachTo : document . body ,
264- } )
232+ const form = dialog ?. querySelector ( 'form' )
233+ form ?. dispatchEvent ( new Event ( 'submit' , { bubbles : true } ) )
265234 await nextTick ( )
266235
267- const dialog = getModalDialog ( )
268236 const alerts = dialog ?. querySelectorAll ( '[role="alert"]' )
269- // Find the alert containing our error message
270237 const errorAlert = Array . from ( alerts || [ ] ) . find ( el =>
271238 el . textContent ?. includes ( 'Could not reach connector' ) ,
272239 )
@@ -275,41 +242,18 @@ describe('HeaderConnectorModal', () => {
275242 } )
276243
277244 describe ( 'Connected state' , ( ) => {
278- beforeEach ( ( ) => {
279- // Start in connected state
280- mockControls . simulateConnect ( )
281- } )
282-
283245 it ( 'shows connected status' , async ( ) => {
284- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
285- props : { open : true } ,
286- attachTo : document . body ,
287- } )
288- await nextTick ( )
289-
290- const dialog = getModalDialog ( )
246+ const dialog = await mountAndOpen ( 'connected' )
291247 expect ( dialog ?. textContent ) . toContain ( 'Connected' )
292248 } )
293249
294250 it ( 'shows logged in username' , async ( ) => {
295- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
296- props : { open : true } ,
297- attachTo : document . body ,
298- } )
299- await nextTick ( )
300-
301- const dialog = getModalDialog ( )
251+ const dialog = await mountAndOpen ( 'connected' )
302252 expect ( dialog ?. textContent ) . toContain ( 'testuser' )
303253 } )
304254
305255 it ( 'shows disconnect button' , async ( ) => {
306- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
307- props : { open : true } ,
308- attachTo : document . body ,
309- } )
310- await nextTick ( )
311-
312- const dialog = getModalDialog ( )
256+ const dialog = await mountAndOpen ( 'connected' )
313257 const buttons = dialog ?. querySelectorAll ( 'button' )
314258 const disconnectBtn = Array . from ( buttons || [ ] ) . find ( b =>
315259 b . textContent ?. toLowerCase ( ) . includes ( 'disconnect' ) ,
@@ -318,72 +262,40 @@ describe('HeaderConnectorModal', () => {
318262 } )
319263
320264 it ( 'hides connection form when connected' , async ( ) => {
321- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
322- props : { open : true } ,
323- attachTo : document . body ,
324- } )
325- await nextTick ( )
326-
327- const dialog = getModalDialog ( )
328- // Form and token input should not exist when connected
265+ const dialog = await mountAndOpen ( 'connected' )
329266 const form = dialog ?. querySelector ( 'form' )
330267 expect ( form ) . toBeNull ( )
331268 } )
332269 } )
333270
334271 describe ( 'Modal behavior' , ( ) => {
335272 it ( 'closes modal when close button is clicked' , async ( ) => {
336- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
337- props : { open : true } ,
338- attachTo : document . body ,
339- } )
340- await nextTick ( )
273+ const dialog = await mountAndOpen ( )
341274
342- const dialog = getModalDialog ( )
343- // Find the close button (X icon) within the dialog header
344- const closeBtn = dialog ?. querySelector ( 'button[aria-label="Close"]' ) as HTMLButtonElement
345- expect ( closeBtn ) . not . toBeNull ( )
346-
347- closeBtn ?. click ( )
348- await nextTick ( )
349-
350- // Check that open was set to false (v-model)
351- const emitted = currentWrapper . emitted ( 'update:open' )
352- expect ( emitted ) . toBeTruthy ( )
353- expect ( emitted ! [ 0 ] ) . toEqual ( [ false ] )
354- } )
355-
356- it ( 'closes modal when backdrop is clicked' , async ( ) => {
357- currentWrapper = await mountSuspended ( HeaderConnectorModal , {
358- props : { open : true } ,
359- attachTo : document . body ,
360- } )
361- await nextTick ( )
362-
363- // Find the backdrop button by aria-label
364- const backdrop = document . body . querySelector (
365- 'button[aria-label="Close modal"]' ,
275+ // Find the close button (ButtonBase with close icon) in the dialog header
276+ const closeBtn = Array . from ( dialog ?. querySelectorAll ( 'button' ) ?? [ ] ) . find (
277+ b =>
278+ b . querySelector ( '[class*="close"]' ) ||
279+ b . getAttribute ( 'aria-label' ) ?. toLowerCase ( ) . includes ( 'close' ) ,
366280 ) as HTMLButtonElement
367- expect ( backdrop ) . not . toBeNull ( )
281+ expect ( closeBtn ) . toBeTruthy ( )
368282
369- backdrop ?. click ( )
283+ closeBtn ?. click ( )
370284 await nextTick ( )
371285
372- // Check that open was set to false (v-model)
373- const emitted = currentWrapper . emitted ( 'update:open' )
374- expect ( emitted ) . toBeTruthy ( )
375- expect ( emitted ! [ 0 ] ) . toEqual ( [ false ] )
286+ // Dialog should be closed (open attribute removed)
287+ expect ( dialog ?. open ) . toBe ( false )
376288 } )
377289
378- it ( 'does not render dialog when open is false ' , async ( ) => {
290+ it ( 'does not render dialog content when not opened ' , async ( ) => {
379291 currentWrapper = await mountSuspended ( HeaderConnectorModal , {
380- props : { open : false } ,
381292 attachTo : document . body ,
382293 } )
383294 await nextTick ( )
384295
385296 const dialog = getModalDialog ( )
386- expect ( dialog ) . toBeNull ( )
297+ // Dialog exists in DOM but should not be open
298+ expect ( dialog ?. open ) . toBeFalsy ( )
387299 } )
388300 } )
389301} )
0 commit comments