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
|
||||
|
||||
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 = () => {
|
||||
if (img.naturalWidth && img.naturalWidth <= SMALL_IMAGE_THRESHOLD) {
|
||||
img.classList.add('article-feature__image--small')
|
||||
@@ -60,6 +60,12 @@ watch(() => feeds.value[currentIndex.value]?.content, async () => {
|
||||
markSmallImages()
|
||||
})
|
||||
|
||||
async function loadReadable(feed, index) {
|
||||
await getReadable(feed, index)
|
||||
await nextTick()
|
||||
markSmallImages()
|
||||
}
|
||||
|
||||
async function shareUrl(url) {
|
||||
if (navigator.share) {
|
||||
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="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>
|
||||
<button type="button" class="list-topbar__next" @click="scrollToNextArticle">Skip to next article ↓</button>
|
||||
</div>
|
||||
<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">
|
||||
@@ -98,7 +103,7 @@ onMounted(async () => {
|
||||
<template v-for="( feed, index ) in feeds ">
|
||||
<div v-bind:id="index" class="observe">
|
||||
<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>
|
||||
<p v-if="!feed.readable" class="feed-original-link">
|
||||
<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>
|
||||
</button>
|
||||
</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>
|
||||
</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 v-else class="article-single">
|
||||
@@ -128,7 +140,7 @@ onMounted(async () => {
|
||||
<template v-else>
|
||||
<article class="article-feature">
|
||||
<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>
|
||||
<p v-if="!feeds[currentIndex].readable" class="feed-original-link">
|
||||
<a :href="feeds[currentIndex].url" target="_blank" rel="noopener noreferrer">Read original article ↗</a>
|
||||
@@ -182,21 +194,11 @@ onMounted(async () => {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.list-topbar__next {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
padding: 0.5rem 0.9rem;
|
||||
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);
|
||||
.list-skip-btn {
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1.5rem;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.observe {
|
||||
@@ -256,6 +258,38 @@ onMounted(async () => {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -7,6 +7,15 @@ import { useFeeds } from '../../composables/useFeeds'
|
||||
|
||||
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', () => {
|
||||
let router
|
||||
|
||||
|
||||
@@ -252,13 +252,13 @@ describe('RssFeeds', () => {
|
||||
useFeeds().toggleViewMode()
|
||||
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
|
||||
// clicking the headline, not fetched automatically on entering the view.
|
||||
expect(axios.post).not.toHaveBeenCalled()
|
||||
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()
|
||||
|
||||
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 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()
|
||||
|
||||
await wrapper.findAll('.article-nav__btn')[0].trigger('click')
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user