feat: Модуль Финансы + Трекер + CI/CD #1

Merged
daniil merged 15 commits from dev into main 2026-03-01 05:14:59 +00:00
2 changed files with 180 additions and 0 deletions
Showing only changes of commit bacacb757d - Show all commits

View File

@@ -0,0 +1,116 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
// Mock client module
vi.mock('../api/client', () => {
return {
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
}
}
})
import client from '../api/client'
import { financeApi } from '../api/finance'
beforeEach(() => {
vi.clearAllMocks()
})
describe('financeApi', () => {
// Categories
describe('categories', () => {
it('listCategories calls GET finance/categories', async () => {
const mockData = [{ id: 1, name: 'Еда', emoji: '🍔' }]
client.get.mockResolvedValue({ data: mockData })
const result = await financeApi.listCategories()
expect(client.get).toHaveBeenCalledWith('finance/categories')
expect(result).toEqual(mockData)
})
it('createCategory calls POST finance/categories', async () => {
const input = { name: 'Test', type: 'expense', emoji: '🧪' }
const mockData = { id: 1, ...input }
client.post.mockResolvedValue({ data: mockData })
const result = await financeApi.createCategory(input)
expect(client.post).toHaveBeenCalledWith('finance/categories', input)
expect(result).toEqual(mockData)
})
it('updateCategory calls PUT finance/categories/:id', async () => {
const data = { name: 'Updated' }
client.put.mockResolvedValue({ data: { id: 5, ...data } })
const result = await financeApi.updateCategory(5, data)
expect(client.put).toHaveBeenCalledWith('finance/categories/5', data)
expect(result.name).toBe('Updated')
})
it('deleteCategory calls DELETE finance/categories/:id', async () => {
client.delete.mockResolvedValue({})
await financeApi.deleteCategory(3)
expect(client.delete).toHaveBeenCalledWith('finance/categories/3')
})
})
// Transactions
describe('transactions', () => {
it('listTransactions calls GET with params', async () => {
const mockData = [{ id: 1, amount: 500 }]
client.get.mockResolvedValue({ data: mockData })
const params = { month: 3, year: 2026 }
const result = await financeApi.listTransactions(params)
expect(client.get).toHaveBeenCalledWith('finance/transactions', { params })
expect(result).toEqual(mockData)
})
it('listTransactions works without params', async () => {
client.get.mockResolvedValue({ data: [] })
const result = await financeApi.listTransactions()
expect(client.get).toHaveBeenCalledWith('finance/transactions', { params: {} })
expect(result).toEqual([])
})
it('createTransaction calls POST', async () => {
const input = { amount: 100, type: 'expense', category_id: 1, date: '2026-03-01' }
client.post.mockResolvedValue({ data: { id: 1, ...input } })
const result = await financeApi.createTransaction(input)
expect(client.post).toHaveBeenCalledWith('finance/transactions', input)
expect(result.amount).toBe(100)
})
it('updateTransaction calls PUT', async () => {
const data = { amount: 200 }
client.put.mockResolvedValue({ data: { id: 7, ...data } })
const result = await financeApi.updateTransaction(7, data)
expect(client.put).toHaveBeenCalledWith('finance/transactions/7', data)
expect(result.amount).toBe(200)
})
it('deleteTransaction calls DELETE', async () => {
client.delete.mockResolvedValue({})
await financeApi.deleteTransaction(10)
expect(client.delete).toHaveBeenCalledWith('finance/transactions/10')
})
})
// Summary & Analytics
describe('summary & analytics', () => {
it('getSummary calls GET with params', async () => {
const mockData = { balance: 5000, total_income: 10000, total_expense: 5000 }
client.get.mockResolvedValue({ data: mockData })
const result = await financeApi.getSummary({ month: 3, year: 2026 })
expect(client.get).toHaveBeenCalledWith('finance/summary', { params: { month: 3, year: 2026 } })
expect(result.balance).toBe(5000)
})
it('getAnalytics calls GET with params', async () => {
const mockData = { monthly_trend: [], avg_daily_expense: 500 }
client.get.mockResolvedValue({ data: mockData })
const result = await financeApi.getAnalytics({ months: 6 })
expect(client.get).toHaveBeenCalledWith('finance/analytics', { params: { months: 6 } })
expect(result.avg_daily_expense).toBe(500)
})
})
})

View File

@@ -0,0 +1,64 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import Tracker from '../pages/Tracker'
// Mock child pages
vi.mock('../pages/Habits', () => ({
default: ({ embedded }) => <div data-testid="habits">Habits {embedded ? 'embedded' : ''}</div>
}))
vi.mock('../pages/Tasks', () => ({
default: ({ embedded }) => <div data-testid="tasks">Tasks {embedded ? 'embedded' : ''}</div>
}))
vi.mock('../pages/Stats', () => ({
default: ({ embedded }) => <div data-testid="stats">Stats {embedded ? 'embedded' : ''}</div>
}))
vi.mock('../components/Navigation', () => ({
default: () => <nav data-testid="navigation">Nav</nav>
}))
describe('Tracker', () => {
it('renders header with title', () => {
render(<Tracker />)
expect(screen.getByText(/Трекер/)).toBeInTheDocument()
})
it('renders all three tab buttons', () => {
render(<Tracker />)
expect(screen.getByText(/Привычки/)).toBeInTheDocument()
expect(screen.getByText(/Задачи/)).toBeInTheDocument()
expect(screen.getByText(/Статистика/)).toBeInTheDocument()
})
it('shows Habits tab by default', () => {
render(<Tracker />)
expect(screen.getByTestId('habits')).toBeInTheDocument()
expect(screen.queryByTestId('tasks')).not.toBeInTheDocument()
expect(screen.queryByTestId('stats')).not.toBeInTheDocument()
})
it('switches to Tasks tab on click', () => {
render(<Tracker />)
fireEvent.click(screen.getByText(/Задачи/))
expect(screen.getByTestId('tasks')).toBeInTheDocument()
expect(screen.queryByTestId('habits')).not.toBeInTheDocument()
})
it('switches to Stats tab on click', () => {
render(<Tracker />)
fireEvent.click(screen.getByText(/Статистика/))
expect(screen.getByTestId('stats')).toBeInTheDocument()
expect(screen.queryByTestId('habits')).not.toBeInTheDocument()
})
it('switches back to Habits', () => {
render(<Tracker />)
fireEvent.click(screen.getByText(/Задачи/))
fireEvent.click(screen.getByText(/Привычки/))
expect(screen.getByTestId('habits')).toBeInTheDocument()
})
it('renders navigation', () => {
render(<Tracker />)
expect(screen.getByTestId('navigation')).toBeInTheDocument()
})
})