feat: add PendingRewardDialog, RewardConfirmDialog, and TaskConfirmDialog components
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 25s
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 25s
- Implemented PendingRewardDialog for handling pending reward requests. - Created RewardConfirmDialog for confirming reward redemption. - Developed TaskConfirmDialog for task confirmation with child name display. test: add unit tests for ChildView and ParentView components - Added comprehensive tests for ChildView including task triggering and SSE event handling. - Implemented tests for ParentView focusing on override modal and SSE event management. test: add ScrollingList component tests - Created tests for ScrollingList to verify item fetching, loading states, and custom item classes. - Included tests for two-step click interactions and edit button display logic. - Moved toward hashed passwords.
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { mount, VueWrapper } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import ScrollingList from '../ScrollingList.vue'
|
||||
|
||||
// Mock image cache
|
||||
vi.mock('@/common/imageCache', () => ({
|
||||
getCachedImageUrl: vi.fn((id: string) => Promise.resolve(`/cached/${id}.png`)),
|
||||
revokeAllImageUrls: vi.fn(),
|
||||
}))
|
||||
|
||||
global.fetch = vi.fn()
|
||||
|
||||
describe('ScrollingList', () => {
|
||||
let wrapper: VueWrapper<any>
|
||||
|
||||
const defaultProps = {
|
||||
title: 'Test List',
|
||||
fetchBaseUrl: '/api/test',
|
||||
ids: ['item-1', 'item-2'],
|
||||
itemKey: 'items',
|
||||
}
|
||||
|
||||
const mockItems = [
|
||||
{ id: 'item-1', name: 'Item One', points: 10, image_id: 'img1' },
|
||||
{ id: 'item-2', name: 'Item Two', points: 20, image_id: 'img2' },
|
||||
]
|
||||
|
||||
const mockItemsWithOverride = [
|
||||
{ id: 'item-1', name: 'Item One', points: 10, image_id: 'img1', custom_value: 15 },
|
||||
{ id: 'item-2', name: 'Item Two', points: 20, image_id: 'img2', custom_value: null },
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock fetch responses
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: mockItems }),
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
describe('Component Mounting', () => {
|
||||
it('renders with title', async () => {
|
||||
wrapper = mount(ScrollingList, { props: defaultProps })
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('h3').text()).toBe('Test List')
|
||||
})
|
||||
|
||||
it('fetches items on mount', async () => {
|
||||
wrapper = mount(ScrollingList, { props: defaultProps })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('/api/test?ids=item-1,item-2')
|
||||
})
|
||||
|
||||
it('displays loading state initially', () => {
|
||||
wrapper = mount(ScrollingList, { props: defaultProps })
|
||||
|
||||
expect(wrapper.find('.loading').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('displays items after loading', async () => {
|
||||
wrapper = mount(ScrollingList, { props: defaultProps })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(wrapper.find('.loading').exists()).toBe(false)
|
||||
expect(wrapper.findAll('.item-card').length).toBe(2)
|
||||
})
|
||||
|
||||
it('displays empty message when no items', async () => {
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: [] }),
|
||||
})
|
||||
|
||||
wrapper = mount(ScrollingList, { props: defaultProps })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(wrapper.find('.empty').text()).toContain('No Test List')
|
||||
})
|
||||
|
||||
it('displays error message on fetch failure', async () => {
|
||||
;(global.fetch as any).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
wrapper = mount(ScrollingList, { props: defaultProps })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(wrapper.find('.error').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Override Badge Display', () => {
|
||||
it('shows override badge when custom_value exists and isParentAuthenticated is true', async () => {
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: mockItemsWithOverride }),
|
||||
})
|
||||
|
||||
wrapper = mount(ScrollingList, { props: { ...defaultProps, isParentAuthenticated: true } })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const badges = wrapper.findAll('.override-badge')
|
||||
expect(badges.length).toBe(1) // Only item-1 has custom_value
|
||||
expect(badges[0].text()).toBe('⭐')
|
||||
})
|
||||
|
||||
it('does not show badge when custom_value is null', async () => {
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: mockItems }),
|
||||
})
|
||||
|
||||
wrapper = mount(ScrollingList, { props: { ...defaultProps, isParentAuthenticated: true } })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(wrapper.findAll('.override-badge').length).toBe(0)
|
||||
})
|
||||
|
||||
it('does not show badge when custom_value is undefined', async () => {
|
||||
const itemsWithUndefined = [{ id: 'item-1', name: 'Item One', points: 10 }]
|
||||
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: itemsWithUndefined }),
|
||||
})
|
||||
|
||||
wrapper = mount(ScrollingList, { props: { ...defaultProps, isParentAuthenticated: true } })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(wrapper.findAll('.override-badge').length).toBe(0)
|
||||
})
|
||||
|
||||
it('does not show badge in child mode even when custom_value exists', async () => {
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: mockItemsWithOverride }),
|
||||
})
|
||||
|
||||
wrapper = mount(ScrollingList, { props: { ...defaultProps, isParentAuthenticated: false } })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(wrapper.findAll('.override-badge').length).toBe(0)
|
||||
})
|
||||
|
||||
it('does not show badge when isParentAuthenticated is undefined', async () => {
|
||||
;(global.fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ items: mockItemsWithOverride }),
|
||||
})
|
||||
|
||||
wrapper = mount(ScrollingList, { props: defaultProps })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(wrapper.findAll('.override-badge').length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Two-Step Click Interaction', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(ScrollingList, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
enableEdit: false, // No edit button for basic click test
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
})
|
||||
|
||||
it('emits item-ready on first click', async () => {
|
||||
const cards = wrapper.findAll('.item-card')
|
||||
await cards[0].trigger('click')
|
||||
|
||||
expect(wrapper.emitted('item-ready')).toBeTruthy()
|
||||
expect(wrapper.emitted('item-ready')![0]).toEqual(['item-1'])
|
||||
})
|
||||
|
||||
it('emits trigger-item on second click of same item', async () => {
|
||||
const cards = wrapper.findAll('.item-card')
|
||||
|
||||
// First click - select item
|
||||
await cards[0].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
// Update prop to simulate parent setting readyItemId
|
||||
await wrapper.setProps({ readyItemId: 'item-1' })
|
||||
await nextTick()
|
||||
|
||||
// Second click - trigger item
|
||||
await cards[0].trigger('click')
|
||||
|
||||
expect(wrapper.emitted('trigger-item')).toBeTruthy()
|
||||
expect(wrapper.emitted('trigger-item')![0][0].id).toBe('item-1')
|
||||
})
|
||||
|
||||
it('resets ready state after triggering', async () => {
|
||||
const cards = wrapper.findAll('.item-card')
|
||||
|
||||
// First click
|
||||
await cards[0].trigger('click')
|
||||
await wrapper.setProps({ readyItemId: 'item-1' })
|
||||
|
||||
// Second click
|
||||
await cards[0].trigger('click')
|
||||
|
||||
const itemReadyEvents = wrapper.emitted('item-ready')
|
||||
expect(itemReadyEvents![itemReadyEvents!.length - 1]).toEqual([''])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edit Button Display', () => {
|
||||
it('shows edit button only for readyItemId when enableEdit is true', async () => {
|
||||
wrapper = mount(ScrollingList, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
enableEdit: true,
|
||||
readyItemId: 'item-1',
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const editButtons = wrapper.findAll('.edit-button')
|
||||
expect(editButtons.length).toBe(1)
|
||||
})
|
||||
|
||||
it('does not show edit button when enableEdit is false', async () => {
|
||||
wrapper = mount(ScrollingList, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
enableEdit: false,
|
||||
readyItemId: 'item-1',
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(wrapper.findAll('.edit-button').length).toBe(0)
|
||||
})
|
||||
|
||||
it('does not show edit button when readyItemId is null', async () => {
|
||||
wrapper = mount(ScrollingList, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
enableEdit: true,
|
||||
readyItemId: null,
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(wrapper.findAll('.edit-button').length).toBe(0)
|
||||
})
|
||||
|
||||
it('emits edit-item when edit button is clicked', async () => {
|
||||
wrapper = mount(ScrollingList, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
enableEdit: true,
|
||||
readyItemId: 'item-1',
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const editButton = wrapper.find('.edit-button')
|
||||
await editButton.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('edit-item')).toBeTruthy()
|
||||
expect(wrapper.emitted('edit-item')![0][0].id).toBe('item-1')
|
||||
})
|
||||
|
||||
it('resets ready state after edit button click', async () => {
|
||||
wrapper = mount(ScrollingList, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
enableEdit: true,
|
||||
readyItemId: 'item-1',
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const editButton = wrapper.find('.edit-button')
|
||||
await editButton.trigger('click')
|
||||
|
||||
const itemReadyEvents = wrapper.emitted('item-ready')
|
||||
expect(itemReadyEvents![itemReadyEvents!.length - 1]).toEqual([''])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Filter Function', () => {
|
||||
it('filters items using provided filterFn', async () => {
|
||||
const filterFn = (item: any) => item.points > 15
|
||||
|
||||
wrapper = mount(ScrollingList, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
filterFn,
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
expect(wrapper.findAll('.item-card').length).toBe(1) // Only item-2 with 20 points
|
||||
})
|
||||
})
|
||||
|
||||
describe('Refresh Method', () => {
|
||||
it('exposes refresh method', async () => {
|
||||
wrapper = mount(ScrollingList, { props: defaultProps })
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.vm.refresh).toBeDefined()
|
||||
expect(typeof wrapper.vm.refresh).toBe('function')
|
||||
})
|
||||
|
||||
it('refetches items when refresh is called', async () => {
|
||||
wrapper = mount(ScrollingList, { props: defaultProps })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
vi.clearAllMocks()
|
||||
|
||||
await wrapper.vm.refresh()
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith('/api/test?ids=item-1,item-2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Image Loading', () => {
|
||||
it('loads images for items with image_id', async () => {
|
||||
wrapper = mount(ScrollingList, { props: defaultProps })
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const { getCachedImageUrl } = await import('@/common/imageCache')
|
||||
expect(getCachedImageUrl).toHaveBeenCalledWith('img1')
|
||||
expect(getCachedImageUrl).toHaveBeenCalledWith('img2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom Item Classes', () => {
|
||||
it('applies custom classes from getItemClass prop', async () => {
|
||||
const getItemClass = (item: any) => ({
|
||||
good: item.points > 15,
|
||||
bad: item.points <= 15,
|
||||
})
|
||||
|
||||
wrapper = mount(ScrollingList, {
|
||||
props: {
|
||||
...defaultProps,
|
||||
getItemClass,
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
|
||||
const cards = wrapper.findAll('.item-card')
|
||||
expect(cards[0].classes()).toContain('bad') // item-1 with 10 points
|
||||
expect(cards[1].classes()).toContain('good') // item-2 with 20 points
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user