Files
finance/frontend/src/test/ledger-view.test.tsx
T
k1nq f5ae63e158
Gitea Actions CI / detect (push) Successful in 33s
Gitea Actions CI / Backend (lint / type / test / build) (push) Failing after 28s
Gitea Actions CI / Compose & nginx validation (push) Failing after 1m8s
Gitea Actions CI / Frontend (lint / type / test / build) (push) Successful in 50s
Update manifest.json with new file metadata and add new test files
- Updated timestamps and AST hashes for existing files in the backend and frontend.
- Added new test files: `backend/tests/test_review_queue_batch.py`, `frontend/src/test/review-queue-save.test.tsx`, `frontend/src/test/app-layout.test.tsx`, and `frontend/src/test/ledger-view.test.tsx`.
- Updated documentation file `docs/tasks/0001-fixes.md` with new metadata.
2026-09-23 15:06:45 +05:00

156 lines
5.0 KiB
TypeScript

import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import userEvent from '@testing-library/user-event'
import { screen, within } from '@testing-library/react'
import { api } from '@/api/endpoints'
import type { Account, BudgetCategory, Operation } from '@/api/types'
import { OperationsPage } from '@/pages/OperationsPage'
import { renderWithProviders } from './helpers'
vi.mock('@/api/endpoints', () => ({
api: {
accounts: {
list: vi.fn(),
create: vi.fn(),
archive: vi.fn(),
adjustBalance: vi.fn(),
},
operations: {
list: vi.fn(),
create: vi.fn(),
void: vi.fn(),
categorize: vi.fn(),
savingsTransfer: vi.fn(),
savingsTransferRevert: vi.fn(),
},
budget: {
summary: vi.fn(),
categories: vi.fn(),
updatePlan: vi.fn(),
fund: vi.fn(),
close: vi.fn(),
reopen: vi.fn(),
copyPrevious: vi.fn(),
categoryDefinitions: vi.fn(),
createCategory: vi.fn(),
updateCategoryDefinition: vi.fn(),
},
},
}))
const listAccountsMock = vi.mocked(api.accounts.list)
const listOperationsMock = vi.mocked(api.operations.list)
const budgetCategoriesMock = vi.mocked(api.budget.categories)
function account(overrides: Partial<Account> = {}): Account {
return {
id: 'a1',
name: 'Alfa Card',
type: 'debit',
status: 'active',
bankCode: null,
maskedNumber: null,
openedOn: '2026-01-15',
currency: 'RUB',
systemKey: null,
nameKey: null,
balanceKopecks: 123456,
...overrides,
}
}
function operation(overrides: Partial<Operation> & { id: string }): Operation {
return {
kind: 'expense',
status: 'confirmed',
amountKopecks: 500000,
occurredOn: '2026-09-05',
postedOn: null,
accountId: 'a1',
counterpartyAccountId: null,
categoryId: null,
categoryName: null,
categoryKey: null,
payeeName: 'Coffee shop',
incomeSourceName: null,
description: null,
parentOperationId: null,
convertedFromExpense: false,
version: 1,
createdAt: '2026-09-05T10:00:00Z',
...overrides,
}
}
describe('OperationsPage filter bar, row actions and create menu', () => {
beforeEach(() => {
listAccountsMock.mockResolvedValue([account()])
listOperationsMock.mockResolvedValue({
items: [operation({ id: 'op-1' })],
nextCursor: null,
})
budgetCategoriesMock.mockResolvedValue([] as BudgetCategory[])
})
afterEach(() => {
vi.clearAllMocks()
})
it('renders a single filter bar with search and equal compact date filters', () => {
const { container } = renderWithProviders(<OperationsPage />)
const form = container.querySelector('form.filters')
expect(form).not.toBeNull()
const search = form?.querySelector('input[type="search"]')
expect(search).toHaveAttribute('aria-label', 'Search')
const selects = Array.from(form?.querySelectorAll('select') ?? [])
expect(selects.map((select) => select.getAttribute('aria-label'))).toEqual([
'Account',
'Type',
'Status',
])
const dates = Array.from(form?.querySelectorAll('input[type="date"]') ?? [])
expect(dates).toHaveLength(2)
const [fromDate, toDate] = dates
if (!fromDate || !toDate) throw new Error('date filters missing')
expect(fromDate).toHaveAttribute('aria-label', 'From date')
expect(toDate).toHaveAttribute('aria-label', 'To date')
expect(fromDate.className).toBe(toDate.className)
expect(fromDate.className).toContain('filter-date')
expect(fromDate.closest('.filter-date-field')).toHaveTextContent('From date')
expect(toDate.closest('.filter-date-field')).toHaveTextContent('To date')
})
it('keeps Void and To savings adjacent in one action line', async () => {
renderWithProviders(<OperationsPage />)
const row = (await screen.findByText('Coffee shop')).closest('tr') as HTMLTableRowElement
const actions = row.querySelector('.operation-actions .inline-actions')
expect(actions).not.toBeNull()
const buttons = Array.from(actions?.querySelectorAll('button') ?? [])
expect(buttons.map((button) => button.textContent)).toEqual(['To savings', 'Void'])
for (const button of buttons) {
expect(button.className).toContain('btn')
}
})
it('opens the create menu as a right-hand drawer with white type options', async () => {
renderWithProviders(<OperationsPage />)
await userEvent.click(screen.getByRole('button', { name: '+ New operation' }))
const dialog = await screen.findByRole('dialog', { name: 'New operation' })
expect(dialog).toHaveClass('modal')
const group = within(dialog).getByRole('group', { name: 'Type' })
const options = Array.from(group.querySelectorAll('button'))
expect(options.map((option) => option.textContent)).toEqual([
'Expense',
'Income',
'Transfer',
'Refund',
])
const [activeOption] = options
if (!activeOption) throw new Error('type options missing')
expect(activeOption.getAttribute('aria-pressed')).toBe('true')
})
})