Files
portal/app/tests/Frontend/BillingView.spec.ts
T
Дмитрий 8cc09d4509
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
fix(ui): биллинг — «Запас» по живому заказу, убрать дубль и строку-чтение
Поправки после первой версии (по замечаниям владельца):
- «Запас» считаем по текущему заказу проектов (requiredLeadsPerDay), так же как
  BalanceCapacityIndicator: 0/день → ∞ (баланс не расходуется), N/день → лиды/N.
  Раньше брался бэкендный runway_days (историческая скорость за 30 дней) —
  давал «2192 дн.» рядом с «при 0 лидов в день», выглядело фейком/противоречиво.
- Убрана строка-чтение под рядом (дубль).
- Убран BalanceCapacityIndicator из BillingView: дублировал ряд и писал
  «по тарифу», хотя тарифов нет.
- Рамки трёх карточек выровнены по высоте (единый border/radius + flex-stretch,
  убран конфликтный height:100%).

Тесты BalanceCard/BillingView обновлены. vitest 19/19, vue-tsc и build — зелёные.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:20:14 +03:00

130 lines
5.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createVuetify } from 'vuetify';
import BillingView from '../../resources/js/views/BillingView.vue';
import * as billingApi from '../../resources/js/api/billing';
import type { Wallet } from '../../resources/js/api/billing';
vi.mock('../../resources/js/api/billing');
const vuetify = createVuetify();
function makeWallet(): Wallet {
return {
balance_rub: '14250.00',
affordable_leads: 28,
current_tier: { no: 1, price_rub: '500.00', leads_left_in_tier: 100 },
next_tier: { no: 2, price_rub: '450.00', leads_in_tier: 200 },
delivered_in_month: 0,
runway_days: 142,
tiers_preview: [
{ tier_no: 1, price_rub: '500.00', leads_in_tier: 100 },
{ tier_no: 2, price_rub: '450.00', leads_in_tier: 200 },
],
tariff: {
code: 'pro',
name: 'Про',
features: ['webhook', 'kanban', 'api'],
},
};
}
describe('BillingView.vue', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.mocked(billingApi.getWallet).mockResolvedValue(makeWallet());
});
const factory = () =>
mount(BillingView, {
global: {
plugins: [vuetify, createPinia()],
stubs: { TransactionsTable: true, InvoicesTable: true, ChargesTab: true, TopupDialog: true },
},
});
it('монтируется с заголовком «Биллинг и тарифы»', async () => {
const wrapper = factory();
await flushPromises();
expect(wrapper.find('h1').text()).toBe('Биллинг и тарифы');
});
it('загружает кошелёк и показывает баланс в шапке', async () => {
const wrapper = factory();
await flushPromises();
expect(billingApi.getWallet).toHaveBeenCalled();
const text = wrapper.text();
expect(text).toMatch(/14\s+250\s*₽/);
});
it('не показывает чип «лидов запас» в шапке', async () => {
const wrapper = factory();
await flushPromises();
expect(wrapper.text()).not.toContain('лидов запас');
});
it('показывает «Запас» в BalanceCard вместо карточки «Тариф»', async () => {
// tenant-store в тесте не замокан → requiredLeadsPerDay = 0 → запас ∞.
const wrapper = factory();
await flushPromises();
const text = wrapper.text();
expect(text).toContain('Запас');
expect(text).toContain('∞');
expect(text).not.toContain('Тариф не выбран');
expect(text).not.toContain('по тарифу');
});
it('показывает error-alert при сбое загрузки кошелька', async () => {
vi.mocked(billingApi.getWallet).mockRejectedValue(new Error('network'));
const wrapper = factory();
await flushPromises();
expect(wrapper.text()).toContain('Не удалось загрузить');
});
it('кнопка «Повторить» перезагружает кошелёк после ошибки', async () => {
vi.mocked(billingApi.getWallet).mockReset();
vi.mocked(billingApi.getWallet).mockRejectedValueOnce(new Error('network')).mockResolvedValueOnce(makeWallet());
const wrapper = factory();
await flushPromises();
expect(wrapper.text()).toContain('Не удалось загрузить');
const retry = wrapper.findAll('button').find((b) => b.text().includes('Повторить'));
expect(retry).toBeDefined();
await retry!.trigger('click');
await flushPromises();
expect(billingApi.getWallet).toHaveBeenCalledTimes(2);
expect(wrapper.text()).toMatch(/14\s+250\s*₽/);
});
it('содержит табы Обзор / Списания', async () => {
const wrapper = factory();
await flushPromises();
const text = wrapper.text();
expect(text).toContain('Обзор');
expect(text).toContain('Списания');
});
it('кнопка «Пополнить баланс» открывает TopupDialog', async () => {
const wrapper = factory();
await flushPromises();
const btn = wrapper.findAll('button').find((b) => b.text().includes('Пополнить баланс'));
expect(btn).toBeDefined();
await btn!.trigger('click');
expect((wrapper.vm as unknown as { topupOpen: boolean }).topupOpen).toBe(true);
});
it('не показывает pending-баннер (E4 — mock убран)', async () => {
const wrapper = factory();
await flushPromises();
expect(wrapper.text()).not.toContain('в обработке');
});
it('рендерит TierPricesPanel между BalanceCard и TransactionsTable', async () => {
const wrapper = factory();
await flushPromises();
expect(wrapper.text()).toContain('Цены за лид');
});
});