Files
portal/app/tests/Frontend/TransactionsTable.spec.ts
T
Дмитрий 995aae4571 fix(portal): карточки вместо таблицы на планшете-портрете (Сделки/Списания)
Широкая таблица (7 колонок) не влезала в узкую колонку планшета рядом с
rail-меню — правые колонки «Комментарий»/«Поставлен» обрезались на 600–1279px.
ResponsiveTable теперь показывает карточки на телефоне И планшете, таблицу —
только на десктопе (>=1280). Тесты табличного слоя зафиксированы на десктоп-ширину.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:57:07 +03:00

128 lines
6.0 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');
// ResponsiveTable показывает таблицу только на десктопе (>=1280); эти тесты
// проверяют табличный слой — фиксируем десктоп-ширину.
Object.defineProperty(window, 'innerWidth', { configurable: true, value: 1440 });
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}/);
});
});