Fix fontend tests, move next button in list view
This commit is contained in:
@@ -41,7 +41,7 @@ function scrollToNextArticle() {
|
|||||||
const SMALL_IMAGE_THRESHOLD = 200
|
const SMALL_IMAGE_THRESHOLD = 200
|
||||||
|
|
||||||
function markSmallImages() {
|
function markSmallImages() {
|
||||||
document.querySelectorAll('.article-feature__content--readable img').forEach(img => {
|
document.querySelectorAll('.article-feature__content--readable img, .feed-content--readable img').forEach(img => {
|
||||||
const checkSize = () => {
|
const checkSize = () => {
|
||||||
if (img.naturalWidth && img.naturalWidth <= SMALL_IMAGE_THRESHOLD) {
|
if (img.naturalWidth && img.naturalWidth <= SMALL_IMAGE_THRESHOLD) {
|
||||||
img.classList.add('article-feature__image--small')
|
img.classList.add('article-feature__image--small')
|
||||||
@@ -60,6 +60,12 @@ watch(() => feeds.value[currentIndex.value]?.content, async () => {
|
|||||||
markSmallImages()
|
markSmallImages()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function loadReadable(feed, index) {
|
||||||
|
await getReadable(feed, index)
|
||||||
|
await nextTick()
|
||||||
|
markSmallImages()
|
||||||
|
}
|
||||||
|
|
||||||
async function shareUrl(url) {
|
async function shareUrl(url) {
|
||||||
if (navigator.share) {
|
if (navigator.share) {
|
||||||
await navigator.share({ url })
|
await navigator.share({ url })
|
||||||
@@ -86,7 +92,6 @@ onMounted(async () => {
|
|||||||
<div v-if="viewMode === 'list'" id='article' class='article' :class="{ 'article--cards': layout === 'cards' }">
|
<div v-if="viewMode === 'list'" id='article' class='article' :class="{ 'article--cards': layout === 'cards' }">
|
||||||
<div v-if="feeds.length" class="list-topbar">
|
<div v-if="feeds.length" class="list-topbar">
|
||||||
<span v-if="!navTitleVisible" class="list-topbar__title">RSS Reader<span v-if="unreadCount" class="list-topbar__unread"> ({{ unreadCount }})</span></span>
|
<span v-if="!navTitleVisible" class="list-topbar__title">RSS Reader<span v-if="unreadCount" class="list-topbar__unread"> ({{ unreadCount }})</span></span>
|
||||||
<button type="button" class="list-topbar__next" @click="scrollToNextArticle">Skip to next article ↓</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="feeds.length == 0" class="empty-state">
|
<div v-if="feeds.length == 0" class="empty-state">
|
||||||
<svg class="empty-state__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<svg class="empty-state__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
@@ -98,7 +103,7 @@ onMounted(async () => {
|
|||||||
<template v-for="( feed, index ) in feeds ">
|
<template v-for="( feed, index ) in feeds ">
|
||||||
<div v-bind:id="index" class="observe">
|
<div v-bind:id="index" class="observe">
|
||||||
<p class="feed-source">{{ feed.feedTitle }}</p>
|
<p class="feed-source">{{ feed.feedTitle }}</p>
|
||||||
<h2 @click="getReadable(feed, index)" class="feed-title">{{ feed.title }}</h2>
|
<h2 @click="loadReadable(feed, index)" class="feed-title">{{ feed.title }}</h2>
|
||||||
<h3>{{ feed.timestamp }}</h3>
|
<h3>{{ feed.timestamp }}</h3>
|
||||||
<p v-if="!feed.readable" class="feed-original-link">
|
<p v-if="!feed.readable" class="feed-original-link">
|
||||||
<a :href="feed.url" target="_blank" rel="noopener noreferrer">Read original article ↗</a>
|
<a :href="feed.url" target="_blank" rel="noopener noreferrer">Read original article ↗</a>
|
||||||
@@ -106,9 +111,16 @@ onMounted(async () => {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<p class="feed-content" v-html='feed.content'></p>
|
<p class="feed-content" :class="{ 'feed-content--readable': feed.readable }" v-html='feed.content'></p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<button
|
||||||
|
v-if="feeds.length"
|
||||||
|
type="button"
|
||||||
|
class="article-nav__btn list-skip-btn"
|
||||||
|
aria-label="Skip to next article"
|
||||||
|
@click="scrollToNextArticle"
|
||||||
|
>↓</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="article-single">
|
<div v-else class="article-single">
|
||||||
@@ -128,7 +140,7 @@ onMounted(async () => {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<article class="article-feature">
|
<article class="article-feature">
|
||||||
<p class="article-feature__source">{{ feeds[currentIndex].feedTitle }}</p>
|
<p class="article-feature__source">{{ feeds[currentIndex].feedTitle }}</p>
|
||||||
<h2 @click="getReadable(feeds[currentIndex], currentIndex)" class="article-feature__title">{{ feeds[currentIndex].title }}</h2>
|
<h2 @click="loadReadable(feeds[currentIndex], currentIndex)" class="article-feature__title">{{ feeds[currentIndex].title }}</h2>
|
||||||
<h3 class="article-feature__meta">{{ feeds[currentIndex].timestamp }}</h3>
|
<h3 class="article-feature__meta">{{ feeds[currentIndex].timestamp }}</h3>
|
||||||
<p v-if="!feeds[currentIndex].readable" class="feed-original-link">
|
<p v-if="!feeds[currentIndex].readable" class="feed-original-link">
|
||||||
<a :href="feeds[currentIndex].url" target="_blank" rel="noopener noreferrer">Read original article ↗</a>
|
<a :href="feeds[currentIndex].url" target="_blank" rel="noopener noreferrer">Read original article ↗</a>
|
||||||
@@ -182,21 +194,11 @@ onMounted(async () => {
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-topbar__next {
|
.list-skip-btn {
|
||||||
display: inline-flex;
|
position: fixed;
|
||||||
align-items: center;
|
right: 1rem;
|
||||||
min-height: 44px;
|
bottom: 1.5rem;
|
||||||
padding: 0.5rem 0.9rem;
|
z-index: 20;
|
||||||
margin-left: auto;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--color-background-soft);
|
|
||||||
color: var(--color-text);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-topbar__next:hover {
|
|
||||||
border-color: var(--color-border-hover);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.observe {
|
.observe {
|
||||||
@@ -256,6 +258,38 @@ onMounted(async () => {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feed-content--readable :deep(img),
|
||||||
|
.feed-content--readable :deep(video) {
|
||||||
|
display: block;
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 100vw;
|
||||||
|
height: auto;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
margin-left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed-content--readable :deep(img.article-feature__image--small) {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 1.5em auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 720px) {
|
||||||
|
.feed-content--readable :deep(img),
|
||||||
|
.feed-content--readable :deep(video) {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 1.5em auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.feed-original-link {
|
.feed-original-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ import { useFeeds } from '../../composables/useFeeds'
|
|||||||
|
|
||||||
vi.mock('axios')
|
vi.mock('axios')
|
||||||
|
|
||||||
|
// jsdom does not implement IntersectionObserver, but AppNav sets one up on mount
|
||||||
|
// to track whether the list view's title is scrolled into view.
|
||||||
|
class FakeIntersectionObserver {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
}
|
||||||
|
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
|
||||||
|
|
||||||
describe('AppNav', () => {
|
describe('AppNav', () => {
|
||||||
let router
|
let router
|
||||||
|
|
||||||
|
|||||||
@@ -252,13 +252,13 @@ describe('RssFeeds', () => {
|
|||||||
useFeeds().toggleViewMode()
|
useFeeds().toggleViewMode()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('.article-single .feed-title').text()).toBe('Article one')
|
expect(wrapper.find('.article-single .article-feature__title').text()).toBe('Article one')
|
||||||
// Same as in list view: the readable content is loaded on demand by
|
// Same as in list view: the readable content is loaded on demand by
|
||||||
// clicking the headline, not fetched automatically on entering the view.
|
// clicking the headline, not fetched automatically on entering the view.
|
||||||
expect(axios.post).not.toHaveBeenCalled()
|
expect(axios.post).not.toHaveBeenCalled()
|
||||||
expect(wrapper.find('.article-single .feed-original-link a').exists()).toBe(true)
|
expect(wrapper.find('.article-single .feed-original-link a').exists()).toBe(true)
|
||||||
|
|
||||||
await wrapper.find('.article-single .feed-title').trigger('click')
|
await wrapper.find('.article-single .article-feature__title').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(axios.post).toHaveBeenCalledWith('/api/v1/article/read', { url: 'https://example.test/1' }, expect.anything())
|
expect(axios.post).toHaveBeenCalledWith('/api/v1/article/read', { url: 'https://example.test/1' }, expect.anything())
|
||||||
@@ -269,13 +269,13 @@ describe('RssFeeds', () => {
|
|||||||
await wrapper.findAll('.article-nav__btn')[1].trigger('click')
|
await wrapper.findAll('.article-nav__btn')[1].trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('.article-single .feed-title').text()).toBe('Article two')
|
expect(wrapper.find('.article-single .article-feature__title').text()).toBe('Article two')
|
||||||
expect(wrapper.findAll('.article-nav__btn')[1].attributes('disabled')).toBeDefined()
|
expect(wrapper.findAll('.article-nav__btn')[1].attributes('disabled')).toBeDefined()
|
||||||
|
|
||||||
await wrapper.findAll('.article-nav__btn')[0].trigger('click')
|
await wrapper.findAll('.article-nav__btn')[0].trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('.article-single .feed-title').text()).toBe('Article one')
|
expect(wrapper.find('.article-single .article-feature__title').text()).toBe('Article one')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('drops articles read while paging through article view once back in the list', async () => {
|
it('drops articles read while paging through article view once back in the list', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user