Skip to content

Commit 85bff46

Browse files
Add Integration test for gutenberg frontend
1 parent e99f814 commit 85bff46

File tree

8 files changed

+869
-3
lines changed

8 files changed

+869
-3
lines changed

.github/workflows/Tests.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ jobs:
6969
working-directory: ui
7070
run: |
7171
npm run test:unit
72+
npm run test:integration
7273
7374
- name: Build
7475
working-directory: ui

ui/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
"dev": "vite",
1111
"build": "run-p type-check \"build-only {@}\" --",
1212
"preview": "vite preview",
13-
"test:unit": "vitest",
13+
"test:unit": "vitest --run --exclude \"**/*.integration.spec.ts\"",
14+
"test:integration": "vitest --run integration",
15+
"test:all": "vitest --run",
16+
"test:watch": "vitest",
1417
"build-only": "vite build",
1518
"type-check": "vue-tsc --build --force",
1619
"lint": "eslint . --fix",
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/**
2+
* Integration tests for main store
3+
* Tests complete data fetching flow with axios and error handling
4+
*/
5+
6+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
7+
import { setActivePinia, createPinia } from 'pinia'
8+
import { useMainStore } from './main'
9+
import axios from 'axios'
10+
import type { Config } from '@/types'
11+
12+
vi.mock('axios')
13+
14+
describe('Main Store Integration', () => {
15+
beforeEach(() => {
16+
setActivePinia(createPinia())
17+
vi.clearAllMocks()
18+
})
19+
20+
afterEach(() => {
21+
vi.restoreAllMocks()
22+
})
23+
24+
describe('Data Fetching', () => {
25+
it('fetches books and updates count', async () => {
26+
const store = useMainStore()
27+
const mockData = { totalCount: 100, books: [] }
28+
vi.mocked(axios.get).mockResolvedValue({ data: mockData })
29+
30+
const result = await store.fetchBooks()
31+
32+
expect(axios.get).toHaveBeenCalledWith('./books.json')
33+
expect(result).toEqual(mockData)
34+
expect(store.booksCount).toBe(100)
35+
expect(store.loading).toBe(false)
36+
expect(store.errorMessage).toBeNull()
37+
})
38+
39+
it('fetches authors and updates count', async () => {
40+
const store = useMainStore()
41+
const mockData = { totalCount: 50, authors: [] }
42+
vi.mocked(axios.get).mockResolvedValue({ data: mockData })
43+
44+
const result = await store.fetchAuthors()
45+
46+
expect(axios.get).toHaveBeenCalledWith('./authors.json')
47+
expect(result).toEqual(mockData)
48+
expect(store.authorsCount).toBe(50)
49+
expect(store.loading).toBe(false)
50+
expect(store.errorMessage).toBeNull()
51+
})
52+
53+
it('fetches shelves and updates count', async () => {
54+
const store = useMainStore()
55+
const mockData = { totalCount: 20, shelves: [] }
56+
vi.mocked(axios.get).mockResolvedValue({ data: mockData })
57+
58+
const result = await store.fetchLCCShelves()
59+
60+
expect(axios.get).toHaveBeenCalledWith('./lcc_shelves.json')
61+
expect(result).toEqual(mockData)
62+
expect(store.shelvesCount).toBe(20)
63+
expect(store.loading).toBe(false)
64+
expect(store.errorMessage).toBeNull()
65+
})
66+
67+
it.each([
68+
{ method: 'fetchBook' as const, param: 42, url: './books/42.json' },
69+
{ method: 'fetchAuthor' as const, param: 'austen-jane', url: './authors/austen-jane.json' },
70+
{ method: 'fetchLCCShelf' as const, param: 'PR', url: './lcc_shelves/PR.json' }
71+
])('$method fetches single item by ID', async ({ method, param, url }) => {
72+
const store = useMainStore()
73+
const mockData = { id: param }
74+
vi.mocked(axios.get).mockResolvedValue({ data: mockData })
75+
76+
const result = await store[method](param as never)
77+
78+
expect(axios.get).toHaveBeenCalledWith(url)
79+
expect(result).toEqual(mockData)
80+
})
81+
82+
it('fetches config data', async () => {
83+
const store = useMainStore()
84+
const mockConfig: Config = {
85+
title: 'Gutenberg Library',
86+
description: 'Project Gutenberg books',
87+
primaryColor: null,
88+
secondaryColor: null
89+
}
90+
91+
vi.mocked(axios.get).mockResolvedValue({ data: mockConfig })
92+
93+
const result = await store.fetchConfig()
94+
95+
expect(axios.get).toHaveBeenCalledWith('./config.json')
96+
expect(result).toEqual(mockConfig)
97+
})
98+
})
99+
100+
describe('Loading State', () => {
101+
it('sets loading during fetch', async () => {
102+
const store = useMainStore()
103+
vi.mocked(axios.get).mockImplementation(
104+
() =>
105+
new Promise((resolve) =>
106+
setTimeout(() => resolve({ data: { totalCount: 0, books: [] } }), 50)
107+
)
108+
)
109+
110+
const promise = store.fetchBooks()
111+
expect(store.loading).toBe(true)
112+
113+
await promise
114+
expect(store.loading).toBe(false)
115+
})
116+
})
117+
118+
describe('Error Handling', () => {
119+
it('sets error message on fetch failure', async () => {
120+
const store = useMainStore()
121+
vi.mocked(axios.get).mockRejectedValue(new Error('Network error'))
122+
123+
await expect(store.fetchBooks()).rejects.toThrow('Network error')
124+
125+
expect(store.errorMessage).toBe('Network error')
126+
expect(store.loading).toBe(false)
127+
})
128+
129+
it('uses custom error message when error message is empty', async () => {
130+
const store = useMainStore()
131+
vi.mocked(axios.get).mockRejectedValue(new Error(''))
132+
133+
await expect(store.fetchBooks()).rejects.toThrow()
134+
135+
expect(store.errorMessage).toBe('Failed to load books')
136+
})
137+
138+
it('clears error message', () => {
139+
const store = useMainStore()
140+
store.errorMessage = 'Test error'
141+
142+
store.clearError()
143+
144+
expect(store.errorMessage).toBeNull()
145+
})
146+
147+
it('clears previous error on successful fetch', async () => {
148+
const store = useMainStore()
149+
store.errorMessage = 'Previous error'
150+
151+
vi.mocked(axios.get).mockResolvedValue({ data: { totalCount: 0, books: [] } })
152+
153+
await store.fetchBooks()
154+
155+
expect(store.errorMessage).toBeNull()
156+
})
157+
158+
it('throws error when response data is empty', async () => {
159+
const store = useMainStore()
160+
vi.mocked(axios.get).mockResolvedValue({ data: null })
161+
162+
await expect(store.fetchBooks()).rejects.toThrow('Empty response from ./books.json')
163+
expect(store.errorMessage).toContain('Empty response')
164+
})
165+
})
166+
167+
describe('Request Deduplication', () => {
168+
it('deduplicates identical concurrent requests', async () => {
169+
const store = useMainStore()
170+
const mockData = { totalCount: 0, books: [] }
171+
vi.mocked(axios.get).mockResolvedValue({ data: mockData })
172+
173+
const [result1, result2, result3] = await Promise.all([
174+
store.fetchBooks(),
175+
store.fetchBooks(),
176+
store.fetchBooks()
177+
])
178+
179+
expect(axios.get).toHaveBeenCalledTimes(1)
180+
expect(result1).toEqual(mockData)
181+
expect(result2).toEqual(mockData)
182+
expect(result3).toEqual(mockData)
183+
})
184+
185+
it('allows different requests concurrently', async () => {
186+
const store = useMainStore()
187+
vi.mocked(axios.get).mockImplementation((url) => {
188+
if (url === './books.json') {
189+
return Promise.resolve({ data: { totalCount: 0, books: [] } })
190+
}
191+
return Promise.resolve({ data: { totalCount: 0, authors: [] } })
192+
})
193+
194+
await Promise.all([store.fetchBooks(), store.fetchAuthors()])
195+
196+
expect(axios.get).toHaveBeenCalledTimes(2)
197+
})
198+
})
199+
})
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/**
2+
* Integration tests for AuthorListView
3+
* Tests author browsing with search and pagination
4+
*/
5+
6+
import { describe, it, expect, beforeEach, vi } from 'vitest'
7+
import { mount, flushPromises } from '@vue/test-utils'
8+
import { createPinia, setActivePinia } from 'pinia'
9+
import { createRouter, createMemoryHistory } from 'vue-router'
10+
import AuthorListView from './AuthorListView.vue'
11+
import { useMainStore } from '@/stores/main'
12+
import type { Authors } from '@/types'
13+
14+
const mockAuthorsData: Authors = {
15+
totalCount: 5,
16+
authors: [
17+
{ id: 'austen-jane', name: 'Jane Austen', bookCount: 6 },
18+
{ id: 'dickens-charles', name: 'Charles Dickens', bookCount: 15 },
19+
{ id: 'shakespeare-william', name: 'William Shakespeare', bookCount: 20 },
20+
{ id: 'hugo-victor', name: 'Victor Hugo', bookCount: 10 },
21+
{ id: 'twain-mark', name: 'Mark Twain', bookCount: 12 }
22+
]
23+
}
24+
25+
describe('AuthorListView Integration', () => {
26+
let router: ReturnType<typeof createRouter>
27+
let store: ReturnType<typeof useMainStore>
28+
let pinia: ReturnType<typeof createPinia>
29+
30+
beforeEach(() => {
31+
pinia = createPinia()
32+
setActivePinia(pinia)
33+
store = useMainStore()
34+
router = createRouter({
35+
history: createMemoryHistory(),
36+
routes: [{ path: '/authors', component: AuthorListView }]
37+
})
38+
})
39+
40+
const mountView = async (mockData: Authors = mockAuthorsData) => {
41+
vi.spyOn(store, 'fetchAuthors').mockResolvedValue(mockData)
42+
const wrapper = mount(AuthorListView, {
43+
global: {
44+
plugins: [pinia, router]
45+
}
46+
})
47+
await flushPromises()
48+
return wrapper
49+
}
50+
51+
describe('Data Loading', () => {
52+
it('loads and displays authors on mount', async () => {
53+
const wrapper = await mountView()
54+
55+
expect(store.fetchAuthors).toHaveBeenCalledOnce()
56+
expect(wrapper.text()).toContain('Jane Austen')
57+
expect(wrapper.text()).toContain('Charles Dickens')
58+
expect(wrapper.text()).toContain('William Shakespeare')
59+
})
60+
61+
it('shows empty state when no authors available', async () => {
62+
const wrapper = await mountView({ totalCount: 0, authors: [] })
63+
64+
const listWrapper = wrapper.findComponent({ name: 'ListViewWrapper' })
65+
expect(listWrapper.props('hasItems')).toBe(false)
66+
})
67+
})
68+
69+
describe('Search Functionality', () => {
70+
it('filters authors by search query', async () => {
71+
const wrapper = await mountView()
72+
73+
expect(wrapper.text()).toContain('Jane Austen')
74+
expect(wrapper.text()).toContain('Charles Dickens')
75+
76+
const listWrapper = wrapper.findComponent({ name: 'ListViewWrapper' })
77+
await listWrapper.vm.$emit('update:searchQuery', 'Jane')
78+
await wrapper.vm.$nextTick()
79+
80+
const authorGrid = wrapper.findComponent({ name: 'AuthorGrid' })
81+
expect(authorGrid.props('authors')).toHaveLength(1)
82+
expect(authorGrid.props('authors')[0].name).toBe('Jane Austen')
83+
})
84+
85+
it('shows all authors when search is cleared', async () => {
86+
const wrapper = await mountView()
87+
88+
const listWrapper = wrapper.findComponent({ name: 'ListViewWrapper' })
89+
await listWrapper.vm.$emit('update:searchQuery', 'Jane')
90+
await wrapper.vm.$nextTick()
91+
92+
const authorGrid = wrapper.findComponent({ name: 'AuthorGrid' })
93+
expect(authorGrid.props('authors')).toHaveLength(1)
94+
95+
await listWrapper.vm.$emit('update:searchQuery', '')
96+
await wrapper.vm.$nextTick()
97+
98+
expect(authorGrid.props('authors')).toHaveLength(5)
99+
})
100+
101+
it('handles case-insensitive search', async () => {
102+
const wrapper = await mountView()
103+
104+
const listWrapper = wrapper.findComponent({ name: 'ListViewWrapper' })
105+
await listWrapper.vm.$emit('update:searchQuery', 'DICKENS')
106+
await wrapper.vm.$nextTick()
107+
108+
const authorGrid = wrapper.findComponent({ name: 'AuthorGrid' })
109+
expect(authorGrid.props('authors')).toHaveLength(1)
110+
expect(authorGrid.props('authors')[0].name).toBe('Charles Dickens')
111+
})
112+
})
113+
114+
describe('Sorting', () => {
115+
it('sorts authors alphabetically by default', async () => {
116+
const wrapper = await mountView()
117+
118+
const authorGrid = wrapper.findComponent({ name: 'AuthorGrid' })
119+
const authors = authorGrid.props('authors')
120+
121+
expect(authors[0].name).toBe('Charles Dickens')
122+
expect(authors[1].name).toBe('Jane Austen')
123+
expect(authors[2].name).toBe('Mark Twain')
124+
})
125+
})
126+
127+
describe('Pagination', () => {
128+
const createManyAuthors = () => ({
129+
totalCount: 30,
130+
authors: Array.from({ length: 30 }, (_, i) => ({
131+
id: `author-${i}`,
132+
name: `Author ${String(i + 1).padStart(2, '0')}`,
133+
bookCount: 5
134+
}))
135+
})
136+
137+
it('paginates when more than 24 authors', async () => {
138+
const wrapper = await mountView(createManyAuthors())
139+
140+
const listWrapper = wrapper.findComponent({ name: 'ListViewWrapper' })
141+
expect(listWrapper.props('totalPages')).toBeGreaterThan(1)
142+
})
143+
144+
it('resets to page 1 when search query changes', async () => {
145+
const wrapper = await mountView(createManyAuthors())
146+
147+
const listWrapper = wrapper.findComponent({ name: 'ListViewWrapper' })
148+
await listWrapper.vm.$emit('goToPage', 2)
149+
await wrapper.vm.$nextTick()
150+
151+
expect(listWrapper.props('currentPage')).toBe(2)
152+
153+
await listWrapper.vm.$emit('update:searchQuery', 'test')
154+
await wrapper.vm.$nextTick()
155+
156+
expect(listWrapper.props('currentPage')).toBe(1)
157+
})
158+
})
159+
160+
describe('Item Count', () => {
161+
it('displays correct author count', async () => {
162+
const wrapper = await mountView()
163+
164+
const listWrapper = wrapper.findComponent({ name: 'ListViewWrapper' })
165+
expect(listWrapper.props('currentCount')).toBe(5)
166+
expect(listWrapper.props('totalCount')).toBe(5)
167+
expect(listWrapper.props('itemType')).toBe('authors')
168+
})
169+
})
170+
})

0 commit comments

Comments
 (0)