variable header size

This commit is contained in:
2026-07-02 22:24:01 +02:00
parent fe0adcf68e
commit b3cf5e4787
4 changed files with 336 additions and 10 deletions
+32 -1
View File
@@ -1,4 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { flushPromises } from '@vue/test-utils'
import axios from 'axios'
import { useFeeds } from '../useFeeds'
@@ -12,7 +13,7 @@ class FakeIntersectionObserver {
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
describe('useFeeds', () => {
const { feeds, showMessage, message, showModal, fetchData, sync, getReadable, setInitialLoad, handleIntersection } = useFeeds()
const { feeds, showMessage, message, showModal, fetchData, sync, getReadable, setInitialLoad, handleIntersection, consumeScrollCorrection } = useFeeds()
beforeEach(() => {
localStorage.setItem('user-token', 'test-token')
@@ -116,6 +117,36 @@ describe('useFeeds', () => {
setInitialLoad(false)
})
it('flags a scroll correction when marking an article read repositions the list', async () => {
// Regression coverage for the header-compacting glitch: AppNav's scroll
// handler needs a way to tell "the page moved because content was
// removed" apart from a real user scroll. jsdom's getBoundingClientRect()
// defaults every element's top to 0, so any topbarHeight > 0 exercises
// the same "first article now sits above the header" branch that fires
// in a real browser.
feeds.value = [{ id: 101, title: 'First' }, { id: 102, title: 'Second' }]
setInitialLoad(true)
axios.put.mockResolvedValue({ status: 200 })
const observeDiv = document.createElement('div')
observeDiv.className = 'observe'
document.body.appendChild(observeDiv)
expect(consumeScrollCorrection()).toBe(false)
await handleIntersection([
{ isIntersecting: false, boundingClientRect: { y: -10 }, target: { id: '0' } },
], 60)
await flushPromises()
expect(consumeScrollCorrection()).toBe(true)
// Consume-once: asking again without another correction reports false.
expect(consumeScrollCorrection()).toBe(false)
document.body.removeChild(observeDiv)
setInitialLoad(false)
})
it('strips leftover embedded-video placeholder headings', async () => {
feeds.value = [{
id: 1,
+20
View File
@@ -14,6 +14,13 @@ const layout = ref(localStorage.getItem('layout') || 'list') // 'list' | 'cards'
let observer; // Declare observer outside the setup function
let initialLoad = false
// Set right before the scroll-position correction below, so AppNav's
// scroll-driven header compacting can tell "the page just moved because
// content was removed" apart from a real user scroll — otherwise every
// article marked read (which resets scrollY toward the top of the
// now-shorter list) would look identical to the user manually scrolling
// back to the top.
let scrollCorrectionPending = false
export function authHeaders() {
return {
@@ -265,6 +272,7 @@ function handleIntersection(entries, topbarHeight = 0) {
if (first) {
const top = first.getBoundingClientRect().top
if (top < topbarHeight) {
scrollCorrectionPending = true
window.scrollBy(0, top - topbarHeight)
}
}
@@ -272,6 +280,17 @@ function handleIntersection(entries, topbarHeight = 0) {
})
}
// Consume-once: returns whether a scroll-position correction is pending
// (and clears it), so a caller can tell this scroll event apart from a
// real user scroll without the flag lingering into later, unrelated ones.
function consumeScrollCorrection() {
if (scrollCorrectionPending) {
scrollCorrectionPending = false
return true
}
return false
}
function disconnectObserver() {
if (observer) {
observer.disconnect()
@@ -386,6 +405,7 @@ export function useFeeds() {
markAllRead,
showMessageForXSeconds,
setupIntersectionObserver,
consumeScrollCorrection,
disconnectObserver,
setInitialLoad,
handleIntersection,