Skip to content

Commit 9d17d8a

Browse files
test: add unit tests for composables and stores
- Add tests for usePostSearch composable - Add tests for usePostAction composable - Add tests for useFormSubmit composable - Add tests for useFormValidation composable - Add tests for usePostModal composable - Add tests for toast store - Fix validation behavior to only validate on touch/submit - Configure test file exclusion from build
1 parent e82d81f commit 9d17d8a

16 files changed

+1337
-5
lines changed

src/App.test.ts

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { mount } from '@vue/test-utils'
2+
import { describe, expect, test, vi } from 'vitest'
3+
import App from './App.vue'
4+
import { createTestingPinia } from '@pinia/testing'
5+
import { usePostsStore } from '@/stores/posts'
6+
7+
describe('App', () => {
8+
const mockPosts = [
9+
{
10+
id: 1,
11+
title: 'Test Post 1',
12+
body: 'Content 1',
13+
userId: 1
14+
},
15+
{
16+
id: 2,
17+
title: 'Test Post 2',
18+
body: 'Content 2',
19+
userId: 2
20+
}
21+
]
22+
23+
const mockUsers = [
24+
{ id: 1, name: 'User 1', email: '[email protected]', username: 'user1' },
25+
{ id: 2, name: 'User 2', email: '[email protected]', username: 'user2' }
26+
]
27+
28+
const mountApp = () => {
29+
return mount(App, {
30+
global: {
31+
plugins: [
32+
createTestingPinia({
33+
createSpy: vi.fn,
34+
initialState: {
35+
posts: {
36+
posts: mockPosts,
37+
users: mockUsers,
38+
isLoading: false,
39+
error: null,
40+
pendingPosts: []
41+
}
42+
}
43+
})
44+
],
45+
stubs: {
46+
Teleport: true,
47+
Transition: true,
48+
QuillEditor: true
49+
}
50+
}
51+
})
52+
}
53+
54+
test('renders posts list', () => {
55+
const wrapper = mountApp()
56+
const rows = wrapper.findAll('[role="row"]')
57+
expect(rows.length).toBeGreaterThan(0)
58+
})
59+
60+
test('handles post selection', async () => {
61+
const wrapper = mountApp()
62+
63+
// Find the first PostRow component
64+
const postRow = wrapper.findComponent({ name: 'PostRow' })
65+
66+
// Emit the checkbox-click event directly
67+
await postRow.vm.$emit('checkbox-click', new MouseEvent('click'), mockPosts[0].id, 0)
68+
69+
// Check if the post is selected
70+
expect(wrapper.vm.selectedPosts).toContain(mockPosts[0].id)
71+
})
72+
73+
test('handles select all', async () => {
74+
const wrapper = mountApp()
75+
76+
// Find the TableHeader component
77+
const tableHeader = wrapper.findComponent({ name: 'TableHeader' })
78+
79+
// Emit the toggle-select-all event
80+
await tableHeader.vm.$emit('toggle-select-all')
81+
82+
// Check if all posts are selected
83+
const allPostIds = mockPosts.map(post => post.id)
84+
expect(wrapper.vm.selectedPosts).toEqual(allPostIds)
85+
})
86+
87+
test('shows loading state', () => {
88+
const wrapper = mount(App, {
89+
global: {
90+
plugins: [
91+
createTestingPinia({
92+
createSpy: vi.fn,
93+
initialState: {
94+
posts: {
95+
posts: [],
96+
users: [],
97+
isLoading: true,
98+
error: null,
99+
pendingPosts: []
100+
}
101+
}
102+
})
103+
],
104+
stubs: {
105+
Teleport: true,
106+
Transition: true
107+
}
108+
}
109+
})
110+
111+
expect(wrapper.find('.animate-pulse').exists()).toBe(true)
112+
})
113+
114+
test('filters posts based on search query', async () => {
115+
const wrapper = mountApp()
116+
const searchInput = wrapper.find('input[type="text"]')
117+
await searchInput.setValue('Test Post 1')
118+
119+
// Wait for the debounce
120+
await new Promise(resolve => setTimeout(resolve, 300))
121+
122+
const rows = wrapper.findAll('[role="row"]')
123+
expect(rows.length).toBe(2) // Header row + 1 matching post
124+
})
125+
126+
test('opens post menu and shows options', async () => {
127+
const wrapper = mountApp()
128+
const menuButton = wrapper.find('button[title="Options"]')
129+
await menuButton.trigger('click')
130+
131+
expect(wrapper.vm.activeMenu).toBe(mockPosts[0].id)
132+
})
133+
134+
test('handles shift-click selection', async () => {
135+
const wrapper = mountApp()
136+
const checkboxes = wrapper.findAll('input[type="checkbox"]')
137+
138+
// Click first checkbox
139+
await checkboxes[1].trigger('click')
140+
141+
// Shift-click last checkbox
142+
await checkboxes[2].trigger('click', {
143+
shiftKey: true
144+
})
145+
146+
expect(wrapper.vm.selectedPosts).toContain(mockPosts[0].id)
147+
expect(wrapper.vm.selectedPosts).toContain(mockPosts[1].id)
148+
})
149+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { mount } from '@vue/test-utils'
2+
import { describe, expect, test } from 'vitest'
3+
import ActionButtons from '../ActionButtons.vue'
4+
5+
describe('ActionButtons', () => {
6+
test('renders create button with correct text and styling', () => {
7+
const wrapper = mount(ActionButtons, {
8+
props: {
9+
isLoading: false,
10+
selectedCount: 0
11+
}
12+
})
13+
14+
const createButton = wrapper.find('button')
15+
expect(createButton.text()).toBe('Create new post')
16+
expect(createButton.classes()).toContain('bg-blue-600')
17+
expect(createButton.classes()).toContain('dark:bg-blue-500')
18+
})
19+
20+
test('shows delete button only when posts are selected', async () => {
21+
const wrapper = mount(ActionButtons, {
22+
props: {
23+
isLoading: false,
24+
selectedCount: 0
25+
}
26+
})
27+
28+
// Initially no delete button
29+
expect(wrapper.text()).not.toContain('Delete Selected')
30+
31+
// Show delete button when posts are selected
32+
await wrapper.setProps({ selectedCount: 2 })
33+
expect(wrapper.text()).toContain('Delete Selected (2)')
34+
})
35+
36+
test('disables create button when loading', () => {
37+
const wrapper = mount(ActionButtons, {
38+
props: {
39+
isLoading: true,
40+
selectedCount: 0
41+
}
42+
})
43+
44+
const createButton = wrapper.find('button')
45+
expect(createButton.attributes('disabled')).toBeDefined()
46+
expect(createButton.classes()).toContain('disabled:opacity-50')
47+
})
48+
49+
test('emits create event when create button is clicked', async () => {
50+
const wrapper = mount(ActionButtons, {
51+
props: {
52+
isLoading: false,
53+
selectedCount: 0
54+
}
55+
})
56+
57+
await wrapper.find('button').trigger('click')
58+
expect(wrapper.emitted('create')).toBeTruthy()
59+
expect(wrapper.emitted('create')).toHaveLength(1)
60+
})
61+
62+
test('emits delete-selected event when delete button is clicked', async () => {
63+
const wrapper = mount(ActionButtons, {
64+
props: {
65+
isLoading: false,
66+
selectedCount: 2
67+
}
68+
})
69+
70+
const deleteButton = wrapper.findAll('button')[1] // Second button
71+
await deleteButton.trigger('click')
72+
expect(wrapper.emitted('delete-selected')).toBeTruthy()
73+
expect(wrapper.emitted('delete-selected')).toHaveLength(1)
74+
})
75+
76+
test('maintains responsive layout classes', () => {
77+
const wrapper = mount(ActionButtons, {
78+
props: {
79+
isLoading: false,
80+
selectedCount: 1
81+
}
82+
})
83+
const buttons = wrapper.findAll('button')
84+
for (const button of buttons) {
85+
expect(button.classes()).toContain('flex-grow')
86+
expect(button.classes()).toContain('sm:flex-grow-0')
87+
}
88+
})
89+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { mount } from '@vue/test-utils'
2+
import { describe, expect, test } from 'vitest'
3+
import DeleteConfirmationModal from '../DeleteConfirmationModal.vue'
4+
5+
describe('DeleteConfirmationModal', () => {
6+
test('does not render when showDeleteModal is false', () => {
7+
const wrapper = mount(DeleteConfirmationModal, {
8+
props: {
9+
showDeleteModal: false
10+
}
11+
})
12+
13+
expect(wrapper.find('[role="dialog"]').exists()).toBe(false)
14+
})
15+
16+
test('renders when showDeleteModal is true', () => {
17+
const wrapper = mount(DeleteConfirmationModal, {
18+
props: {
19+
showDeleteModal: true
20+
}
21+
})
22+
23+
expect(wrapper.find('[role="dialog"]').exists()).toBe(true)
24+
expect(wrapper.find('#modal-title').text()).toBe('Confirm Delete')
25+
})
26+
27+
test('emits confirm event when delete button is clicked', async () => {
28+
const wrapper = mount(DeleteConfirmationModal, {
29+
props: {
30+
showDeleteModal: true
31+
}
32+
})
33+
34+
const deleteButton = wrapper.find('button.bg-red-600')
35+
await deleteButton.trigger('click')
36+
37+
expect(wrapper.emitted('confirm')).toBeTruthy()
38+
expect(wrapper.emitted('confirm')).toHaveLength(1)
39+
})
40+
41+
test('emits close event when cancel button is clicked', async () => {
42+
const wrapper = mount(DeleteConfirmationModal, {
43+
props: {
44+
showDeleteModal: true
45+
}
46+
})
47+
48+
const cancelButton = wrapper.find('button.bg-gray-200')
49+
await cancelButton.trigger('click')
50+
51+
expect(wrapper.emitted('close')).toBeTruthy()
52+
expect(wrapper.emitted('close')).toHaveLength(1)
53+
})
54+
55+
test('emits close event when backdrop is clicked', async () => {
56+
const wrapper = mount(DeleteConfirmationModal, {
57+
props: {
58+
showDeleteModal: true
59+
}
60+
})
61+
62+
const backdrop = wrapper.find('.backdrop-blur-sm')
63+
await backdrop.trigger('click')
64+
65+
expect(wrapper.emitted('close')).toBeTruthy()
66+
expect(wrapper.emitted('close')).toHaveLength(1)
67+
})
68+
69+
test('displays correct confirmation message', () => {
70+
const wrapper = mount(DeleteConfirmationModal, {
71+
props: {
72+
showDeleteModal: true
73+
}
74+
})
75+
76+
const message = wrapper.find('.text-gray-700')
77+
expect(message.text()).toBe('Are you sure you want to delete this post? This action cannot be undone.')
78+
})
79+
})

0 commit comments

Comments
 (0)