Skip to content

Commit 11d3781

Browse files
committed
test: fix
1 parent 074d2da commit 11d3781

File tree

2 files changed

+89
-169
lines changed

2 files changed

+89
-169
lines changed

knip.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ const config: KnipConfig = {
2626
'uno-preset-rtl.ts!',
2727
'scripts/**/*.ts',
2828
],
29-
project: ['**/*.{ts,vue,cjs,mjs}', '!test/fixtures/**'],
29+
project: [
30+
'**/*.{ts,vue,cjs,mjs}',
31+
'!test/fixtures/**',
32+
'!test/test-utils/**',
33+
'!test/e2e/helpers/**',
34+
],
3035
ignoreDependencies: [
3136
'@iconify-json/*',
3237
'@voidzero-dev/vite-plus-core',
@@ -43,6 +48,9 @@ const config: KnipConfig = {
4348
/** Oxlint plugins don't get picked up yet */
4449
'@e18e/eslint-plugin',
4550
'eslint-plugin-regexp',
51+
52+
/** Used in test/e2e/helpers/ which is excluded from knip project scope */
53+
'h3-next',
4654
],
4755
ignoreUnresolved: ['#components', '#oauth/config'],
4856
},

test/nuxt/components/HeaderConnectorModal.spec.ts

Lines changed: 80 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
99
import { mountSuspended } from '@nuxt/test-utils/runtime'
1010
import { ref, computed, readonly, nextTick } from 'vue'
1111
import 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'>
1612
import type { PendingOperation } from '../../../cli/src/types'
1713
import { 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', {
146125
let 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
156156
beforeEach(() => {
157-
mockControls.reset()
157+
resetMockState()
158158
mockWriteText.mockClear()
159159
})
160160

161161
afterEach(() => {
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(() => {
170169
describe('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

Comments
 (0)