Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m56s
- Introduced a mechanism to hide the help button when modal dialogs are open by adding `helpButtonHidden` state in the tutorial controller. - Updated various components to set the help button visibility based on dialog states. - Added tests to verify help button visibility during reward and task confirmation dialogs. - Created new E2E tests for dialog help button functionality across different assignment views. - Refactored existing dialog components to utilize the new help button visibility logic. - Added unit tests for `RewardConfirmDialog` and `TaskConfirmDialog` to ensure correct titles are rendered based on task type. - Enhanced `HelpButton` component tests to validate visibility based on tutorial state.
721 lines
21 KiB
TypeScript
721 lines
21 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { mount, VueWrapper } from '@vue/test-utils'
|
|
import { nextTick } from 'vue'
|
|
import ChildView from '../ChildView.vue'
|
|
import { eventBus } from '@/common/eventBus'
|
|
import { helpButtonHidden } from '@/tutorial/controller'
|
|
|
|
// Mock dependencies
|
|
vi.mock('vue-router', () => ({
|
|
useRoute: vi.fn(() => ({
|
|
params: { id: 'child-123' },
|
|
})),
|
|
useRouter: vi.fn(() => ({
|
|
push: vi.fn(),
|
|
})),
|
|
}))
|
|
|
|
global.fetch = vi.fn()
|
|
|
|
describe('ChildView', () => {
|
|
let wrapper: VueWrapper<any>
|
|
|
|
const mockChild = {
|
|
id: 'child-123',
|
|
name: 'Test Child',
|
|
age: 8,
|
|
points: 50,
|
|
tasks: ['task-1', 'task-2'],
|
|
rewards: ['reward-1'],
|
|
image_id: 'boy01',
|
|
}
|
|
|
|
const mockChore = {
|
|
id: 'task-1',
|
|
name: 'Clean Room',
|
|
points: 10,
|
|
type: 'chore' as const,
|
|
image_url: '/images/task.png',
|
|
custom_value: null,
|
|
}
|
|
|
|
const mockChoreWithOverride = {
|
|
id: 'task-1',
|
|
name: 'Clean Room',
|
|
points: 10,
|
|
type: 'chore' as const,
|
|
image_url: '/images/task.png',
|
|
custom_value: 15,
|
|
}
|
|
|
|
const mockPenalty = {
|
|
id: 'task-2',
|
|
name: 'Hit Sibling',
|
|
points: 5,
|
|
type: 'penalty' as const,
|
|
image_url: '/images/penalty.png',
|
|
custom_value: null,
|
|
}
|
|
|
|
const mockPenaltyWithOverride = {
|
|
id: 'task-2',
|
|
name: 'Hit Sibling',
|
|
points: 5,
|
|
type: 'penalty' as const,
|
|
image_url: '/images/penalty.png',
|
|
custom_value: 8,
|
|
}
|
|
|
|
const mockRoutine = {
|
|
id: 'routine-1',
|
|
name: 'Morning Routine',
|
|
points: 12,
|
|
image_id: 'routine-image',
|
|
image_url: '/images/routine.png',
|
|
items: [
|
|
{
|
|
id: 'routine-item-1',
|
|
routine_id: 'routine-1',
|
|
name: 'Make Bed',
|
|
image_id: null,
|
|
order: 0,
|
|
},
|
|
],
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
|
|
// Mock fetch responses
|
|
;(global.fetch as any).mockImplementation((url: string) => {
|
|
if (url.includes('/child/child-123')) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve(mockChild),
|
|
})
|
|
}
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ tasks: [], reward_status: [] }),
|
|
})
|
|
})
|
|
|
|
// Mock speech synthesis
|
|
global.window.speechSynthesis = {
|
|
speak: vi.fn(),
|
|
cancel: vi.fn(),
|
|
} as any
|
|
global.window.SpeechSynthesisUtterance = vi.fn() as any
|
|
})
|
|
|
|
afterEach(() => {
|
|
if (wrapper) {
|
|
wrapper.unmount()
|
|
}
|
|
})
|
|
|
|
describe('Component Mounting', () => {
|
|
it('loads and displays child data on mount', async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
|
|
expect(global.fetch).toHaveBeenCalledWith('/api/child/child-123')
|
|
})
|
|
|
|
it('registers SSE event listeners on mount', async () => {
|
|
const onSpy = vi.spyOn(eventBus, 'on')
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
|
|
expect(onSpy).toHaveBeenCalledWith('child_task_triggered', expect.any(Function))
|
|
expect(onSpy).toHaveBeenCalledWith('child_reward_triggered', expect.any(Function))
|
|
expect(onSpy).toHaveBeenCalledWith('child_reward_request', expect.any(Function))
|
|
})
|
|
|
|
it('sets up inactivity timer on mount', async () => {
|
|
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
|
|
// Should set up inactivity timer (60 seconds)
|
|
expect(setTimeoutSpy).toHaveBeenCalled()
|
|
})
|
|
|
|
it('cleans up inactivity timer on unmount', async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
|
|
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')
|
|
wrapper.unmount()
|
|
|
|
expect(clearTimeoutSpy).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Custom Value Display - Chores', () => {
|
|
beforeEach(async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
})
|
|
|
|
it('displays default points for chore without override', () => {
|
|
// The template should display mockChore.points (10) when custom_value is null
|
|
// Template logic: item.custom_value !== undefined && item.custom_value !== null ? item.custom_value : item.points
|
|
const expectedValue = mockChore.points
|
|
expect(expectedValue).toBe(10)
|
|
})
|
|
|
|
it('displays custom_value for chore with override', () => {
|
|
// The template should display mockChoreWithOverride.custom_value (15)
|
|
const expectedValue = mockChoreWithOverride.custom_value
|
|
expect(expectedValue).toBe(15)
|
|
})
|
|
})
|
|
|
|
describe('Custom Value Display - Penalties', () => {
|
|
beforeEach(async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
})
|
|
|
|
it('displays negative default points for penalty without override', () => {
|
|
// The template should display -mockPenalty.points (-5)
|
|
// Template logic: item.custom_value !== undefined && item.custom_value !== null ? -item.custom_value : -item.points
|
|
const expectedValue = -mockPenalty.points
|
|
expect(expectedValue).toBe(-5)
|
|
})
|
|
|
|
it('displays negative custom_value for penalty with override', () => {
|
|
// The template should display -mockPenaltyWithOverride.custom_value (-8)
|
|
const expectedValue = -mockPenaltyWithOverride.custom_value!
|
|
expect(expectedValue).toBe(-8)
|
|
})
|
|
})
|
|
|
|
describe('Task Triggering', () => {
|
|
beforeEach(async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
})
|
|
|
|
it('speaks task name when triggered', () => {
|
|
wrapper.vm.triggerTask(mockChore)
|
|
|
|
expect(window.speechSynthesis.cancel).toHaveBeenCalled()
|
|
expect(window.speechSynthesis.speak).toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not call trigger-task API in child mode', async () => {
|
|
await wrapper.vm.triggerTask(mockChore)
|
|
|
|
expect(
|
|
(global.fetch as any).mock.calls.some((call: [string]) =>
|
|
call[0].includes('/trigger-task'),
|
|
),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('does not crash if speechSynthesis is not available', () => {
|
|
const originalSpeechSynthesis = global.window.speechSynthesis
|
|
delete (global.window as any).speechSynthesis
|
|
|
|
expect(() => wrapper.vm.triggerTask(mockChore)).not.toThrow()
|
|
|
|
// Restore for other tests
|
|
global.window.speechSynthesis = originalSpeechSynthesis
|
|
})
|
|
})
|
|
|
|
describe('Routine Overlay', () => {
|
|
beforeEach(async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
})
|
|
|
|
it('opens the routine overlay when a routine is clicked', async () => {
|
|
vi.useFakeTimers()
|
|
|
|
wrapper.vm.handleRoutineClick(mockRoutine)
|
|
vi.advanceTimersByTime(200)
|
|
await nextTick()
|
|
|
|
expect(wrapper.vm.showRoutineOverlay).toBe(true)
|
|
expect(wrapper.vm.dialogRoutine?.id).toBe('routine-1')
|
|
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('speaks the routine task name when a routine task is clicked', () => {
|
|
wrapper.vm.handleRoutineTaskClick(mockRoutine.items[0])
|
|
|
|
expect(window.speechSynthesis.cancel).toHaveBeenCalled()
|
|
expect(window.speechSynthesis.speak).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Reward Triggering', () => {
|
|
beforeEach(async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
})
|
|
|
|
it('speaks reward text when triggered', () => {
|
|
wrapper.vm.triggerReward({
|
|
id: 'reward-1',
|
|
name: 'Ice Cream',
|
|
cost: 50,
|
|
points_needed: 10,
|
|
redeeming: false,
|
|
})
|
|
|
|
expect(window.speechSynthesis.cancel).toHaveBeenCalled()
|
|
expect(window.speechSynthesis.speak).toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not call reward request/cancel APIs in child mode', () => {
|
|
wrapper.vm.triggerReward({
|
|
id: 'reward-1',
|
|
name: 'Ice Cream',
|
|
cost: 50,
|
|
points_needed: 0,
|
|
redeeming: false,
|
|
})
|
|
|
|
const requestCalls = (global.fetch as any).mock.calls.filter(
|
|
(call: [string]) =>
|
|
call[0].includes('/request-reward') || call[0].includes('/cancel-request-reward'),
|
|
)
|
|
expect(requestCalls.length).toBe(0)
|
|
})
|
|
|
|
it('opens redeem dialog when reward is ready and not pending', async () => {
|
|
vi.useFakeTimers()
|
|
wrapper.vm.triggerReward({
|
|
id: 'reward-1',
|
|
name: 'Ice Cream',
|
|
cost: 50,
|
|
points_needed: 0,
|
|
redeeming: false,
|
|
})
|
|
vi.advanceTimersByTime(200)
|
|
await nextTick()
|
|
|
|
expect(wrapper.vm.showRewardDialog).toBe(true)
|
|
expect(wrapper.vm.showCancelDialog).toBe(false)
|
|
expect(wrapper.vm.dialogReward?.id).toBe('reward-1')
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('does not open redeem dialog when reward is not yet ready', () => {
|
|
wrapper.vm.triggerReward({
|
|
id: 'reward-1',
|
|
name: 'Ice Cream',
|
|
cost: 50,
|
|
points_needed: 10,
|
|
redeeming: false,
|
|
})
|
|
|
|
expect(wrapper.vm.showRewardDialog).toBe(false)
|
|
expect(wrapper.vm.showCancelDialog).toBe(false)
|
|
})
|
|
|
|
it('opens cancel dialog when reward is already pending', async () => {
|
|
vi.useFakeTimers()
|
|
wrapper.vm.triggerReward({
|
|
id: 'reward-1',
|
|
name: 'Ice Cream',
|
|
cost: 50,
|
|
points_needed: 0,
|
|
redeeming: true,
|
|
})
|
|
vi.advanceTimersByTime(200)
|
|
await nextTick()
|
|
|
|
expect(wrapper.vm.showCancelDialog).toBe(true)
|
|
expect(wrapper.vm.showRewardDialog).toBe(false)
|
|
expect(wrapper.vm.dialogReward?.id).toBe('reward-1')
|
|
vi.useRealTimers()
|
|
})
|
|
})
|
|
|
|
describe('Reward Redeem Dialog', () => {
|
|
const readyReward = {
|
|
id: 'reward-1',
|
|
name: 'Ice Cream',
|
|
cost: 50,
|
|
points_needed: 0,
|
|
redeeming: false,
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
vi.useFakeTimers()
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
vi.advanceTimersByTime(100)
|
|
await nextTick()
|
|
wrapper.vm.triggerReward(readyReward)
|
|
vi.advanceTimersByTime(200)
|
|
await nextTick()
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('closes redeem dialog on cancelRedeemReward', async () => {
|
|
expect(wrapper.vm.showRewardDialog).toBe(true)
|
|
wrapper.vm.cancelRedeemReward()
|
|
expect(wrapper.vm.showRewardDialog).toBe(false)
|
|
expect(wrapper.vm.dialogReward).toBe(null)
|
|
})
|
|
|
|
it('calls request-reward API on confirmRedeemReward', async () => {
|
|
;(global.fetch as any).mockResolvedValueOnce({ ok: true })
|
|
|
|
await wrapper.vm.confirmRedeemReward()
|
|
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
`/api/child/child-123/request-reward`,
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
body: JSON.stringify({ reward_id: 'reward-1' }),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('closes redeem dialog after confirmRedeemReward', async () => {
|
|
;(global.fetch as any).mockResolvedValueOnce({ ok: true })
|
|
|
|
await wrapper.vm.confirmRedeemReward()
|
|
await nextTick()
|
|
|
|
expect(wrapper.vm.showRewardDialog).toBe(false)
|
|
expect(wrapper.vm.dialogReward).toBe(null)
|
|
})
|
|
})
|
|
|
|
describe('Help FAB visibility during reward redeem dialog', () => {
|
|
beforeEach(() => {
|
|
helpButtonHidden.value = false
|
|
wrapper = mount(ChildView)
|
|
})
|
|
|
|
it('hides the help button while the reward redeem dialog is open', async () => {
|
|
wrapper.vm.showRewardDialog = true
|
|
await nextTick()
|
|
expect(helpButtonHidden.value).toBe(true)
|
|
|
|
wrapper.vm.showRewardDialog = false
|
|
await nextTick()
|
|
expect(helpButtonHidden.value).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Cancel Pending Reward Dialog', () => {
|
|
const pendingReward = {
|
|
id: 'reward-1',
|
|
name: 'Ice Cream',
|
|
cost: 50,
|
|
points_needed: 0,
|
|
redeeming: true,
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
vi.useFakeTimers()
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
vi.advanceTimersByTime(100)
|
|
await nextTick()
|
|
wrapper.vm.triggerReward(pendingReward)
|
|
vi.advanceTimersByTime(200)
|
|
await nextTick()
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('closes cancel dialog on closeCancelDialog', async () => {
|
|
expect(wrapper.vm.showCancelDialog).toBe(true)
|
|
wrapper.vm.closeCancelDialog()
|
|
expect(wrapper.vm.showCancelDialog).toBe(false)
|
|
expect(wrapper.vm.dialogReward).toBe(null)
|
|
})
|
|
|
|
it('calls cancel-request-reward API on cancelPendingReward', async () => {
|
|
;(global.fetch as any).mockResolvedValueOnce({ ok: true })
|
|
|
|
await wrapper.vm.cancelPendingReward()
|
|
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
`/api/child/child-123/cancel-request-reward`,
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
body: JSON.stringify({ reward_id: 'reward-1' }),
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('closes cancel dialog after cancelPendingReward', async () => {
|
|
;(global.fetch as any).mockResolvedValueOnce({ ok: true })
|
|
|
|
await wrapper.vm.cancelPendingReward()
|
|
await nextTick()
|
|
|
|
expect(wrapper.vm.showCancelDialog).toBe(false)
|
|
expect(wrapper.vm.dialogReward).toBe(null)
|
|
})
|
|
})
|
|
|
|
describe('SSE Event Handlers', () => {
|
|
beforeEach(async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
})
|
|
|
|
it('handles child_task_triggered event and refreshes reward list', async () => {
|
|
const mockRefresh = vi.fn()
|
|
wrapper.vm.childRewardListRef = { refresh: mockRefresh }
|
|
|
|
wrapper.vm.handleTaskTriggered({
|
|
type: 'child_task_triggered',
|
|
payload: { child_id: 'child-123', points: 60, task_id: 'task-1' },
|
|
})
|
|
|
|
expect(wrapper.vm.child.points).toBe(60)
|
|
expect(mockRefresh).toHaveBeenCalled()
|
|
})
|
|
|
|
it('handles child_reward_triggered event', async () => {
|
|
const mockRefresh = vi.fn()
|
|
wrapper.vm.childRewardListRef = { refresh: mockRefresh }
|
|
|
|
wrapper.vm.handleRewardTriggered({
|
|
type: 'child_reward_triggered',
|
|
payload: { child_id: 'child-123', points: 40, reward_id: 'reward-1' },
|
|
})
|
|
|
|
expect(wrapper.vm.child.points).toBe(40)
|
|
expect(mockRefresh).toHaveBeenCalled()
|
|
})
|
|
|
|
it('handles child_tasks_set event', () => {
|
|
wrapper.vm.handleChildTaskSet({
|
|
type: 'child_tasks_set',
|
|
payload: { child_id: 'child-123', task_ids: ['task-1', 'task-3'] },
|
|
})
|
|
|
|
expect(wrapper.vm.tasks).toEqual(['task-1', 'task-3'])
|
|
})
|
|
|
|
it('handles child_rewards_set event', () => {
|
|
wrapper.vm.handleChildRewardSet({
|
|
type: 'child_rewards_set',
|
|
payload: { child_id: 'child-123', reward_ids: ['reward-1', 'reward-2'] },
|
|
})
|
|
|
|
expect(wrapper.vm.rewards).toEqual(['reward-1', 'reward-2'])
|
|
})
|
|
|
|
it('handles reward_modified event and refreshes reward list', async () => {
|
|
const mockRefresh = vi.fn()
|
|
wrapper.vm.childRewardListRef = { refresh: mockRefresh }
|
|
|
|
wrapper.vm.handleRewardModified({
|
|
type: 'reward_modified',
|
|
payload: { reward_id: 'reward-1', operation: 'EDIT' },
|
|
})
|
|
|
|
expect(mockRefresh).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Inactivity Timer', () => {
|
|
beforeEach(async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
})
|
|
|
|
it('resets timer on user interaction', () => {
|
|
const setTimeoutSpy = vi.spyOn(global, 'setTimeout')
|
|
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')
|
|
|
|
wrapper.vm.resetInactivityTimer()
|
|
|
|
expect(clearTimeoutSpy).toHaveBeenCalled()
|
|
expect(setTimeoutSpy).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Reward Request Handling', () => {
|
|
beforeEach(async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
})
|
|
|
|
it('handles reward request event and refreshes list', async () => {
|
|
const mockRefresh = vi.fn()
|
|
wrapper.vm.childRewardListRef = { refresh: mockRefresh }
|
|
|
|
wrapper.vm.handleRewardRequest({
|
|
type: 'child_reward_request',
|
|
payload: { child_id: 'child-123', reward_id: 'reward-1' },
|
|
})
|
|
|
|
expect(mockRefresh).toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not refresh if reward not in child rewards', async () => {
|
|
const mockRefresh = vi.fn()
|
|
wrapper.vm.childRewardListRef = { refresh: mockRefresh }
|
|
|
|
wrapper.vm.handleRewardRequest({
|
|
type: 'child_reward_request',
|
|
payload: { child_id: 'child-123', reward_id: 'reward-999' },
|
|
})
|
|
|
|
expect(mockRefresh).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('Item Ready State Management', () => {
|
|
beforeEach(async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
})
|
|
|
|
it('initializes readyItemId to null', () => {
|
|
expect(wrapper.vm.readyItemId).toBe(null)
|
|
})
|
|
|
|
it('updates readyItemId when handleItemReady is called with an item ID', () => {
|
|
wrapper.vm.handleItemReady('task-1')
|
|
expect(wrapper.vm.readyItemId).toBe('task-1')
|
|
|
|
wrapper.vm.handleItemReady('reward-2')
|
|
expect(wrapper.vm.readyItemId).toBe('reward-2')
|
|
})
|
|
|
|
it('clears readyItemId when handleItemReady is called with empty string', () => {
|
|
wrapper.vm.readyItemId = 'task-1'
|
|
wrapper.vm.handleItemReady('')
|
|
expect(wrapper.vm.readyItemId).toBe('')
|
|
})
|
|
|
|
it('passes readyItemId prop to Chores ScrollingList', async () => {
|
|
wrapper.vm.readyItemId = 'task-1'
|
|
await nextTick()
|
|
|
|
const choresScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[0]
|
|
expect(choresScrollingList.props('readyItemId')).toBe('task-1')
|
|
})
|
|
|
|
it('passes readyItemId prop to Penalties ScrollingList', async () => {
|
|
wrapper.vm.readyItemId = 'task-2'
|
|
await nextTick()
|
|
|
|
const penaltiesScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[1]
|
|
expect(penaltiesScrollingList.props('readyItemId')).toBe('task-2')
|
|
})
|
|
|
|
it('passes readyItemId prop to Rewards ScrollingList', async () => {
|
|
wrapper.vm.readyItemId = 'reward-1'
|
|
await nextTick()
|
|
|
|
const rewardsScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[2]
|
|
expect(rewardsScrollingList.props('readyItemId')).toBe('reward-1')
|
|
})
|
|
|
|
it('handles item-ready event from Chores ScrollingList', async () => {
|
|
const choresScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[0]
|
|
|
|
choresScrollingList.vm.$emit('item-ready', 'task-1')
|
|
await nextTick()
|
|
|
|
expect(wrapper.vm.readyItemId).toBe('task-1')
|
|
})
|
|
|
|
it('handles item-ready event from Penalties ScrollingList', async () => {
|
|
const penaltiesScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[1]
|
|
|
|
penaltiesScrollingList.vm.$emit('item-ready', 'task-2')
|
|
await nextTick()
|
|
|
|
expect(wrapper.vm.readyItemId).toBe('task-2')
|
|
})
|
|
|
|
it('handles item-ready event from Rewards ScrollingList', async () => {
|
|
const rewardsScrollingList = wrapper.findAllComponents({ name: 'ScrollingList' })[2]
|
|
|
|
rewardsScrollingList.vm.$emit('item-ready', 'reward-1')
|
|
await nextTick()
|
|
|
|
expect(wrapper.vm.readyItemId).toBe('reward-1')
|
|
})
|
|
|
|
it('maintains 2-step click workflow: first click sets ready, second click triggers', async () => {
|
|
// Initial state
|
|
expect(wrapper.vm.readyItemId).toBe(null)
|
|
|
|
// First click - item should become ready
|
|
wrapper.vm.handleItemReady('task-1')
|
|
expect(wrapper.vm.readyItemId).toBe('task-1')
|
|
|
|
// Second click would trigger the item (tested via ScrollingList component)
|
|
// After trigger, ready state should be cleared
|
|
wrapper.vm.handleItemReady('')
|
|
expect(wrapper.vm.readyItemId).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('choreDueLabel', () => {
|
|
beforeEach(async () => {
|
|
wrapper = mount(ChildView)
|
|
await nextTick()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
})
|
|
|
|
it('returns null when item has no schedule', () => {
|
|
const result = wrapper.vm.choreDueLabel({
|
|
id: 'task-1',
|
|
name: 'Test',
|
|
points: 5,
|
|
type: 'chore' as const,
|
|
schedule: null,
|
|
})
|
|
expect(result).toBe(null)
|
|
})
|
|
|
|
it('returns "Due by ..." prefix format when chore has a future due time today', () => {
|
|
vi.useFakeTimers()
|
|
// Feb 24 2026 is a Tuesday (day index 2) at 10:00am
|
|
vi.setSystemTime(new Date('2026-02-24T10:00:00'))
|
|
try {
|
|
const item = {
|
|
id: 'task-1',
|
|
name: 'Test',
|
|
points: 5,
|
|
type: 'chore' as const,
|
|
schedule: {
|
|
mode: 'days' as const,
|
|
day_configs: [{ day: 2, hour: 14, minute: 30 }], // Tuesday 2:30pm — future
|
|
interval_days: null,
|
|
anchor_weekday: null,
|
|
interval_hour: null,
|
|
interval_minute: null,
|
|
},
|
|
extension_date: null,
|
|
}
|
|
const result = wrapper.vm.choreDueLabel(item)
|
|
expect(result).toMatch(/^Due by /)
|
|
} finally {
|
|
vi.useRealTimers()
|
|
}
|
|
})
|
|
})
|
|
})
|