Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions frontend/src/components/Shared/DateRangePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,11 @@ const applyPreset = (preset) => {

const applyRange = () => {
if (isValidRange.value) {
emit('update:range', {
start: tempRange.value.start,
end: tempRange.value.end
});
// Ensure we always emit Date objects even if input gave us strings
const start = tempRange.value.start instanceof Date ? tempRange.value.start : new Date(tempRange.value.start);
const end = tempRange.value.end instanceof Date ? tempRange.value.end : new Date(tempRange.value.end);

emit('update:range', { start, end });
closePicker();
}
};
Expand Down
16 changes: 8 additions & 8 deletions frontend/tests/components/DateRangePicker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ describe('DateRangePicker.vue', () => {
expect(emitted).toBeTruthy();

const { start, end } = emitted[0][0];
// Our stubbed DatePicker emits strings because we use setValue on an input
// But the component logic should ideally convert them if it was doing so.
// Wait, the component logic only converts PROPS to Dates in togglePicker.
// Manual selection in the DatePicker stub sets tempRange.start/end directly via v-model.
// In the real app, PrimeVue DatePicker v-model would be a Date object.
// For the test, we'll verify it's at least truthy and matches our input.
expect(start).toBe('2026-01-01');
expect(end).toBe('2026-01-31');

// Component now ensures Date objects are emitted
expect(start).toBeInstanceOf(Date);
expect(end).toBeInstanceOf(Date);

// Verify formatted values match our inputs
expect(start.toISOString().split('T')[0]).toBe('2026-01-01');
expect(end.toISOString().split('T')[0]).toBe('2026-01-31');
});

it('emits Date objects when Last 30 Days preset is selected', async () => {
Expand Down
98 changes: 98 additions & 0 deletions frontend/tests/views/SpendingAnalysisView.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { mount } from '@vue/test-utils';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
import SpendingAnalysisView from '@/views/SpendingAnalysisView.vue';
import PrimeVue from 'primevue/config';
import { useApi } from '@/services/apiInstance';
import { useAuthStore } from '@/stores/auth';

// Mock PrimeVue components
const stubs = {
Card: { template: '<div><slot name="title" /><slot name="subtitle" /><slot name="content" /></div>' },
Button: { template: '<button @click="$emit(\'click\')"><slot /></button>' },
DataTable: { template: '<table><slot /></table>' },
Column: { template: '<td><slot /></td>' },
ProgressBar: { template: '<div><slot /></div>' },
Tag: { template: '<span><slot /></span>' },
ProgressSpinner: { template: '<div></div>' },
AppChart: { template: '<div>Chart</div>' },
DateRangePicker: {
name: 'DateRangePicker',
template: '<div @click="$emit(\'update:range\', { start: new Date(), end: new Date() })">Picker</div>',
props: ['startDate', 'endDate']
}
};

vi.mock('@/services/apiInstance', () => ({
useApi: vi.fn()
}));

vi.mock('@/stores/auth', () => ({
useAuthStore: vi.fn()
}));

vi.mock('vue-router', () => ({
useRouter: vi.fn(() => ({
push: vi.fn()
}))
}));

describe('SpendingAnalysisView.vue', () => {
let mockApi;
let authStore;

beforeEach(() => {
setActivePinia(createPinia());

mockApi = {
fetchCategorySpendingData: vi.fn().mockResolvedValue({
statistics: { totalSpending: 1000 },
categoryData: []
}),
fetchMonthlySpendingDataWithRange: vi.fn().mockResolvedValue({
statistics: { totalSpending: 5000, yoyChange: 10 }
})
};
useApi.mockReturnValue(mockApi);

authStore = {
user: { name: 'Test User' },
getToken: vi.fn().mockResolvedValue('token')
};
useAuthStore.mockReturnValue(authStore);

vi.clearAllMocks();
});

it('refreshes data when DateRangePicker emits Date objects', async () => {
const wrapper = mount(SpendingAnalysisView, {
global: {
plugins: [PrimeVue],
stubs
}
});

// Use UTC to avoid timezone fragility
const startDate = new Date(Date.UTC(2023, 9, 1)); // Oct 1, 2023
const endDate = new Date(Date.UTC(2023, 9, 31)); // Oct 31, 2023

const picker = wrapper.findComponent({ name: 'DateRangePicker' });
await picker.vm.$emit('update:range', { start: startDate, end: endDate });

// Should be formatted as YYYY-MM-DD
expect(mockApi.fetchCategorySpendingData).toHaveBeenCalledWith('2023-10-01', '2023-10-31');
});

it('correctly handles the initial data load on mount', async () => {
mount(SpendingAnalysisView, {
global: {
plugins: [PrimeVue],
stubs
}
});

// fetchData is called in onMounted
expect(mockApi.fetchCategorySpendingData).toHaveBeenCalled();
expect(mockApi.fetchMonthlySpendingDataWithRange).toHaveBeenCalled();
});
});
Loading