2026-05-16 07:56:22 +03:00
|
|
|
|
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',
|
2026-05-23 14:35:42 +03:00
|
|
|
|
display_amount_rub: '5000.00',
|
2026-05-16 07:56:22 +03: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');
|
2026-06-17 05:17:12 +03:00
|
|
|
|
expect(billingApi.getTransactions).toHaveBeenLastCalledWith(expect.objectContaining({ type: 'topup' }));
|
2026-05-16 07:56:22 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
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('Не удалось загрузить транзакции');
|
|
|
|
|
|
});
|
2026-05-23 14:35:42 +03:00
|
|
|
|
|
|
|
|
|
|
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('лид.');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-17 15:07:26 +03:00
|
|
|
|
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('Пополнение баланса');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-23 14:35:42 +03:00
|
|
|
|
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}/);
|
|
|
|
|
|
});
|
2026-05-16 07:56:22 +03:00
|
|
|
|
});
|