import { describe, it, expect, vi, beforeEach } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { createPinia } from 'pinia'; import { createRouter, createMemoryHistory } from 'vue-router'; import SalesTariffsView from '../../resources/js/views/sales/SalesTariffsView.vue'; // ─── mock api/sales ─────────────────────────────────────────────────────────── // vi.mock hoisted к верху файла → мок-данные объявляем ВНУТРИ фабрики. // Модель 03.07.2026: ДВА вида — topup_step (порог+бонус+ступени) и daily_salary (суточный оклад). vi.mock('../../resources/js/api/sales', () => { const tariffs = [ { id: 1, name: 'Стандарт-пополнения', kind: 'topup_step', params: { threshold: 30000, reward: 10000, periods: [ { from: 1, to: 3, rate: 10 }, { from: 4, to: 6, rate: 7 }, ], }, }, { id: 2, name: 'Суточный оклад', kind: 'daily_salary', params: {}, }, ]; const managers = [ { id: 11, name: 'Алексей Смирнов', current_tariff_id: 2, // на суточном окладе current_tariff_name: 'Суточный оклад', current_tariff_kind: 'daily_salary', base_salary_rub: 2000, clients_count: 7, topups_rub: 240000, oborot_rub: 235050, estimated_earned_rub: 44000, }, { id: 22, name: 'Мария Котова', current_tariff_id: 1, // на проценте от пополнений current_tariff_name: 'Стандарт-пополнения', current_tariff_kind: 'topup_step', base_salary_rub: 0, clients_count: 5, topups_rub: 168000, oborot_rub: 165300, estimated_earned_rub: 54795, }, ]; return { listSalesTariffs: vi.fn().mockResolvedValue({ tariffs, managers }), createSalesTariff: vi .fn() .mockResolvedValue({ id: 99, name: 'Новый', kind: 'topup_step', params: { threshold: 0, reward: 0, periods: [] }, }), updateSalesTariff: vi .fn() .mockResolvedValue({ id: 1, name: 'Стандарт-пополнения', kind: 'topup_step', params: {} }), assignSalesTariff: vi .fn() .mockResolvedValue({ id: 11, name: 'Алексей Смирнов', current_tariff_id: 1, base_salary_rub: 0 }), extractSalesErrorMessage: vi.fn().mockReturnValue('Произошла ошибка. Попробуйте позже.'), }; }); beforeEach(() => vi.clearAllMocks()); // ─── helpers ────────────────────────────────────────────────────────────────── function makeRouter() { const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/sales/tariffs', name: 'sales-tariffs', component: SalesTariffsView }, { path: '/sales', name: 'sales-overview', component: { template: '
' } }, ], }); return router.push('/sales/tariffs').then(() => router); } async function mountView() { const router = await makeRouter(); const wrapper = mount(SalesTariffsView, { global: { plugins: [createVuetify(), createPinia(), router] }, }); await flushPromises(); return wrapper; } describe('SalesTariffsView', () => { it('вызывает listSalesTariffs при монтировании', async () => { await mountView(); const api = await import('../../resources/js/api/sales'); expect(api.listSalesTariffs).toHaveBeenCalled(); }); it('рендерит два вида тарифов', async () => { const wrapper = await mountView(); const text = wrapper.text(); // Заголовки двух секций expect(text).toContain('Процент от пополнений'); expect(text).toContain('Суточный оклад'); // По карточке/строке на каждый вид expect(wrapper.find('[data-testid="tariff-card-1"]').exists()).toBe(true); // topup_step expect(wrapper.find('[data-testid="daily-row-2"]').exists()).toBe(true); // daily_salary // Названия тарифов лежат в input value (v-text-field) const nameInput = wrapper.find('[data-testid="tariff-name-1"] input').element as HTMLInputElement; expect(nameInput.value).toBe('Стандарт-пополнения'); // Порог и бонус — поля topup_step expect(wrapper.find('[data-testid="tariff-threshold-1"]').exists()).toBe(true); expect(wrapper.find('[data-testid="tariff-reward-1"]').exists()).toBe(true); }); it('рендерит таблицу менеджеров с оценкой начисления', async () => { const wrapper = await mountView(); const text = wrapper.text(); expect(text).toContain('Алексей Смирнов'); expect(text).toContain('Мария Котова'); // estimated_earned_rub форматируется ru-RU expect(text).toContain('44'); expect(text).toContain('54'); }); it('«добавить период» добавляет строку в topup_step-тариф', async () => { const wrapper = await mountView(); const before = wrapper.findAll('[data-testid="period-row"]').length; const addBtn = wrapper.find('[data-testid="add-period-1"]'); expect(addBtn.exists()).toBe(true); await addBtn.trigger('click'); await flushPromises(); const after = wrapper.findAll('[data-testid="period-row"]').length; expect(after).toBe(before + 1); }); it('сохранение topup_step-тарифа кладёт threshold, reward и periods в params', async () => { const wrapper = await mountView(); const api = await import('../../resources/js/api/sales'); const saveBtn = wrapper.find('[data-testid="save-tariff-1"]'); expect(saveBtn.exists()).toBe(true); await saveBtn.trigger('click'); await flushPromises(); expect(api.updateSalesTariff).toHaveBeenCalledWith( 1, expect.objectContaining({ name: 'Стандарт-пополнения', params: expect.objectContaining({ threshold: 30000, reward: 10000, periods: expect.arrayContaining([expect.objectContaining({ from: 1, to: 3, rate: 10 })]), }), }), ); }); it('создание нового topup_step-тарифа вызывает createSalesTariff с kind=topup_step', async () => { const wrapper = await mountView(); const api = await import('../../resources/js/api/sales'); const nameInput = wrapper.find('[data-testid="new-topup-name"] input'); expect(nameInput.exists()).toBe(true); await nameInput.setValue('Мой новый тариф'); const createBtn = wrapper.find('[data-testid="create-topup"]'); await createBtn.trigger('click'); await flushPromises(); expect(api.createSalesTariff).toHaveBeenCalledWith( expect.objectContaining({ name: 'Мой новый тариф', kind: 'topup_step', params: expect.objectContaining({ threshold: 0, reward: 0, periods: expect.any(Array) }), }), ); }); it('сохранение daily_salary-тарифа вызывает updateSalesTariff с пустыми params', async () => { const wrapper = await mountView(); const api = await import('../../resources/js/api/sales'); const saveBtn = wrapper.find('[data-testid="save-tariff-2"]'); expect(saveBtn.exists()).toBe(true); await saveBtn.trigger('click'); await flushPromises(); expect(api.updateSalesTariff).toHaveBeenCalledWith( 2, expect.objectContaining({ name: 'Суточный оклад', params: {} }), ); }); it('создание нового daily_salary-тарифа вызывает createSalesTariff с kind=daily_salary', async () => { const wrapper = await mountView(); const api = await import('../../resources/js/api/sales'); const nameInput = wrapper.find('[data-testid="new-daily-name"] input'); expect(nameInput.exists()).toBe(true); await nameInput.setValue('Оклад стажёра'); const createBtn = wrapper.find('[data-testid="create-daily"]'); await createBtn.trigger('click'); await flushPromises(); expect(api.createSalesTariff).toHaveBeenCalledWith( expect.objectContaining({ name: 'Оклад стажёра', kind: 'daily_salary', params: {} }), ); }); it('менеджер на суточном окладе → поле «Ставка, ₽/день» + assign со ставкой', async () => { const wrapper = await mountView(); const api = await import('../../resources/js/api/sales'); // Смирнов (id 11) на daily_salary (tariff 2) → поле ставки видно. expect(wrapper.find('[data-testid="assign-salary-11"]').exists()).toBe(true); const saveBtn = wrapper.find('[data-testid="save-assign-11"]'); expect(saveBtn.exists()).toBe(true); await saveBtn.trigger('click'); await flushPromises(); expect(api.assignSalesTariff).toHaveBeenCalledWith( expect.objectContaining({ manager_id: 11, tariff_id: 2, base_salary_rub: 2000 }), ); }); it('смена тарифа менеджера на процент + сохранение вызывает assignSalesTariff без ставки', async () => { const wrapper = await mountView(); const api = await import('../../resources/js/api/sales'); const vm = wrapper.vm as unknown as { managers: Array<{ id: number; current_tariff_id: number | null }>; }; // Котова (index 1) уже на topup_step (1); переключим Смирнова (index 0) на topup_step. vm.managers[0].current_tariff_id = 1; await flushPromises(); const saveBtn = wrapper.find('[data-testid="save-assign-11"]'); await saveBtn.trigger('click'); await flushPromises(); expect(api.assignSalesTariff).toHaveBeenCalledWith(expect.objectContaining({ manager_id: 11, tariff_id: 1 })); }); });