import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { createRouter, createWebHistory } from 'vue-router' import axios from 'axios' import AppNav from '../AppNav.vue' import { useFeeds } from '../../composables/useFeeds' vi.mock('axios') describe('AppNav', () => { let router beforeEach(async () => { localStorage.setItem('user-token', 'abc123') localStorage.setItem('user-id', '7') vi.clearAllMocks() const { feeds, showMessage, message, showModal, viewMode, currentIndex, layout } = useFeeds() feeds.value = [] showMessage.value = false message.value = '' showModal.value = false viewMode.value = 'list' currentIndex.value = 0 layout.value = 'list' router = createRouter({ history: createWebHistory(), routes: [ { path: '/login', name: 'login', component: { template: '
' } }, { path: '/feeds', name: 'feeds', component: { template: '' } }, ], }) router.push('/feeds') await router.isReady() }) async function mountWithMenuOpen() { const wrapper = mount(AppNav, { global: { plugins: [router] } }) await wrapper.find('.app-nav__hamburger').trigger('click') await flushPromises() return wrapper } it('toggles the menu open and closed via the hamburger button', async () => { const wrapper = mount(AppNav, { global: { plugins: [router] } }) expect(wrapper.find('.app-nav__menu').exists()).toBe(false) await wrapper.find('.app-nav__hamburger').trigger('click') expect(wrapper.find('.app-nav__menu').exists()).toBe(true) await wrapper.find('.app-nav__hamburger').trigger('click') expect(wrapper.find('.app-nav__menu').exists()).toBe(false) }) it('clears stored credentials and redirects to login on logout', async () => { const wrapper = await mountWithMenuOpen() await wrapper.find('.app-nav__logout').trigger('click') await flushPromises() expect(localStorage.getItem('user-token')).toBeNull() expect(localStorage.getItem('user-id')).toBeNull() expect(router.currentRoute.value.name).toBe('login') }) it('triggers a sync from the menu', async () => { axios.get.mockResolvedValue({ data: { feeds: [] } }) axios.post.mockResolvedValueOnce({ status: 200 }) const wrapper = await mountWithMenuOpen() const syncButton = wrapper.findAll('.app-nav__menu-item').find(el => el.text() === 'Sync') await syncButton.trigger('click') await flushPromises() expect(axios.post).toHaveBeenCalledWith( '/api/v1/article/sync', { user_id: 7 }, expect.anything(), ) // Menu auto-closes after an action expect(wrapper.find('.app-nav__menu').exists()).toBe(false) }) it('opens the add-feed modal from the menu', async () => { const wrapper = await mountWithMenuOpen() const { showModal } = useFeeds() const addButton = wrapper.findAll('.app-nav__menu-item').find(el => el.text() === 'Add RSS') await addButton.trigger('click') expect(showModal.value).toBe(true) }) it('switches the view mode from the menu and closes it', async () => { const wrapper = await mountWithMenuOpen() const { viewMode } = useFeeds() const viewButton = wrapper.findAll('.app-nav__menu-item').find(el => el.text() === 'Article view') await viewButton.trigger('click') expect(viewMode.value).toBe('article') expect(wrapper.find('.app-nav__menu').exists()).toBe(false) }) it('switches the list layout from the menu and closes it', async () => { const wrapper = await mountWithMenuOpen() const { layout } = useFeeds() const layoutButton = wrapper.findAll('.app-nav__menu-item').find(el => el.text() === 'Card layout') await layoutButton.trigger('click') expect(layout.value).toBe('cards') expect(wrapper.find('.app-nav__menu').exists()).toBe(false) }) it('hides the layout toggle while in article view', async () => { const { viewMode } = useFeeds() viewMode.value = 'article' const wrapper = await mountWithMenuOpen() expect(wrapper.findAll('.app-nav__menu-item').find(el => el.text().includes('layout'))).toBeUndefined() }) it('marks all articles as read from the menu after confirmation', async () => { const { feeds } = useFeeds() feeds.value = [ { id: 1, title: 'Article one', content: '', url: 'https://example.test/1', timestamp: '2026-01-01' }, { id: 2, title: 'Article two', content: '', url: 'https://example.test/2', timestamp: '2026-01-02' }, ] axios.put.mockResolvedValue({ status: 200 }) const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true) const wrapper = await mountWithMenuOpen() const markAllButton = wrapper.findAll('.app-nav__menu-item').find(el => el.text() === 'Mark all as read') await markAllButton.trigger('click') await flushPromises() expect(confirmSpy).toHaveBeenCalled() expect(axios.put).toHaveBeenCalledWith('/api/v1/article/read/1', null, expect.anything()) expect(axios.put).toHaveBeenCalledWith('/api/v1/article/read/2', null, expect.anything()) expect(feeds.value).toHaveLength(0) expect(wrapper.find('.app-nav__menu').exists()).toBe(false) confirmSpy.mockRestore() }) it('shows the unread count in the title when there are articles', async () => { const { feeds } = useFeeds() feeds.value = [ { id: 1, title: 'Article one', content: '', url: 'https://example.test/1', timestamp: '2026-01-01' }, { id: 2, title: 'Article two', content: '', url: 'https://example.test/2', timestamp: '2026-01-02' }, ] const wrapper = mount(AppNav, { global: { plugins: [router] } }) await flushPromises() expect(wrapper.find('.app-nav__title').text()).toContain('(2)') }) it('excludes already-read articles from the counter while in article view', async () => { const { feeds } = useFeeds() feeds.value = [ { id: 1, title: 'Article one', read: true, content: '', url: 'https://example.test/1', timestamp: '2026-01-01' }, { id: 2, title: 'Article two', read: false, content: '', url: 'https://example.test/2', timestamp: '2026-01-02' }, ] const wrapper = mount(AppNav, { global: { plugins: [router] } }) await flushPromises() expect(wrapper.find('.app-nav__title').text()).toContain('(1)') }) it('hides the unread count when there are no articles', async () => { const wrapper = mount(AppNav, { global: { plugins: [router] } }) await flushPromises() expect(wrapper.find('.app-nav__unread').exists()).toBe(false) }) it('does not mark articles as read when the confirmation is dismissed', async () => { const { feeds } = useFeeds() feeds.value = [ { id: 1, title: 'Article one', content: '', url: 'https://example.test/1', timestamp: '2026-01-01' }, ] const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false) const wrapper = await mountWithMenuOpen() const markAllButton = wrapper.findAll('.app-nav__menu-item').find(el => el.text() === 'Mark all as read') await markAllButton.trigger('click') await flushPromises() expect(confirmSpy).toHaveBeenCalled() expect(axios.put).not.toHaveBeenCalled() expect(feeds.value).toHaveLength(1) confirmSpy.mockRestore() }) })