Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions test/nuxt/components/Header/MobileMenu.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, it, expect, vi } from 'vitest'
import { mockNuxtImport, mountSuspended } from '@nuxt/test-utils/runtime'
import { computed, nextTick } from 'vue'
import { HeaderMobileMenu } from '#components'

// Mock useConnector
mockNuxtImport('useConnector', () => () => ({
isConnected: computed(() => false),
npmUser: computed(() => null),
avatar: computed(() => null),
}))
Comment on lines +7 to +11
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align the useConnector mock shape with the real composable contract.

Line 7 mocks isConnected, but the production useConnector contract appears to use connected. This mock can hide a real integration mismatch and give false confidence.

Suggested adjustment
 mockNuxtImport('useConnector', () => () => ({
-  isConnected: computed(() => false),
+  connected: computed(() => false),
   npmUser: computed(() => null),
   avatar: computed(() => null),
 }))

If MobileMenu.client.vue expects isConnected, consider aliasing at destructure time there (e.g. connected: isConnected) so both runtime and tests reflect the same contract.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mockNuxtImport('useConnector', () => () => ({
isConnected: computed(() => false),
npmUser: computed(() => null),
avatar: computed(() => null),
}))
mockNuxtImport('useConnector', () => () => ({
connected: computed(() => false),
npmUser: computed(() => null),
avatar: computed(() => null),
}))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't seem to be the case, where did you get your info?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

Let me check the actual useConnector composable in the codebase to verify.


🏁 Script executed:

#!/bin/bash
# Find useConnector composable definition
rg -n "useConnector" --type ts -C 5

# Also search in .vue files
rg -n "useConnector" --type vue -C 5

Length of output: 2504


🏁 Script executed:

#!/bin/bash
cat app/composables/useConnector.ts

echo "---"
# Also check the mock factory used in HeaderConnectorModal tests
rg -n "createMockUseConnector" test/nuxt/components/HeaderConnectorModal.spec.ts -A 20

# Check MobileMenu.client.vue for what it actually destructures from useConnector
fd "MobileMenu.client.vue" --exec cat {}

Length of output: 23403


@ghostdevv, you're right β€” I apologise for the incorrect comment. Looking at the actual source:

In app/composables/useConnector.ts, the composable's return object explicitly exposes isConnected: computed(() => state.value.connected). While the internal ConnectorState interface uses connected as the raw state field, the public API surface returns it under the name isConnected. The component itself confirms this with const { isConnected, npmUser, avatar: npmAvatar } = useConnector(), and the existing createMockUseConnector in HeaderConnectorModal.spec.ts also uses isConnected. The test mock is correct as written β€” no change needed.


✏️ Learnings added
Learnt from: ghostdevv
Repo: npmx-dev/npmx.dev PR: 2195
File: test/nuxt/components/Header/MobileMenu.spec.ts:7-11
Timestamp: 2026-03-23T01:33:11.889Z
Learning: In npmx-dev/npmx.dev, `useConnector` (app/composables/useConnector.ts) exposes `isConnected` (not `connected`) in its public return object: `isConnected: computed(() => state.value.connected)`. The internal `ConnectorState` interface field is named `connected`, but the composable's returned API uses `isConnected`. Do not flag `isConnected` in mocks or usages as incorrect.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: thasmo
Repo: npmx-dev/npmx.dev PR: 968
File: test/nuxt/components/PackageSidebar.spec.ts:1-14
Timestamp: 2026-02-07T21:01:27.915Z
Learning: In this Nuxt project, h from Vue's render function is auto-imported in test files under test/nuxt/ and does not require an explicit import. This behavior is consistent across all tests in test/nuxt/. When reviewing tests in that directory, assume h is available without an import; avoid adding unnecessary imports or setup steps for h in those files.

Learnt from: CR
Repo: npmx-dev/npmx.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-03T10:09:38.777Z
Learning: Applies to **/*.{test,spec}.{ts,tsx} : Write unit tests for core functionality using `vitest`

Learnt from: alexdln
Repo: npmx-dev/npmx.dev PR: 1757
File: test/nuxt/components/PackageLikeCard.spec.ts:26-32
Timestamp: 2026-02-28T17:51:39.239Z
Learning: In npmx.dev, tests under test/nuxt/ run in Vitest browser mode with Playwright (Chromium). That means DOM properties like scrollWidth, clientWidth, offsetWidth are computed by a real browser engine and can be asserted reliably. Do not assume JSDOM/happy-dom behavior for these tests; prefer using actual browser-driven measurements and ensure tests target layout/visual-related assertions against the browser engine.

Learnt from: CR
Repo: npmx-dev/npmx.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-02-03T10:09:38.777Z
Learning: Applies to **/e2e/**/*.{test,spec}.{ts,tsx} : Write end-to-end tests using Playwright and `nuxt/test-utils`

Learnt from: gameroman
Repo: npmx-dev/npmx.dev PR: 2135
File: test/e2e/interactions.spec.ts:89-90
Timestamp: 2026-03-19T13:44:54.800Z
Learning: In the npmx.dev repository, E2E tests under `test/e2e/` that hit internal API routes (e.g. `/api/registry/analysis/`, `/api/registry/install-size/`) are backed by static JSON fixtures in `test/fixtures/npm-registry/packuments/`. The `nuxt/kit` packument fixture pins `latest` to `4.3.0` with `"type": "module"` and `.mjs` exports, so asserting `moduleFormat === 'esm'` for `nuxt/kit` in E2E tests is stable and not flaky. Do not suggest weakening such assertions on the grounds of live-data brittleness.


// Mock useAtproto
mockNuxtImport('useAtproto', () => () => ({
user: computed(() => null),
}))

// Mock useFocusTrap (from @vueuse/integrations)
vi.mock('@vueuse/integrations/useFocusTrap', () => ({
useFocusTrap: () => ({
activate: vi.fn(),
deactivate: vi.fn(),
}),
}))

describe('MobileMenu', () => {
async function mountMenu(open = false) {
return mountSuspended(HeaderMobileMenu, {
props: {
open,
links: [
{
type: 'group' as const,
name: 'main',
label: 'Navigation',
items: [
{
type: 'link' as const,
name: 'home',
label: 'Home',
to: '/',
iconClass: 'i-lucide:home',
},
],
},
],
},
attachTo: document.body,
})
}

it('is closed by default', async () => {
const wrapper = await mountMenu(false)
try {
// Menu content is behind v-if="isOpen" inside a Teleport
expect(document.querySelector('[role="dialog"]')).toBeNull()
} finally {
wrapper.unmount()
}
})

it('opens when the open prop is set to true', async () => {
const wrapper = await mountMenu(true)
try {
await nextTick()
const dialog = document.querySelector('[role="dialog"]')
expect(dialog).not.toBeNull()
expect(dialog?.getAttribute('aria-modal')).toBe('true')
} finally {
wrapper.unmount()
}
})

it('closes when open prop changes from true to false', async () => {
const wrapper = await mountMenu(true)
try {
await nextTick()
expect(document.querySelector('[role="dialog"]')).not.toBeNull()

await wrapper.setProps({ open: false })
await nextTick()
expect(document.querySelector('[role="dialog"]')).toBeNull()
} finally {
wrapper.unmount()
}
})

it('emits update:open false when backdrop is clicked', async () => {
const wrapper = await mountMenu(true)
try {
await nextTick()
const backdrop = document.querySelector('[role="dialog"] > button')
expect(backdrop).not.toBeNull()
backdrop?.dispatchEvent(new Event('click', { bubbles: true }))
await nextTick()
expect(wrapper.emitted('update:open')).toBeTruthy()
expect(wrapper.emitted('update:open')![0]).toEqual([false])
} finally {
wrapper.unmount()
}
})

it('emits update:open false when close button is clicked', async () => {
const wrapper = await mountMenu(true)
try {
await nextTick()
// Close button has aria-label matching $t('common.close') β€” find it inside nav
const closeBtn = document.querySelector('nav button[aria-label]')
expect(closeBtn).not.toBeNull()
closeBtn?.dispatchEvent(new Event('click', { bubbles: true }))
await nextTick()
expect(wrapper.emitted('update:open')).toBeTruthy()
expect(wrapper.emitted('update:open')![0]).toEqual([false])
} finally {
wrapper.unmount()
}
})
})
Loading