Files
pictureFrame-webApp/frontend/src/test/components/BottomNav.test.ts
T
football2801 5fcfb806be
CI / test (push) Has been cancelled
feat: ship as a real iOS-installable PWA, restructure bottom nav, fix safe-area
- Add manifest.webmanifest with standalone display + warm-craft theme colors,
  apple-touch-icon, and 192/512/512-maskable icons (frame-with-sunset glyph).
- Add PWA meta tags + viewport-fit=cover so add-to-home-screen produces a
  true standalone app on iOS instead of a Safari bookmark.
- Drop the Shared bottom-nav tab; the in-page sub-tabs already cover that.
  Three nav tabs total (Home / Library / Settings); pending-share badge
  moves to the Library tab. Predicate-based isActive() now correctly
  disambiguates /library vs /library?tab=shared.
- Safe-area handling: bottom nav, bottom sheet, upload overlay, and #app
  respect env(safe-area-inset-*); sticky Library tabs anchor below the
  iPhone status bar. Introduces --bottom-nav-height token consumed by
  Settings, Library, and the toast.
- LibraryView reactively follows route.query.tab so deep-linking
  /library?tab=shared lands on the right sub-tab.
- Theme-color meta syncs client-side via useTheme.applyTheme so the
  user's chosen theme follows them into Android Chrome's chrome bar.

Test suite expanded to 278 tests / 100% line coverage (99.84% statements,
99.78% branches). Remaining gaps are unreachable defensive code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:07:16 -04:00

107 lines
3.6 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { setActivePinia, createPinia } from 'pinia'
import { createRouter, createMemoryHistory, type Router } from 'vue-router'
import BottomNav from '@/components/BottomNav.vue'
import { useImagesStore } from '@/stores/images'
const Stub = { template: '<div />' }
function makeRouter(): Router {
return createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: Stub },
{ path: '/library', component: Stub },
{ path: '/settings', component: Stub },
],
})
}
async function mountAt(router: Router, path: string) {
await router.push(path)
await router.isReady()
return mount(BottomNav, { global: { plugins: [router] } })
}
function activeTabName(wrapper: ReturnType<typeof mount>): string | undefined {
const active = wrapper.find('.bottom-nav__tab--active')
if (!active.exists()) return undefined
return active.find('.bottom-nav__label').text()
}
describe('BottomNav', () => {
let router: Router
beforeEach(() => {
setActivePinia(createPinia())
router = makeRouter()
})
it('renders three tabs (Home, Library, Settings)', async () => {
const wrapper = await mountAt(router, '/')
const labels = wrapper.findAll('.bottom-nav__label').map(n => n.text())
expect(labels).toEqual(['Home', 'Library', 'Settings'])
})
it('marks Home active on /', async () => {
const wrapper = await mountAt(router, '/')
expect(activeTabName(wrapper)).toBe('Home')
})
it('marks Library active on /library', async () => {
const wrapper = await mountAt(router, '/library')
expect(activeTabName(wrapper)).toBe('Library')
})
it('marks Library active on /library?tab=shared', async () => {
const wrapper = await mountAt(router, '/library?tab=shared')
expect(activeTabName(wrapper)).toBe('Library')
})
it('marks Settings active on /settings', async () => {
const wrapper = await mountAt(router, '/settings')
expect(activeTabName(wrapper)).toBe('Settings')
})
it('does not mark Home active on /library (regression: startsWith bug)', async () => {
const wrapper = await mountAt(router, '/library')
const homeTab = wrapper.findAll('.bottom-nav__tab')
.find(t => t.find('.bottom-nav__label').text() === 'Home')
expect(homeTab?.classes()).not.toContain('bottom-nav__tab--active')
})
it('updates active tab when route changes', async () => {
const wrapper = await mountAt(router, '/')
expect(activeTabName(wrapper)).toBe('Home')
await router.push('/library')
await flushPromises()
expect(activeTabName(wrapper)).toBe('Library')
})
it('shows the pending-share count as a badge on the Library tab', async () => {
const images = useImagesStore()
images.pendingCount = 3
const wrapper = await mountAt(router, '/')
const libraryTab = wrapper.findAll('.bottom-nav__tab')
.find(t => t.find('.bottom-nav__label').text() === 'Library')!
const badge = libraryTab.find('.bottom-nav__badge')
expect(badge.exists()).toBe(true)
expect(badge.text()).toBe('3')
})
it('clamps the badge count to "9+" when 10 or more are pending', async () => {
const images = useImagesStore()
images.pendingCount = 12
const wrapper = await mountAt(router, '/')
expect(wrapper.find('.bottom-nav__badge').text()).toBe('9+')
})
it('hides the badge when no shares are pending', async () => {
const images = useImagesStore()
images.pendingCount = 0
const wrapper = await mountAt(router, '/')
expect(wrapper.find('.bottom-nav__badge').exists()).toBe(false)
})
})