Skip to content

Commit be6054e

Browse files
committed
Add tests
1 parent c0f88bb commit be6054e

8 files changed

Lines changed: 561 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ jobs:
145145
- name: Run tests with phpunit
146146
run: vendor/bin/phpunit tests
147147

148+
# - name: Run tests with vitest
149+
# run: node vendor/bin/phpunit node_modules/vitest/vitest.mjs --run
150+
148151
- name: Upload Panther screenshots
149152
if: failure()
150153
uses: actions/upload-artifact@v4

assets/vue/components/base/BaseBadge.spec.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@ import { mount } from '@vue/test-utils'
22
import BaseBadge from './BaseBadge.vue'
33

44
describe('BaseBadge', () => {
5+
it('applies shared base badge classes', () => {
6+
const wrapper = mount(BaseBadge)
7+
const classes = wrapper.get('span').classes()
8+
9+
expect(classes).toContain('inline-flex')
10+
expect(classes).toContain('items-center')
11+
expect(classes).toContain('px-2')
12+
expect(classes).toContain('py-0.5')
13+
expect(classes).toContain('rounded-full')
14+
expect(classes).toContain('text-xs')
15+
expect(classes).toContain('font-medium')
16+
})
17+
518
it('renders neutral variant by default', () => {
619
const wrapper = mount(BaseBadge, {
720
slots: {
@@ -28,6 +41,30 @@ describe('BaseBadge', () => {
2841
const classes = wrapper.get('span').classes()
2942
expect(classes).toContain('bg-indigo-50')
3043
expect(classes).toContain('text-ext-wf3')
44+
expect(classes).toContain('border')
45+
expect(classes).toContain('border-indigo-100')
3146
expect(wrapper.text()).toContain('10')
3247
})
48+
49+
it('falls back to neutral styles for unknown variant', () => {
50+
const wrapper = mount(BaseBadge, {
51+
props: {
52+
variant: 'unexpected',
53+
},
54+
})
55+
56+
const classes = wrapper.get('span').classes()
57+
expect(classes).toContain('bg-gray-100')
58+
expect(classes).toContain('text-gray-800')
59+
})
60+
61+
it('forwards attributes to the root span', () => {
62+
const wrapper = mount(BaseBadge, {
63+
attrs: {
64+
'data-testid': 'badge',
65+
},
66+
})
67+
68+
expect(wrapper.get('span').attributes('data-testid')).toBe('badge')
69+
})
3370
})
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { mount } from '@vue/test-utils'
2+
import { describe, it, expect } from 'vitest'
3+
import BaseCard from './BaseCard.vue'
4+
5+
describe('BaseCard.vue', () => {
6+
describe('default variant', () => {
7+
it('renders with default variant when no prop is passed', () => {
8+
const wrapper = mount(BaseCard)
9+
expect(wrapper.classes()).toContain('rounded-lg')
10+
expect(wrapper.classes()).toContain('shadow-sm')
11+
expect(wrapper.classes()).toContain('border')
12+
expect(wrapper.classes()).toContain('border-gray-100')
13+
expect(wrapper.classes()).toContain('bg-white')
14+
})
15+
16+
it('renders body with default padding', () => {
17+
const wrapper = mount(BaseCard)
18+
const body = wrapper.find('[data-testid="card-body"]')
19+
expect(body.classes()).toContain('p-4')
20+
})
21+
})
22+
23+
describe('subtle variant', () => {
24+
it('applies subtle card classes', () => {
25+
const wrapper = mount(BaseCard, { props: { variant: 'subtle' } })
26+
expect(wrapper.classes()).toContain('bg-gray-50')
27+
expect(wrapper.classes()).toContain('border-0')
28+
expect(wrapper.classes()).not.toContain('bg-white')
29+
})
30+
31+
it('applies subtle body classes', () => {
32+
const wrapper = mount(BaseCard, { props: { variant: 'subtle' } })
33+
const body = wrapper.find('[data-testid="card-body"]')
34+
expect(body.classes()).toContain('p-4')
35+
})
36+
})
37+
38+
describe('danger variant', () => {
39+
it('applies danger card classes', () => {
40+
const wrapper = mount(BaseCard, { props: { variant: 'danger' } })
41+
expect(wrapper.classes()).toContain('bg-red-600')
42+
expect(wrapper.classes()).toContain('text-white')
43+
expect(wrapper.classes()).toContain('border-0')
44+
})
45+
46+
it('applies danger body classes', () => {
47+
const wrapper = mount(BaseCard, { props: { variant: 'danger' } })
48+
const body = wrapper.find('[data-testid="card-body"]')
49+
expect(body.classes()).toContain('p-4')
50+
})
51+
})
52+
53+
describe('success variant', () => {
54+
it('applies success card classes', () => {
55+
const wrapper = mount(BaseCard, { props: { variant: 'success' } })
56+
expect(wrapper.classes()).toContain('bg-green-600')
57+
expect(wrapper.classes()).toContain('text-white')
58+
expect(wrapper.classes()).toContain('border-0')
59+
})
60+
61+
it('applies success body classes', () => {
62+
const wrapper = mount(BaseCard, { props: { variant: 'success' } })
63+
const body = wrapper.find('[data-testid="card-body"]')
64+
expect(body.classes()).toContain('p-4')
65+
})
66+
})
67+
68+
describe('unknown variant fallback', () => {
69+
it('falls back to default classes for an unrecognised variant', () => {
70+
const wrapper = mount(BaseCard, { props: { variant: 'ghost' } })
71+
expect(wrapper.classes()).toContain('bg-white')
72+
expect(wrapper.classes()).toContain('border-gray-100')
73+
})
74+
})
75+
})

assets/vue/components/base/BaseCard.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
22
<div :class="cardClasses">
3-
<div :class="bodyClasses">
3+
<div :class="bodyClasses" data-testid="card-body">
44
<slot />
55
</div>
66
</div>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { flushPromises, mount } from '@vue/test-utils'
2+
import PublicPageEditor from './PublicPageEditor.vue'
3+
4+
const {
5+
routeMock,
6+
pushMock,
7+
replaceMock,
8+
backendFetchMock,
9+
fetchAllAdminsMock,
10+
fetchAllListsMock,
11+
fetchAllAttributeDefinitionsMock,
12+
subscribePagesClientMock,
13+
} = vi.hoisted(() => ({
14+
routeMock: {
15+
params: {},
16+
query: {},
17+
},
18+
pushMock: vi.fn(() => Promise.resolve()),
19+
replaceMock: vi.fn(() => Promise.resolve()),
20+
backendFetchMock: vi.fn(),
21+
fetchAllAdminsMock: vi.fn(),
22+
fetchAllListsMock: vi.fn(),
23+
fetchAllAttributeDefinitionsMock: vi.fn(),
24+
subscribePagesClientMock: {
25+
getSubscribePage: vi.fn(),
26+
createSubscribePage: vi.fn(),
27+
updateSubscribePage: vi.fn(),
28+
},
29+
}))
30+
31+
vi.mock('vue-router', () => ({
32+
useRoute: () => routeMock,
33+
useRouter: () => ({
34+
push: pushMock,
35+
replace: replaceMock,
36+
}),
37+
}))
38+
39+
vi.mock('../../api', () => ({
40+
backendFetch: backendFetchMock,
41+
fetchAllAdmins: fetchAllAdminsMock,
42+
fetchAllLists: fetchAllListsMock,
43+
fetchAllAttributeDefinitions: fetchAllAttributeDefinitionsMock,
44+
subscribePagesClient: subscribePagesClientMock,
45+
}))
46+
47+
const mountComponent = () => mount(PublicPageEditor)
48+
49+
describe('PublicPageEditor', () => {
50+
beforeEach(() => {
51+
vi.clearAllMocks()
52+
53+
routeMock.params = {}
54+
routeMock.query = {}
55+
56+
backendFetchMock.mockResolvedValue({
57+
ok: true,
58+
json: () => Promise.resolve(['english.inc', 'armenian.inc']),
59+
})
60+
fetchAllAdminsMock.mockResolvedValue([
61+
{ id: 2, loginName: 'second-admin' },
62+
{ id: 1, loginName: 'first-admin' },
63+
])
64+
fetchAllListsMock.mockResolvedValue([
65+
{ id: 2, name: 'Non-public', public: false },
66+
{ id: 1, name: 'Public list', public: true },
67+
])
68+
fetchAllAttributeDefinitionsMock.mockResolvedValue([
69+
{ id: 2, name: 'City', type: 'text' },
70+
{ id: 1, name: 'Email', type: 'text' },
71+
])
72+
subscribePagesClientMock.getSubscribePage.mockResolvedValue({
73+
id: 42,
74+
title: 'Loaded page',
75+
owner: { id: 4 },
76+
data: [],
77+
})
78+
subscribePagesClientMock.createSubscribePage.mockResolvedValue({ id: 7 })
79+
subscribePagesClientMock.updateSubscribePage.mockResolvedValue({ id: 42 })
80+
vi.spyOn(window, 'alert').mockImplementation(() => {})
81+
vi.spyOn(console, 'error').mockImplementation(() => {})
82+
})
83+
84+
it('renders create mode and loads initial dependencies', async () => {
85+
const wrapper = mountComponent()
86+
await flushPromises()
87+
88+
expect(wrapper.text()).toContain('Create Subscribe Page')
89+
expect(fetchAllAdminsMock).toHaveBeenCalled()
90+
expect(fetchAllListsMock).toHaveBeenCalled()
91+
expect(fetchAllAttributeDefinitionsMock).toHaveBeenCalled()
92+
expect(subscribePagesClientMock.getSubscribePage).not.toHaveBeenCalled()
93+
})
94+
95+
it('renders edit mode and populates title from loaded page data', async () => {
96+
routeMock.params = { pageId: '42' }
97+
subscribePagesClientMock.getSubscribePage.mockResolvedValueOnce({
98+
id: 42,
99+
title: 'Server page title',
100+
owner: { id: 11 },
101+
data: [
102+
{ key: 'title', value: 'Loaded Title' },
103+
{ key: 'language_file', value: 'armenian.inc' },
104+
{ key: 'attributes', value: '' },
105+
],
106+
})
107+
108+
const wrapper = mountComponent()
109+
await flushPromises()
110+
111+
expect(wrapper.text()).toContain('Edit Subscribe Page')
112+
expect(subscribePagesClientMock.getSubscribePage).toHaveBeenCalledWith(42)
113+
expect(wrapper.find('input[type="text"]').element.value).toBe('Loaded Title')
114+
})
115+
116+
it('updates route query when moving to a different step', async () => {
117+
const wrapper = mountComponent()
118+
await flushPromises()
119+
120+
const listsStepButton = wrapper.findAll('button').find((button) => button.text().includes('Lists'))
121+
await listsStepButton.trigger('click')
122+
await flushPromises()
123+
124+
expect(replaceMock).toHaveBeenCalledWith({
125+
query: {
126+
step: '2',
127+
},
128+
})
129+
})
130+
131+
it('shows validation alert when saving without title', async () => {
132+
const wrapper = mountComponent()
133+
await flushPromises()
134+
135+
const saveButton = wrapper.findAll('button').find((button) => button.text().trim() === 'Save')
136+
await saveButton.trigger('click')
137+
138+
expect(window.alert).toHaveBeenCalledWith('Title is required.')
139+
expect(subscribePagesClientMock.createSubscribePage).not.toHaveBeenCalled()
140+
expect(subscribePagesClientMock.updateSubscribePage).not.toHaveBeenCalled()
141+
})
142+
})

0 commit comments

Comments
 (0)