From 140f4d97d7209f04d788754292b7e1b23b842982 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Mon, 25 May 2026 20:09:31 -0400 Subject: [PATCH 1/2] test: re-work some brittle atproto mocks Stop mocking `useAtproto` in a few tests and instead set the shared composable state that the app reads at runtime. These tests were brittle because the mocked binding could diverge from the import path used by the component or composable under test, suddenly quietly shifting the exercised state from under us, leading to difficult to debug failures. --- test/nuxt/components/Package/Likes.spec.ts | 50 +++++++++---------- .../components/ProfileInviteSection.spec.ts | 37 +++++++------- .../use-command-palette-commands.spec.ts | 34 ++++++++----- 3 files changed, 62 insertions(+), 59 deletions(-) diff --git a/test/nuxt/components/Package/Likes.spec.ts b/test/nuxt/components/Package/Likes.spec.ts index 4dd367f9b6..4ae4648a37 100644 --- a/test/nuxt/components/Package/Likes.spec.ts +++ b/test/nuxt/components/Package/Likes.spec.ts @@ -1,26 +1,23 @@ import type { VueWrapper } from '@vue/test-utils' import { mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { ref } from 'vue' import Likes from '~/components/Package/Likes.vue' +import { useAtproto } from '~/composables/atproto/useAtproto' -const { mockUseAtproto } = vi.hoisted(() => ({ - mockUseAtproto: vi.fn(), -})) - -vi.mock('~/composables/atproto/useAtproto', () => ({ - useAtproto: mockUseAtproto, -})) +function createAtprotoUser(handle: string) { + return { + did: `did:plc:${handle}`, + handle, + pds: 'https://bsky.social', + } +} describe('PackageLikes', () => { let wrapper: VueWrapper | undefined beforeEach(() => { - mockUseAtproto.mockReturnValue({ - user: ref(null), - pending: ref(false), - logout: vi.fn(), - }) + const { user } = useAtproto() + user.value = null }) afterEach(() => { @@ -75,26 +72,25 @@ describe('PackageLikes', () => { it('keeps the top liked badge when a like response omits the rank', async () => { let likeRequests = 0 - - mockUseAtproto.mockReturnValue({ - user: ref({ handle: 'tester.test' }), - pending: ref(false), - logout: vi.fn(), - }) + const { user } = useAtproto() + user.value = createAtprotoUser('tester.test') registerEndpoint('/api/social/likes/svelte', () => ({ totalLikes: 42, userHasLiked: false, topLikedRank: 3, })) - registerEndpoint('/api/social/like', () => { - likeRequests++ - - return { - totalLikes: 43, - userHasLiked: true, - topLikedRank: null, - } + registerEndpoint('/api/social/like', { + method: 'POST', + handler: () => { + likeRequests++ + + return { + totalLikes: 43, + userHasLiked: true, + topLikedRank: null, + } + }, }) wrapper = await mountSuspended(Likes, { diff --git a/test/nuxt/components/ProfileInviteSection.spec.ts b/test/nuxt/components/ProfileInviteSection.spec.ts index 6b5a897ce7..d27a00fd46 100644 --- a/test/nuxt/components/ProfileInviteSection.spec.ts +++ b/test/nuxt/components/ProfileInviteSection.spec.ts @@ -1,16 +1,23 @@ import { mockNuxtImport, mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime' import { describe, expect, it, vi, beforeEach } from 'vitest' +import { useAtproto } from '~/composables/atproto/useAtproto' -const { mockUseAtproto, mockUseProfileLikes } = vi.hoisted(() => ({ - mockUseAtproto: vi.fn(), +const { mockUseProfileLikes } = vi.hoisted(() => ({ mockUseProfileLikes: vi.fn(), })) -mockNuxtImport('useAtproto', () => mockUseAtproto) mockNuxtImport('useProfileLikes', () => mockUseProfileLikes) import ProfilePage from '~/pages/profile/[identity]/index.vue' +function createAtprotoUser(handle: string) { + return { + did: `did:plc:${handle}`, + handle, + pds: 'https://bsky.social', + } +} + registerEndpoint('/api/social/profile/test-handle', () => ({ displayName: 'Test User', description: '', @@ -21,16 +28,14 @@ registerEndpoint('/api/social/profile/test-handle', () => ({ describe('Profile invite section', () => { beforeEach(() => { - mockUseAtproto.mockReset() + const { user } = useAtproto() + user.value = null mockUseProfileLikes.mockReset() }) it('does not show invite section while auth is still loading', async () => { - mockUseAtproto.mockReturnValue({ - user: ref(null), - pending: ref(true), - logout: vi.fn(), - }) + const { user } = useAtproto() + user.value = undefined mockUseProfileLikes.mockReturnValue({ data: ref({ records: [] }), @@ -45,11 +50,8 @@ describe('Profile invite section', () => { }) it('shows invite section after auth resolves for non-owner', async () => { - mockUseAtproto.mockReturnValue({ - user: ref({ handle: 'other-user' }), - pending: ref(false), - logout: vi.fn(), - }) + const { user } = useAtproto() + user.value = createAtprotoUser('other-user') mockUseProfileLikes.mockReturnValue({ data: ref({ records: [] }), @@ -64,11 +66,8 @@ describe('Profile invite section', () => { }) it('does not show invite section for profile owner', async () => { - mockUseAtproto.mockReturnValue({ - user: ref({ handle: 'test-handle' }), - pending: ref(false), - logout: vi.fn(), - }) + const { user } = useAtproto() + user.value = createAtprotoUser('test-handle') mockUseProfileLikes.mockReturnValue({ data: ref({ records: [] }), diff --git a/test/nuxt/composables/use-command-palette-commands.spec.ts b/test/nuxt/composables/use-command-palette-commands.spec.ts index ae56784db3..5c365cabbb 100644 --- a/test/nuxt/composables/use-command-palette-commands.spec.ts +++ b/test/nuxt/composables/use-command-palette-commands.spec.ts @@ -1,7 +1,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { computed, defineComponent, h, ref, watchEffect, type Ref } from 'vue' import type { RouteLocationRaw } from 'vue-router' -import { mockNuxtImport, mountSuspended } from '@nuxt/test-utils/runtime' +import { mockNuxtImport, mountSuspended, registerEndpoint } from '@nuxt/test-utils/runtime' +import { useAtproto } from '~/composables/atproto/useAtproto' import { downloadPackageTarball } from '~/utils/package-download' import type { CommandPaletteCommand, @@ -23,9 +24,15 @@ const mockConnectorState = ref<{ npmUser: null, }) -const mockAtprotoHandle = ref(null) const mockDisconnectNpm = vi.fn() -const mockLogout = vi.fn(async () => {}) + +function createAtprotoUser(handle: string) { + return { + did: `did:plc:${handle}`, + handle, + pds: 'https://bsky.social', + } +} mockNuxtImport('useConnector', () => { return () => ({ @@ -35,13 +42,6 @@ mockNuxtImport('useConnector', () => { }) }) -mockNuxtImport('useAtproto', () => { - return () => ({ - user: computed(() => (mockAtprotoHandle.value ? { handle: mockAtprotoHandle.value } : null)), - logout: mockLogout, - }) -}) - async function captureCommandPalette(options?: { route?: string query?: string @@ -62,7 +62,8 @@ async function captureCommandPalette(options?: { connected: !!options?.npmUser, npmUser: options?.npmUser ?? null, } - mockAtprotoHandle.value = options?.atprotoHandle ?? null + const { user } = useAtproto() + user.value = options?.atprotoHandle ? createAtprotoUser(options.atprotoHandle) : null const WrapperComponent = defineComponent({ setup() { @@ -122,7 +123,8 @@ afterEach(() => { connected: false, npmUser: null, } - mockAtprotoHandle.value = null + const { user } = useAtproto() + user.value = null vi.clearAllMocks() }) @@ -312,6 +314,12 @@ describe('useCommandPaletteCommands', () => { }) it('adds atproto account commands and disconnect support when a profile is connected', async () => { + const deleteSession = vi.fn(() => null) + registerEndpoint('/api/auth/session', { + method: 'DELETE', + handler: deleteSession, + }) + const { wrapper, flatCommands } = await captureCommandPalette({ route: '/profile/alice.bsky.social', atprotoHandle: 'alice.bsky.social', @@ -324,7 +332,7 @@ describe('useCommandPaletteCommands', () => { await flatCommands.value.find(command => command.id === 'atproto-disconnect')?.action?.() - expect(mockLogout).toHaveBeenCalledTimes(1) + expect(deleteSession).toHaveBeenCalledTimes(1) expect(commandPalette.isOpen.value).toBe(false) wrapper.unmount() From 2163103586e9006cc7f00f892bcd18667f1b762f Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Mon, 25 May 2026 23:52:20 -0400 Subject: [PATCH 2/2] fix: don't compare nil user handle to profile handle --- app/pages/profile/[identity]/index.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/pages/profile/[identity]/index.vue b/app/pages/profile/[identity]/index.vue index 61ca0f69eb..4d63fd6ba8 100644 --- a/app/pages/profile/[identity]/index.vue +++ b/app/pages/profile/[identity]/index.vue @@ -25,7 +25,7 @@ if (!profile.value || profileError.value?.statusCode === 404) { }) } -const { user, pending: userPending } = useAtproto() +const { user } = useAtproto() const isEditing = ref(false) const displayNameInput = ref() const descriptionInput = ref() @@ -86,8 +86,8 @@ const showInviteSection = computed(() => { profile.value.recordExists === false && status.value === 'success' && !likes.value?.records?.length && - !userPending.value && - user.value?.handle !== profile.value.handle + user.value && + user.value.handle !== profile.value.handle ) })