Skip to content

Commit 6f47d6c

Browse files
authored
Merge pull request #817 from Chris0Jeky/test/frontend-view-component-coverage
TST-49: Frontend view and component coverage gaps
2 parents 20586f3 + 977de7f commit 6f47d6c

16 files changed

+1421
-0
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { mount } from '@vue/test-utils'
3+
import { reactive, ref, computed } from 'vue'
4+
import ActivityResults from '../../components/activity/ActivityResults.vue'
5+
6+
const mockAuditStore = reactive({
7+
loading: false,
8+
entries: [] as Array<{
9+
action: string | number
10+
timestamp: string
11+
entityType: string
12+
entityId: string
13+
userName: string | null
14+
changes: string | null
15+
}>,
16+
})
17+
18+
vi.mock('../../store/auditStore', () => ({
19+
useAuditStore: () => mockAuditStore,
20+
}))
21+
22+
// Mock the virtual list composable to render items directly
23+
vi.mock('../../composables/useVirtualList', () => ({
24+
useVirtualList: (options: { count: { value: number } }) => ({
25+
parentRef: ref(null),
26+
virtualItemEls: ref([]),
27+
virtualRows: computed(() =>
28+
Array.from({ length: options.count.value }, (_, i) => ({
29+
index: i,
30+
key: i,
31+
start: i * 100,
32+
end: (i + 1) * 100,
33+
size: 100,
34+
})),
35+
),
36+
totalSize: computed(() => options.count.value * 100),
37+
translateY: computed(() => 0),
38+
}),
39+
}))
40+
41+
vi.mock('../../composables/useActivityQuery', () => ({
42+
formatAction: (action: string | number) => (typeof action === 'string' ? action : `Action${action}`),
43+
formatTimestamp: (ts: string) => ts,
44+
}))
45+
46+
describe('ActivityResults', () => {
47+
beforeEach(() => {
48+
vi.clearAllMocks()
49+
mockAuditStore.loading = false
50+
mockAuditStore.entries = []
51+
})
52+
53+
it('shows loading state', () => {
54+
mockAuditStore.loading = true
55+
const wrapper = mount(ActivityResults, {
56+
props: { emptyStateTitle: 'No Activity', emptyStateBody: 'Nothing here.' },
57+
})
58+
expect(wrapper.text()).toContain('Loading activity...')
59+
})
60+
61+
it('shows empty state with custom title and body', () => {
62+
const wrapper = mount(ActivityResults, {
63+
props: { emptyStateTitle: 'No Activity', emptyStateBody: 'Nothing here yet.' },
64+
})
65+
expect(wrapper.text()).toContain('No Activity')
66+
expect(wrapper.text()).toContain('Nothing here yet.')
67+
})
68+
69+
it('shows action buttons in empty state', () => {
70+
const wrapper = mount(ActivityResults, {
71+
props: { emptyStateTitle: 'No Activity', emptyStateBody: 'Nothing.' },
72+
})
73+
expect(wrapper.text()).toContain('Open Review')
74+
expect(wrapper.text()).toContain('Open Boards')
75+
})
76+
77+
it('emits navigate when empty state action buttons are clicked', async () => {
78+
const wrapper = mount(ActivityResults, {
79+
props: { emptyStateTitle: 'No Activity', emptyStateBody: 'Nothing.' },
80+
})
81+
const reviewBtn = wrapper.findAll('button').find((b) => b.text() === 'Open Review')
82+
expect(reviewBtn).toBeTruthy()
83+
await reviewBtn?.trigger('click')
84+
expect(wrapper.emitted('navigate')?.[0]).toEqual(['/workspace/review'])
85+
86+
const boardsBtn = wrapper.findAll('button').find((b) => b.text() === 'Open Boards')
87+
expect(boardsBtn).toBeTruthy()
88+
await boardsBtn?.trigger('click')
89+
expect(wrapper.emitted('navigate')?.[1]).toEqual(['/workspace/boards'])
90+
})
91+
92+
it('renders timeline entries', () => {
93+
mockAuditStore.entries = [
94+
{
95+
action: 'Created',
96+
timestamp: '2026-03-15T10:00:00Z',
97+
entityType: 'Card',
98+
entityId: 'card-1',
99+
userName: 'alice',
100+
changes: null,
101+
},
102+
{
103+
action: 1,
104+
timestamp: '2026-03-15T11:00:00Z',
105+
entityType: 'Board',
106+
entityId: 'board-1',
107+
userName: 'bob',
108+
changes: 'Updated name',
109+
},
110+
]
111+
const wrapper = mount(ActivityResults, {
112+
props: { emptyStateTitle: 'No Activity', emptyStateBody: 'Nothing.' },
113+
})
114+
expect(wrapper.text()).toContain('Created')
115+
expect(wrapper.text()).toContain('Card - card-1')
116+
expect(wrapper.text()).toContain('by alice')
117+
expect(wrapper.text()).toContain('Action1')
118+
expect(wrapper.text()).toContain('Board - board-1')
119+
expect(wrapper.text()).toContain('by bob')
120+
expect(wrapper.text()).toContain('Updated name')
121+
})
122+
123+
it('does not show actor when userName is null', () => {
124+
mockAuditStore.entries = [
125+
{
126+
action: 'Archived',
127+
timestamp: '2026-03-15T10:00:00Z',
128+
entityType: 'Card',
129+
entityId: 'card-2',
130+
userName: null,
131+
changes: null,
132+
},
133+
]
134+
const wrapper = mount(ActivityResults, {
135+
props: { emptyStateTitle: 'No Activity', emptyStateBody: 'Nothing.' },
136+
})
137+
expect(wrapper.text()).not.toContain('by')
138+
})
139+
})
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { mount } from '@vue/test-utils'
3+
import { reactive } from 'vue'
4+
import ActivitySelector from '../../components/activity/ActivitySelector.vue'
5+
6+
const mockSessionStore = reactive({
7+
username: 'alice',
8+
})
9+
10+
vi.mock('../../store/sessionStore', () => ({
11+
useSessionStore: () => mockSessionStore,
12+
}))
13+
14+
const defaultProps = {
15+
viewMode: 'board' as const,
16+
selectedBoardId: '',
17+
selectedEntityType: '' as const,
18+
selectedEntityBoardId: '',
19+
selectedEntityId: '',
20+
limit: 50,
21+
loadingEntitySource: false,
22+
boardOptions: [
23+
{ id: 'b1', label: 'Board One' },
24+
{ id: 'b2', label: 'Board Two' },
25+
],
26+
requiresEntityBoardContext: false,
27+
entityOptions: [],
28+
canFetch: true,
29+
selectedIdForCopy: '',
30+
selectedIdLabel: '',
31+
}
32+
33+
describe('ActivitySelector', () => {
34+
beforeEach(() => {
35+
vi.clearAllMocks()
36+
mockSessionStore.username = 'alice'
37+
})
38+
39+
it('renders view mode selector with all options', () => {
40+
const wrapper = mount(ActivitySelector, { props: defaultProps })
41+
const modeSelect = wrapper.find('#activity-view-mode')
42+
expect(modeSelect.exists()).toBe(true)
43+
const options = modeSelect.findAll('option')
44+
expect(options).toHaveLength(3)
45+
expect(options[0].text()).toBe('Board History')
46+
expect(options[1].text()).toBe('Entity History')
47+
expect(options[2].text()).toBe('User History')
48+
})
49+
50+
it('emits update:viewMode when view mode changes', async () => {
51+
const wrapper = mount(ActivitySelector, { props: defaultProps })
52+
await wrapper.find('#activity-view-mode').setValue('entity')
53+
expect(wrapper.emitted('update:viewMode')?.[0]).toEqual(['entity'])
54+
})
55+
56+
it('shows board selector when viewMode is "board"', () => {
57+
const wrapper = mount(ActivitySelector, { props: defaultProps })
58+
const boardSelect = wrapper.find('#activity-board-select')
59+
expect(boardSelect.exists()).toBe(true)
60+
const options = boardSelect.findAll('option')
61+
// 1 disabled placeholder + 2 board options
62+
expect(options).toHaveLength(3)
63+
expect(options[1].text()).toBe('Board One')
64+
expect(options[2].text()).toBe('Board Two')
65+
})
66+
67+
it('emits update:selectedBoardId when board is changed', async () => {
68+
const wrapper = mount(ActivitySelector, { props: defaultProps })
69+
await wrapper.find('#activity-board-select').setValue('b2')
70+
expect(wrapper.emitted('update:selectedBoardId')?.[0]).toEqual(['b2'])
71+
})
72+
73+
it('shows current user info when viewMode is "user"', () => {
74+
const wrapper = mount(ActivitySelector, {
75+
props: { ...defaultProps, viewMode: 'user' as const },
76+
})
77+
expect(wrapper.text()).toContain('Current user:')
78+
expect(wrapper.text()).toContain('alice')
79+
// Board selector should NOT be shown in user mode
80+
expect(wrapper.find('#activity-board-select').exists()).toBe(false)
81+
})
82+
83+
it('shows entity type selector when viewMode is "entity"', () => {
84+
const wrapper = mount(ActivitySelector, {
85+
props: { ...defaultProps, viewMode: 'entity' as const },
86+
})
87+
const entityTypeSelect = wrapper.find('#activity-entity-type')
88+
expect(entityTypeSelect.exists()).toBe(true)
89+
const options = entityTypeSelect.findAll('option')
90+
// placeholder + Board, Column, Card, Label
91+
expect(options).toHaveLength(5)
92+
})
93+
94+
it('emits update:selectedEntityType when entity type changes', async () => {
95+
const wrapper = mount(ActivitySelector, {
96+
props: { ...defaultProps, viewMode: 'entity' as const },
97+
})
98+
await wrapper.find('#activity-entity-type').setValue('Card')
99+
expect(wrapper.emitted('update:selectedEntityType')?.[0]).toEqual(['Card'])
100+
})
101+
102+
it('shows limit selector with all options', () => {
103+
const wrapper = mount(ActivitySelector, { props: defaultProps })
104+
const limitSelect = wrapper.find('[aria-label="Activity limit"]')
105+
expect(limitSelect.exists()).toBe(true)
106+
const options = limitSelect.findAll('option')
107+
expect(options).toHaveLength(3)
108+
expect(options.map((o) => o.text())).toEqual(['25', '50', '100'])
109+
})
110+
111+
it('emits fetch when Fetch button is clicked', async () => {
112+
const wrapper = mount(ActivitySelector, { props: defaultProps })
113+
const fetchBtn = wrapper.findAll('button').find((b) => b.text() === 'Fetch')
114+
expect(fetchBtn).toBeTruthy()
115+
await fetchBtn?.trigger('click')
116+
expect(wrapper.emitted('fetch')).toHaveLength(1)
117+
})
118+
119+
it('disables Fetch button when canFetch is false', () => {
120+
const wrapper = mount(ActivitySelector, {
121+
props: { ...defaultProps, canFetch: false },
122+
})
123+
const fetchBtn = wrapper.findAll('button').find((b) => b.text() === 'Fetch')
124+
expect(fetchBtn).toBeTruthy()
125+
expect(fetchBtn?.attributes('disabled')).toBeDefined()
126+
})
127+
128+
it('shows loading helper text when loadingEntitySource is true', () => {
129+
const wrapper = mount(ActivitySelector, {
130+
props: { ...defaultProps, viewMode: 'entity' as const, loadingEntitySource: true },
131+
})
132+
expect(wrapper.text()).toContain('Loading entities for selected board...')
133+
})
134+
135+
it('shows copy ID affordance when selectedIdForCopy is set', () => {
136+
const wrapper = mount(ActivitySelector, {
137+
props: { ...defaultProps, selectedIdForCopy: 'abc-123', selectedIdLabel: 'Board ID' },
138+
})
139+
expect(wrapper.text()).toContain('Board ID')
140+
expect(wrapper.text()).toContain('abc-123')
141+
expect(wrapper.text()).toContain('Copy Raw ID')
142+
})
143+
144+
it('emits copyId when Copy Raw ID button is clicked', async () => {
145+
const wrapper = mount(ActivitySelector, {
146+
props: { ...defaultProps, selectedIdForCopy: 'abc-123', selectedIdLabel: 'Board ID' },
147+
})
148+
const copyBtn = wrapper.findAll('button').find((b) => b.text().includes('Copy Raw ID'))
149+
expect(copyBtn).toBeTruthy()
150+
await copyBtn?.trigger('click')
151+
expect(wrapper.emitted('copyId')).toHaveLength(1)
152+
})
153+
})

0 commit comments

Comments
 (0)