fix sync missing articles

This commit is contained in:
2026-06-10 06:08:17 +02:00
parent 972e967432
commit 0420cf0dd5
3 changed files with 48 additions and 140 deletions
+26 -1
View File
@@ -12,7 +12,7 @@ class FakeIntersectionObserver {
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
describe('useFeeds', () => {
const { feeds, showMessage, message, showModal, fetchData, sync, getReadable } = useFeeds()
const { feeds, showMessage, message, showModal, fetchData, sync, getReadable, setInitialLoad, handleIntersection } = useFeeds()
beforeEach(() => {
localStorage.setItem('user-token', 'test-token')
@@ -91,6 +91,31 @@ describe('useFeeds', () => {
expect(axios.get).toHaveBeenCalledWith('/api/v1/article/get/7', expect.anything())
})
it('marks the correct articles read when several scroll out of view in one batch', async () => {
feeds.value = [
{ id: 101, title: 'First' },
{ id: 102, title: 'Second' },
{ id: 103, title: 'Third' },
]
setInitialLoad(true)
axios.put.mockResolvedValue({ status: 200 })
// Both the first and second articles scrolled above the viewport in the
// same IntersectionObserver callback — their `target.id` reflects their
// original render-time indices (0 and 1).
await handleIntersection([
{ isIntersecting: false, boundingClientRect: { y: -10 }, target: { id: '0' } },
{ isIntersecting: false, boundingClientRect: { y: -5 }, target: { id: '1' } },
])
expect(axios.put).toHaveBeenCalledWith('/api/v1/article/read/101', null, expect.anything())
expect(axios.put).toHaveBeenCalledWith('/api/v1/article/read/102', null, expect.anything())
expect(axios.put).not.toHaveBeenCalledWith('/api/v1/article/read/103', null, expect.anything())
expect(feeds.value).toEqual([{ id: 103, title: 'Third' }])
setInitialLoad(false)
})
it('resolves Deutsche-Welle-style templated image URLs from data-format/data-url', async () => {
feeds.value = [{
id: 1,
+20 -16
View File
@@ -1,4 +1,4 @@
import { ref, unref, nextTick } from 'vue';
import { ref, nextTick } from 'vue';
import axios from 'axios';
import { Readability } from '@mozilla/readability';
@@ -155,21 +155,25 @@ function setupIntersectionObserver() {
}
async function handleIntersection(entries) {
// The callback function for when the target element enters or exits the viewport
for (const entry of entries) {
// An article that has scrolled above the viewport (not intersecting,
// bounding box above the top edge) has been read — mark it and remove it.
if (initialLoad === true && !entry.isIntersecting && entry.boundingClientRect.y < 0) {
await markRead(feeds.value[entry.target.id].id)
removeFeed(entry.target.id)
document.getElementById(0)?.scrollIntoView()
}
}
}
// An article that has scrolled above the viewport (not intersecting,
// bounding box above the top edge) has been read. Resolve all affected
// feeds up front, before any removal — splicing `feeds` while iterating
// would shift the array indices that later entries' `target.id` refer to,
// causing the wrong item to be marked read and removed.
const readFeeds = entries
.filter(entry => initialLoad === true && !entry.isIntersecting && entry.boundingClientRect.y < 0)
.map(entry => feeds.value[entry.target.id])
.filter(Boolean)
function removeFeed(index) {
const array = unref(feeds);
array.splice(index, 1);
if (readFeeds.length === 0) return
for (const feed of readFeeds) {
await markRead(feed.id)
}
const readIds = new Set(readFeeds.map(feed => feed.id))
feeds.value = feeds.value.filter(feed => !readIds.has(feed.id))
document.getElementById(0)?.scrollIntoView()
}
function setInitialLoad(value) {
@@ -263,7 +267,7 @@ export function useFeeds() {
markAllRead,
showMessageForXSeconds,
setupIntersectionObserver,
removeFeed,
setInitialLoad,
handleIntersection,
}
}