Feed filter implementation
This commit is contained in:
@@ -25,8 +25,13 @@ describe('AppNav', () => {
|
||||
localStorage.setItem('user-id', '7')
|
||||
vi.clearAllMocks()
|
||||
|
||||
const { feeds, showMessage, message, showModal, viewMode, currentIndex, layout } = useFeeds()
|
||||
const { feeds, allItems, feedFilter, lastProgrammaticScroll, showMessage, message, showModal, viewMode, currentIndex, layout } = useFeeds()
|
||||
feeds.value = []
|
||||
allItems.value = []
|
||||
feedFilter.value = null
|
||||
// Module-singleton state: a prior test may have stamped this via a mocked
|
||||
// performance.now(); reset it so the scroll-driven reveal gate starts clean.
|
||||
lastProgrammaticScroll.value = 0
|
||||
showMessage.value = false
|
||||
message.value = ''
|
||||
showModal.value = false
|
||||
@@ -84,6 +89,17 @@ describe('AppNav', () => {
|
||||
expect(wrapper.find('.app-nav__menu').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('closes the menu on a click outside of it', async () => {
|
||||
const wrapper = await mountWithMenuOpen()
|
||||
expect(wrapper.find('.app-nav__menu').exists()).toBe(true)
|
||||
|
||||
// A click anywhere outside the menu strip (e.g. on page content) closes it.
|
||||
document.body.dispatchEvent(new Event('click', { bubbles: true }))
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.app-nav__menu').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('clears stored credentials and redirects to login on logout', async () => {
|
||||
const wrapper = await mountWithMenuOpen()
|
||||
|
||||
@@ -179,8 +195,10 @@ describe('AppNav', () => {
|
||||
})
|
||||
|
||||
it('shows the unread count in the title when there are articles', async () => {
|
||||
const { feeds } = useFeeds()
|
||||
feeds.value = [
|
||||
// The badge is the global unread total, sourced from the master list
|
||||
// (allItems) so it stays correct regardless of any active feed filter.
|
||||
const { allItems } = useFeeds()
|
||||
allItems.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' },
|
||||
]
|
||||
@@ -192,8 +210,8 @@ describe('AppNav', () => {
|
||||
})
|
||||
|
||||
it('excludes already-read articles from the counter while in article view', async () => {
|
||||
const { feeds } = useFeeds()
|
||||
feeds.value = [
|
||||
const { allItems } = useFeeds()
|
||||
allItems.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' },
|
||||
]
|
||||
@@ -211,6 +229,74 @@ describe('AppNav', () => {
|
||||
expect(wrapper.find('.app-nav__unread').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders a feed filter with an "All feeds" option plus one per distinct feed', async () => {
|
||||
const { allItems } = useFeeds()
|
||||
allItems.value = [
|
||||
{ id: 1, feedTitle: 'Feed B', title: 'a', url: 'https://example.test/1', timestamp: '2026-01-02' },
|
||||
{ id: 2, feedTitle: 'Feed A', title: 'b', url: 'https://example.test/2', timestamp: '2026-01-01' },
|
||||
{ id: 3, feedTitle: 'Feed A', title: 'c', url: 'https://example.test/3', timestamp: '2026-01-03' },
|
||||
]
|
||||
|
||||
const wrapper = mountNav()
|
||||
await flushPromises()
|
||||
|
||||
const options = wrapper.find('.app-nav__filter').findAll('option').map(o => o.text())
|
||||
// "All feeds" first, then distinct titles sorted alphabetically.
|
||||
expect(options).toEqual(['All feeds', 'Feed A', 'Feed B'])
|
||||
})
|
||||
|
||||
it('does not render the feed filter when there are no feeds', async () => {
|
||||
const wrapper = mountNav()
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.app-nav__filter').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('applies the selected feed to the displayed list, and "All feeds" restores it', async () => {
|
||||
const { allItems, feeds, feedFilter } = useFeeds()
|
||||
allItems.value = [
|
||||
{ id: 1, feedTitle: 'Feed A', title: 'a', url: 'https://example.test/1', timestamp: '2026-01-01' },
|
||||
{ id: 2, feedTitle: 'Feed B', title: 'b', url: 'https://example.test/2', timestamp: '2026-01-02' },
|
||||
]
|
||||
|
||||
const wrapper = mountNav()
|
||||
await flushPromises()
|
||||
|
||||
const select = wrapper.find('.app-nav__filter')
|
||||
await select.setValue('Feed A')
|
||||
await flushPromises()
|
||||
|
||||
expect(feedFilter.value).toBe('Feed A')
|
||||
expect(feeds.value.map(f => f.id)).toEqual([1])
|
||||
|
||||
await select.setValue('')
|
||||
await flushPromises()
|
||||
|
||||
expect(feedFilter.value).toBeNull()
|
||||
expect(feeds.value.map(f => f.id)).toEqual([1, 2])
|
||||
})
|
||||
|
||||
it('shows the selected feed\'s unread count in the title when a filter is active', async () => {
|
||||
const { allItems } = useFeeds()
|
||||
allItems.value = [
|
||||
{ id: 1, feedTitle: 'Feed A', title: 'a', url: 'https://example.test/1', timestamp: '2026-01-01' },
|
||||
{ id: 2, feedTitle: 'Feed A', title: 'b', url: 'https://example.test/2', timestamp: '2026-01-02' },
|
||||
{ id: 3, feedTitle: 'Feed B', title: 'c', url: 'https://example.test/3', timestamp: '2026-01-03' },
|
||||
]
|
||||
|
||||
const wrapper = mountNav()
|
||||
await flushPromises()
|
||||
|
||||
// No filter → global total (3).
|
||||
expect(wrapper.find('.app-nav__title').text()).toContain('(3)')
|
||||
|
||||
await wrapper.find('.app-nav__filter').setValue('Feed A')
|
||||
await flushPromises()
|
||||
|
||||
// Filtered → unread in Feed A only (2).
|
||||
expect(wrapper.find('.app-nav__title').text()).toContain('(2)')
|
||||
})
|
||||
|
||||
describe('scroll-driven show/hide', () => {
|
||||
// The scroll handler is rAF-throttled; run rAF synchronously so a single
|
||||
// dispatched scroll event resolves before we assert. Per the CLAUDE.md
|
||||
|
||||
Reference in New Issue
Block a user