5fcfb806be
CI / test (push) Has been cancelled
- 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>
107 lines
3.6 KiB
TypeScript
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)
|
|
})
|
|
})
|