mark all as read
This commit is contained in:
@@ -4,7 +4,7 @@ import { RouterLink, useRouter } from 'vue-router'
|
|||||||
import { useFeeds } from '@/composables/useFeeds'
|
import { useFeeds } from '@/composables/useFeeds'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { sync, showModal, viewMode, toggleViewMode } = useFeeds()
|
const { sync, showModal, viewMode, toggleViewMode, markAllRead } = useFeeds()
|
||||||
|
|
||||||
const menuOpen = ref(false)
|
const menuOpen = ref(false)
|
||||||
|
|
||||||
@@ -28,6 +28,11 @@ function handleSync() {
|
|||||||
closeMenu()
|
closeMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMarkAllRead() {
|
||||||
|
markAllRead()
|
||||||
|
closeMenu()
|
||||||
|
}
|
||||||
|
|
||||||
function openAddModal() {
|
function openAddModal() {
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
closeMenu()
|
closeMenu()
|
||||||
@@ -70,6 +75,7 @@ function handleToggleViewMode() {
|
|||||||
{{ viewMode === 'list' ? 'Article view' : 'List view' }}
|
{{ viewMode === 'list' ? 'Article view' : 'List view' }}
|
||||||
</button>
|
</button>
|
||||||
<button class="app-nav__menu-item" type="button" @click="handleSync">Sync</button>
|
<button class="app-nav__menu-item" type="button" @click="handleSync">Sync</button>
|
||||||
|
<button class="app-nav__menu-item" type="button" @click="handleMarkAllRead">Mark all as read</button>
|
||||||
<button class="app-nav__menu-item" type="button" @click="openAddModal">Add RSS</button>
|
<button class="app-nav__menu-item" type="button" @click="openAddModal">Add RSS</button>
|
||||||
<button class="app-nav__menu-item app-nav__logout" type="button" @click="logout">Logout</button>
|
<button class="app-nav__menu-item app-nav__logout" type="button" @click="logout">Logout</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ describe('AppNav', () => {
|
|||||||
localStorage.setItem('user-id', '7')
|
localStorage.setItem('user-id', '7')
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
|
||||||
const { feeds, showMessage, message, showModal } = useFeeds()
|
const { feeds, showMessage, message, showModal, viewMode, currentIndex } = useFeeds()
|
||||||
feeds.value = []
|
feeds.value = []
|
||||||
showMessage.value = false
|
showMessage.value = false
|
||||||
message.value = ''
|
message.value = ''
|
||||||
showModal.value = false
|
showModal.value = false
|
||||||
|
viewMode.value = 'list'
|
||||||
|
currentIndex.value = 0
|
||||||
|
|
||||||
router = createRouter({
|
router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
@@ -90,4 +92,57 @@ describe('AppNav', () => {
|
|||||||
|
|
||||||
expect(showModal.value).toBe(true)
|
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('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('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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,11 +23,13 @@ describe('RssFeeds', () => {
|
|||||||
|
|
||||||
// useFeeds() returns module-level singleton refs shared across the whole
|
// useFeeds() returns module-level singleton refs shared across the whole
|
||||||
// app (and this spec file) — reset them so state doesn't leak between tests.
|
// app (and this spec file) — reset them so state doesn't leak between tests.
|
||||||
const { feeds, showMessage, message, showModal } = useFeeds()
|
const { feeds, showMessage, message, showModal, viewMode, currentIndex } = useFeeds()
|
||||||
feeds.value = []
|
feeds.value = []
|
||||||
showMessage.value = false
|
showMessage.value = false
|
||||||
message.value = ''
|
message.value = ''
|
||||||
showModal.value = false
|
showModal.value = false
|
||||||
|
viewMode.value = 'list'
|
||||||
|
currentIndex.value = 0
|
||||||
})
|
})
|
||||||
|
|
||||||
it('fetches the current user articles and shows the empty state', async () => {
|
it('fetches the current user articles and shows the empty state', async () => {
|
||||||
@@ -177,7 +179,9 @@ describe('RssFeeds', () => {
|
|||||||
const wrapper = mount(RssFeeds)
|
const wrapper = mount(RssFeeds)
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
await wrapper.find('.view-toggle__btn').trigger('click')
|
// The view-toggle button now lives in AppNav's hamburger menu, not here —
|
||||||
|
// switch modes directly through the shared composable, as AppNav would.
|
||||||
|
useFeeds().toggleViewMode()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('.article-single .feed-title').text()).toBe('Article one')
|
expect(wrapper.find('.article-single .feed-title').text()).toBe('Article one')
|
||||||
|
|||||||
@@ -159,6 +159,18 @@ function setInitialLoad(value) {
|
|||||||
initialLoad = value
|
initialLoad = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function markAllRead() {
|
||||||
|
if (feeds.value.length === 0) return
|
||||||
|
if (!window.confirm('Mark all articles as read?')) return
|
||||||
|
|
||||||
|
const ids = feeds.value.map(feed => feed.id)
|
||||||
|
feeds.value = []
|
||||||
|
currentIndex.value = 0
|
||||||
|
// markRead swallows its own errors, so Promise.all can't reject here.
|
||||||
|
await Promise.all(ids.map(id => markRead(id)))
|
||||||
|
showMessageForXSeconds('All articles marked as read.', 5)
|
||||||
|
}
|
||||||
|
|
||||||
function markCurrentArticleRead() {
|
function markCurrentArticleRead() {
|
||||||
const feed = feeds.value[currentIndex.value]
|
const feed = feeds.value[currentIndex.value]
|
||||||
// Marking read here (rather than via removeFeed, as the scroll-based list
|
// Marking read here (rather than via removeFeed, as the scroll-based list
|
||||||
@@ -203,6 +215,7 @@ export function useFeeds() {
|
|||||||
sync,
|
sync,
|
||||||
getReadable,
|
getReadable,
|
||||||
markRead,
|
markRead,
|
||||||
|
markAllRead,
|
||||||
showMessageForXSeconds,
|
showMessageForXSeconds,
|
||||||
setupIntersectionObserver,
|
setupIntersectionObserver,
|
||||||
removeFeed,
|
removeFeed,
|
||||||
|
|||||||
Reference in New Issue
Block a user