full text
import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import axios from 'axios' import RssFeeds from '../RssFeeds.vue' import { useFeeds } from '../../composables/useFeeds' vi.mock('axios') // jsdom does not implement IntersectionObserver, but the component sets one up // once the feed list has rendered. class FakeIntersectionObserver { observe() {} unobserve() {} disconnect() {} } vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver) describe('RssFeeds', () => { beforeEach(() => { localStorage.setItem('user-token', 'test-token') localStorage.setItem('user-id', '7') vi.clearAllMocks() // useFeeds() returns module-level singleton refs shared across the whole // app (and this spec file) — reset them so state doesn't leak between tests. 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' }) it('fetches the current user articles and shows the empty state', async () => { axios.get.mockResolvedValueOnce({ data: { feeds: [] } }) const wrapper = mount(RssFeeds) await flushPromises() expect(axios.get).toHaveBeenCalledWith('/api/v1/article/get/7', expect.anything()) expect(wrapper.text()).toContain('All caught up') }) it('renders the fetched feed items', async () => { axios.get.mockResolvedValueOnce({ data: { feeds: [ { title: 'My Feed', items: [ { id: 1, title: 'Article one', content: '
hello
', url: 'https://example.test/1', timestamp: '2026-01-01', }, ], }, ], }, }) const wrapper = mount(RssFeeds) await flushPromises() expect(wrapper.text()).toContain('Article one') expect(wrapper.text()).toContain('My Feed') expect(wrapper.text()).not.toContain('All caught up') }) it('renders the list as cards when the card layout is selected', async () => { axios.get.mockResolvedValueOnce({ data: { feeds: [ { title: 'My Feed', items: [ { id: 1, title: 'Article one', content: 'hello
', url: 'https://example.test/1', timestamp: '2026-01-01', }, ], }, ], }, }) const { layout } = useFeeds() layout.value = 'cards' const wrapper = mount(RssFeeds) await flushPromises() expect(wrapper.find('.article').classes()).toContain('article--cards') }) it('shows the full article content in cards with no truncation, growing the card to fit', async () => { axios.get.mockResolvedValueOnce({ data: { feeds: [ { title: 'My Feed', items: [ { id: 1, title: 'Article one', content: '
full text
old
', url: 'https://example.test/1', timestamp: '2026-01-01 10:00:00', }, ], }, { title: 'New Feed', items: [ { id: 2, title: 'Newer article', content: 'new
', url: 'https://example.test/2', timestamp: '2026-02-01 10:00:00', }, ], }, ], }, }) const wrapper = mount(RssFeeds) await flushPromises() const titles = wrapper.findAll('.feed-title').map(el => el.text()) expect(titles).toEqual(['Newer article', 'Older article']) }) it('keeps a link to the original article visible after the readable version is loaded', async () => { axios.get.mockResolvedValueOnce({ data: { feeds: [ { title: 'My Feed', items: [ { id: 1, title: 'Article one', content: 'short summary
', url: 'https://example.test/1', timestamp: '2026-01-01', }, ], }, ], }, }) // axios.post is also hit by the sync triggered on mount, so branch on the // URL rather than relying on call order via `mockResolvedValueOnce`. axios.post.mockImplementation((url) => { if (url === '/api/v1/article/sync') { return Promise.resolve({ status: 200 }) } return Promise.resolve({ data: { content: 'full text
one
', url: 'https://example.test/1', timestamp: '2026-02-01 10:00:00', }, { id: 2, title: 'Article two', content: 'two
', url: 'https://example.test/2', timestamp: '2026-01-01 10:00:00', }, ], }, ], }, }) axios.put.mockResolvedValue({ status: 200 }) axios.post.mockResolvedValue({ data: { content: 'full text
one
', url: 'https://example.test/1', timestamp: '2026-03-01 10:00:00', }, { id: 2, title: 'Article two', content: 'two
', url: 'https://example.test/2', timestamp: '2026-02-01 10:00:00', }, { id: 3, title: 'Article three', content: 'three
', url: 'https://example.test/3', timestamp: '2026-01-01 10:00:00', }, ], }, ], }, }) axios.put.mockResolvedValue({ status: 200 }) const wrapper = mount(RssFeeds) await flushPromises() const { toggleViewMode, leaveArticleView } = useFeeds() // Enter article view (marks "Article one" read), page forward to "Article // two" (marks it read too), then leave without visiting "Article three". toggleViewMode() await flushPromises() await wrapper.findAll('.article-nav__btn')[1].trigger('click') await flushPromises() leaveArticleView() await flushPromises() const titles = wrapper.findAll('.feed-title').map(el => el.text()) expect(titles).toEqual(['Article three']) }) })