Files
portal/app/tests/Frontend/TransactionsTable.spec.ts
T
Дмитрий 6cc8cd86ef
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
fix(billing): столбец «Операция» в обзоре — ярлык по типу операции — F4
История транзакций в обзоре биллинга показывала пустой столбец «Операция»:
списания за лид LedgerService создаёт без description, а таблица выводила
поле как есть без запасного текста. Добавлен ярлык по типу операции
с приоритетом сохранённого description. Косметика отображения,
денежных значений не касается. TDD: 2 vitest, 955 passed / 3 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:07:26 +03:00

124 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import TransactionsTable from '../../resources/js/components/billing/TransactionsTable.vue';
import * as billingApi from '../../resources/js/api/billing';
import type { BillingTransaction, TransactionsPage } from '../../resources/js/api/billing';
vi.mock('../../resources/js/api/billing');
const vuetify = createVuetify();
function txn(over: Partial<BillingTransaction> = {}): BillingTransaction {
return {
id: 1,
code: 'TX-1',
type: 'topup',
description: 'Пополнение баланса',
amount_rub: '5000.00',
amount_leads: 0,
balance_rub_after: '5000.00',
display_amount_rub: '5000.00',
created_at: '2026-05-10T14:21:00Z',
...over,
};
}
function makePage(txns: BillingTransaction[]): TransactionsPage {
return { data: txns, meta: { current_page: 1, last_page: 1, total: txns.length, per_page: 20 } };
}
describe('TransactionsTable.vue', () => {
beforeEach(() => {
vi.mocked(billingApi.getTransactions).mockResolvedValue(makePage([txn()]));
});
it('загружает транзакции при монтировании', async () => {
const wrapper = mount(TransactionsTable, { global: { plugins: [vuetify] } });
await flushPromises();
expect(billingApi.getTransactions).toHaveBeenCalled();
expect((wrapper.vm as unknown as { total: number }).total).toBe(1);
});
it('смена таба «Пополнения» шлёт type=topup', async () => {
const wrapper = mount(TransactionsTable, { global: { plugins: [vuetify] } });
await flushPromises();
await (wrapper.vm as unknown as { changeTab: (id: string) => Promise<void> }).changeTab('topup');
expect(billingApi.getTransactions).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'topup' }));
});
it('таб «Все» не шлёт type', async () => {
const wrapper = mount(TransactionsTable, { global: { plugins: [vuetify] } });
await flushPromises();
await (wrapper.vm as unknown as { changeTab: (id: string) => Promise<void> }).changeTab('all');
const lastCall = vi.mocked(billingApi.getTransactions).mock.calls.at(-1)?.[0];
expect(lastCall).not.toHaveProperty('type');
});
it('показывает error-alert при сбое', async () => {
vi.mocked(billingApi.getTransactions).mockRejectedValue(new Error('fail'));
const wrapper = mount(TransactionsTable, { global: { plugins: [vuetify] } });
await flushPromises();
expect(wrapper.text()).toContain('Не удалось загрузить транзакции');
});
it('does NOT render «Возвраты» tab', async () => {
const wrapper = mount(TransactionsTable, { global: { plugins: [vuetify] } });
await flushPromises();
expect(wrapper.text()).not.toContain('Возвраты');
});
it('uses display_amount_rub for amount column on historic prepaid rows', async () => {
// historic prepaid row: amount_rub='0.00', amount_leads=null, display_amount_rub='0.00'
vi.mocked(billingApi.getTransactions).mockResolvedValue(
makePage([
txn({
type: 'historical_import',
amount_rub: '0.00',
amount_leads: null,
display_amount_rub: '0.00',
}),
]),
);
const wrapper = mount(TransactionsTable, { global: { plugins: [vuetify] } });
await flushPromises();
// Old code would have shown ' 1 лид.' for amount_leads=-1; new code uses display_amount_rub
// With display_amount_rub='0.00' we expect '0' somewhere in the amount cell (formatted as 0 ₽)
expect(wrapper.text()).not.toContain('лид.');
});
it('показывает ярлык «Списание за лид» когда description пуст (lead_charge)', async () => {
// Корень F4: LedgerService создаёт lead_charge-транзакции без description →
// столбец «Операция» был пустым. Фолбэк выводит ярлык по типу операции.
vi.mocked(billingApi.getTransactions).mockResolvedValue(
makePage([
txn({
type: 'lead_charge',
description: null,
amount_rub: '-500.00',
display_amount_rub: '-500.00',
}),
]),
);
const wrapper = mount(TransactionsTable, { global: { plugins: [vuetify] } });
await flushPromises();
expect(wrapper.text()).toContain('Списание за лид');
});
it('сохраняет явный description, когда он задан (пополнение)', async () => {
const wrapper = mount(TransactionsTable, { global: { plugins: [vuetify] } });
await flushPromises();
// default txn(): type=topup, description='Пополнение баланса' — фолбэк не перетирает.
expect(wrapper.text()).toContain('Пополнение баланса');
});
it('formats date with 2-digit year', async () => {
const wrapper = mount(TransactionsTable, { global: { plugins: [vuetify] } });
await flushPromises();
// Date '2026-05-10T14:21:00Z' formatted as ru-RU with year:'2-digit' → includes '26'
// Pattern: DD.MM.YY, HH:MM (ru-RU locale with 2-digit year)
const text = wrapper.text();
expect(text).toMatch(/\d{2}\.\d{2}\.\d{2}/);
});
});