Ctrl K
ComunidadGuías…

web/src/App.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { shallowMount, flushPromises } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'

// ── Mocks (hoisted before imports) ───────────────────────────────────────────

vi.mock('@/utils/axios', () => ({
  default: {
    get: vi.fn().mockResolvedValue({ data: { count: 0 } }),
    post: vi.fn().mockResolvedValue({}),
    defaults: { headers: { common: {} } },
  },
}))

vi.mock('@/utils/offlineStorage', () => ({
  addToPendingSync: vi.fn(),
  syncWithServer: vi.fn(),
  getPendingSync: vi.fn().mockResolvedValue([]),
}))

vi.mock('@/utils/timezone', () => ({
  formatDateInTimezone: vi.fn(() => 'Fri, Apr 3, 2026'),
}))

vi.mock('@/utils/url', () => ({
  getProfileImageUrl: vi.fn((p) => p),
}))

// vue-router — use a mutable plain object so computed properties in App.vue
// read the raw string value (not a ref) and react when we change it per-test.
const mockRoute = { name: 'dashboard', path: '/dashboard' }
vi.mock('vue-router', async () => {
  const actual = await vi.importActual('vue-router')
  return {
    ...actual,
    useRoute: vi.fn(() => mockRoute),
    useRouter: vi.fn(() => ({ push: vi.fn() })),
    RouterLink: { template: '<a><slot /></a>' },
    RouterView: { template: '<div />' },
  }
})

// Vuetify useTheme
vi.mock('vuetify', async () => {
  const actual = await vi.importActual('vuetify')
  return {
    ...actual,
    useTheme: vi.fn(() => ({ global: { name: { value: 'light' } } })),
  }
})

// Auth store
const mockAuthStore = {
  isAuthenticated: false,
  user: null,
  logoPath: '/logo.svg',
}
vi.mock('@/stores/auth', () => ({ useAuthStore: () => mockAuthStore }))

// Network store
const mockNetworkStore = {
  isOnline: true,
  initNetworkListeners: vi.fn(),
  incrementPendingSync: vi.fn(),
}
vi.mock('@/stores/network', () => ({ useNetworkStore: () => mockNetworkStore }))

// Subscription store
const mockSubscriptionStore = {
  isExpired: false,
  fetchStatus: vi.fn().mockResolvedValue({}),
}
vi.mock('@/stores/subscription', () => ({ useSubscriptionStore: () => mockSubscriptionStore }))

// Settings store
const mockSettingsStore = { timezone: 'UTC' }
vi.mock('@/stores/settings', () => ({ useSettingsStore: () => mockSettingsStore }))

// Child components
vi.mock('@/components/InstallPrompt.vue', () => ({ default: { template: '<div />' } }))
vi.mock('@/components/UpdatePrompt.vue', () => ({ default: { template: '<div />' } }))
vi.mock('@/components/SubscriptionExpiredBanner.vue', () => ({ default: { template: '<div />' } }))
vi.mock('@/components/ThemeSwitcher.vue', () => ({ default: { template: '<div />' } }))

import App from './App.vue'
import axios from '@/utils/axios'

// ── Helpers ───────────────────────────────────────────────────────────────────

const createWrapper = () => {
  const pinia = createPinia()
  setActivePinia(pinia)
  return shallowMount(App, { global: { plugins: [pinia] } })
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('App.vue', () => {
  let wrapper

  beforeEach(() => {
    vi.useFakeTimers()
    vi.clearAllMocks()
    axios.get.mockResolvedValue({ data: { count: 0 } })
    mockAuthStore.isAuthenticated = false
    mockAuthStore.user = null
    mockRoute.name = 'dashboard'
    mockNetworkStore.isOnline = true
  })

  afterEach(async () => {
    if (wrapper) {
      wrapper.unmount()
      wrapper = null
    }
    vi.useRealTimers()
    await flushPromises()
  })

  // =========================================================================
  // showAppBar computed
  // =========================================================================

  describe('showAppBar', () => {
    it('is false when not authenticated', async () => {
      mockAuthStore.isAuthenticated = false
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.showAppBar).toBe(false)
    })

    it('is false on the login route even when authenticated', async () => {
      mockAuthStore.isAuthenticated = true
      mockRoute.name = 'login'
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.showAppBar).toBe(false)
    })

    it('is false on the register route', async () => {
      mockAuthStore.isAuthenticated = true
      mockRoute.name = 'register'
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.showAppBar).toBe(false)
    })

    it('is true when authenticated and on a protected route', async () => {
      mockAuthStore.isAuthenticated = true
      mockRoute.name = 'dashboard'
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.showAppBar).toBe(true)
    })
  })

  // =========================================================================
  // showBottomNav computed
  // =========================================================================

  describe('showBottomNav', () => {
    it('mirrors showAppBar — false when not authenticated', async () => {
      mockAuthStore.isAuthenticated = false
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.showBottomNav).toBe(false)
    })

    it('mirrors showAppBar — true when authenticated on dashboard', async () => {
      mockAuthStore.isAuthenticated = true
      mockRoute.name = 'dashboard'
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.showBottomNav).toBe(true)
    })
  })

  // =========================================================================
  // mainStyle computed
  // =========================================================================

  describe('mainStyle', () => {
    it('has paddingTop 0 when not showing app bar', async () => {
      mockAuthStore.isAuthenticated = false
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.mainStyle.paddingTop).toBe('0')
    })

    it('uses CSS variable for paddingTop when showing app bar', async () => {
      mockAuthStore.isAuthenticated = true
      mockRoute.name = 'dashboard'
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.mainStyle.paddingTop).toBe('var(--v-layout-top, 56px)')
    })

    it('has paddingBottom 0 when not showing bottom nav', async () => {
      mockAuthStore.isAuthenticated = false
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.mainStyle.paddingBottom).toBe('0')
    })

    it('uses CSS variable with safe-area for paddingBottom when showing bottom nav', async () => {
      mockAuthStore.isAuthenticated = true
      mockRoute.name = 'dashboard'
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.mainStyle.paddingBottom).toContain('var(--v-layout-bottom, 56px)')
      expect(wrapper.vm.mainStyle.paddingBottom).toContain('safe-area-inset-bottom')
    })

    it('always sets minHeight and overflowX', async () => {
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.mainStyle.minHeight).toBe('100%')
      expect(wrapper.vm.mainStyle.maxWidth).toBe('100vw')
      expect(wrapper.vm.mainStyle.overflowX).toBe('hidden')
    })
  })

  // =========================================================================
  // Notification badge count
  // =========================================================================

  describe('unreadNotificationCount', () => {
    it('starts at 0', async () => {
      wrapper = createWrapper()
      await flushPromises()

      expect(wrapper.vm.unreadNotificationCount).toBe(0)
    })

    it('fetches count from API when authenticated on mount', async () => {
      mockAuthStore.isAuthenticated = true
      axios.get.mockResolvedValue({ data: { count: 5 } })

      wrapper = createWrapper()
      await flushPromises()

      expect(axios.get).toHaveBeenCalledWith('/api/notifications/count')
      expect(wrapper.vm.unreadNotificationCount).toBe(5)
    })

    it('does not fetch when not authenticated', async () => {
      mockAuthStore.isAuthenticated = false
      wrapper = createWrapper()
      await flushPromises()

      expect(axios.get).not.toHaveBeenCalledWith('/api/notifications/count')
      expect(wrapper.vm.unreadNotificationCount).toBe(0)
    })
  })

  // =========================================================================
  // provide — refreshNotificationCount
  // =========================================================================

  describe('provide refreshNotificationCount', () => {
    it('re-fetches count when fetchUnreadNotificationCount is called directly', async () => {
      mockAuthStore.isAuthenticated = true
      axios.get
        .mockResolvedValueOnce({ data: { count: 3 } }) // initial fetch on mount
        .mockResolvedValueOnce({ data: { count: 0 } }) // after explicit refresh

      wrapper = createWrapper()
      await flushPromises()
      expect(wrapper.vm.unreadNotificationCount).toBe(3)

      // Call the internal function directly (same one exposed via provide)
      await wrapper.vm.fetchUnreadNotificationCount?.()
      await flushPromises()

      // If fetchUnreadNotificationCount is exposed, count updates;
      // either way the API must have been called twice
      expect(axios.get).toHaveBeenCalledTimes(2)
    })
  })
})