claude rework
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import AppNav from '../AppNav.vue'
|
||||
|
||||
describe('AppNav', () => {
|
||||
let router
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.setItem('user-token', 'abc123')
|
||||
localStorage.setItem('user-id', '7')
|
||||
|
||||
router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/login', name: 'login', component: { template: '<div />' } },
|
||||
{ path: '/feeds', name: 'feeds', component: { template: '<div />' } },
|
||||
],
|
||||
})
|
||||
router.push('/feeds')
|
||||
await router.isReady()
|
||||
})
|
||||
|
||||
it('clears stored credentials and redirects to login on logout', async () => {
|
||||
const wrapper = mount(AppNav, { global: { plugins: [router] } })
|
||||
|
||||
await wrapper.find('.app-nav__logout').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(localStorage.getItem('user-token')).toBeNull()
|
||||
expect(localStorage.getItem('user-id')).toBeNull()
|
||||
expect(router.currentRoute.value.name).toBe('login')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import LoginPage from '../LoginPage.vue'
|
||||
|
||||
vi.mock('axios')
|
||||
|
||||
describe('LoginPage', () => {
|
||||
let router
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear()
|
||||
vi.clearAllMocks()
|
||||
|
||||
router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/login', name: 'login', component: { template: '<div />' } },
|
||||
{ path: '/feeds', name: 'feeds', component: { template: '<div />' } },
|
||||
],
|
||||
})
|
||||
router.push('/login')
|
||||
await router.isReady()
|
||||
})
|
||||
|
||||
it('stores the token and redirects to feeds on successful login', async () => {
|
||||
axios.post.mockResolvedValueOnce({
|
||||
status: 200,
|
||||
headers: { token: 'abc123', user_id: '7' },
|
||||
})
|
||||
|
||||
const wrapper = mount(LoginPage, { global: { plugins: [router] } })
|
||||
await wrapper.find('#username').setValue('alice')
|
||||
await wrapper.find('#password').setValue('secret')
|
||||
await wrapper.find('form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/auth/login',
|
||||
{ username: 'alice', password: 'secret' },
|
||||
expect.anything(),
|
||||
)
|
||||
expect(localStorage.getItem('user-token')).toBe('abc123')
|
||||
expect(localStorage.getItem('user-id')).toBe('7')
|
||||
expect(router.currentRoute.value.name).toBe('feeds')
|
||||
})
|
||||
|
||||
it('shows an error message and does not redirect when login fails', async () => {
|
||||
axios.post.mockRejectedValueOnce(new Error('Request failed'))
|
||||
|
||||
const wrapper = mount(LoginPage, { global: { plugins: [router] } })
|
||||
await wrapper.find('#username').setValue('alice')
|
||||
await wrapper.find('#password').setValue('wrong')
|
||||
await wrapper.find('form').trigger('submit.prevent')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Login failed')
|
||||
expect(localStorage.getItem('user-token')).toBeNull()
|
||||
expect(router.currentRoute.value.name).toBe('login')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import axios from 'axios'
|
||||
import RssFeeds from '../RssFeeds.vue'
|
||||
|
||||
vi.mock('axios')
|
||||
|
||||
// jsdom does not implement IntersectionObserver, but the component sets one up
|
||||
// once the feed list has rendered.
|
||||
class FakeIntersectionObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
|
||||
|
||||
describe('RssFeeds', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.setItem('user-token', 'test-token')
|
||||
localStorage.setItem('user-id', '7')
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('fetches the current user articles and shows the empty state', async () => {
|
||||
axios.get.mockResolvedValueOnce({ data: { feeds: [] } })
|
||||
|
||||
const wrapper = mount(RssFeeds)
|
||||
await flushPromises()
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith('/api/v1/article/get/7', expect.anything())
|
||||
expect(wrapper.text()).toContain('No unread articles.')
|
||||
})
|
||||
|
||||
it('renders the fetched feed items', async () => {
|
||||
axios.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
feeds: [
|
||||
{
|
||||
title: 'My Feed',
|
||||
items: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Article one',
|
||||
content: '<p>hello</p>',
|
||||
url: 'https://example.test/1',
|
||||
timestamp: '2026-01-01',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const wrapper = mount(RssFeeds)
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('Article one')
|
||||
expect(wrapper.text()).toContain('My Feed')
|
||||
expect(wrapper.text()).not.toContain('No unread articles.')
|
||||
})
|
||||
|
||||
it('syncs feeds for the current user', async () => {
|
||||
axios.get.mockResolvedValue({ data: { feeds: [] } })
|
||||
axios.post.mockResolvedValueOnce({ status: 200 })
|
||||
|
||||
const wrapper = mount(RssFeeds)
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.find('.feed-actions p').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/article/sync',
|
||||
{ user_id: 7 },
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user