import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { mount, flushPromises } from '@vue/test-utils'; import { createVuetify } from 'vuetify'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import SalesSmsView from '../../resources/js/views/sales/SalesSmsView.vue'; import * as api from '../../resources/js/api/sales'; import type { SalesAdAudienceFirmRow } from '../../resources/js/api/sales'; const view = readFileSync( resolve(__dirname, '../../resources/js/views/sales/SalesSmsView.vue'), 'utf-8', ); describe('Экран «Прогрев СМС»', () => { it('показывает цену до отправки', () => { expect(view).toContain('estimated_cost_kopecks'); }); it('помечает режим песочницы явно — «отправлено» нельзя спутать с настоящей отправкой', () => { expect(view).toMatch(/песочниц/i); }); it('блокирует кнопку без согласованного имени отправителя', () => { expect(view).toMatch(/:disabled/); expect(view).toMatch(/имя отправителя/i); }); it('показывает счётчик сегментов', () => { expect(view).toContain('segments'); }); it('показывает причины пропуска номеров', () => { expect(view).toContain('skipped'); }); // Task 10.5: у начальника 99 фирм и 177 номеров — руками их не переписывают. it('получателей отмечают галочками, а не вводят руками', () => { expect(view).toContain('v-checkbox'); expect(view).toContain('fetchSmsRecipients'); }); it('ходит в сервер через общую прослойку, а не напрямую', () => { expect(view).not.toContain('axios.post'); expect(view).not.toContain('axios.get'); }); }); // ─── Task 8: пагинация, Ниша/Дата запуска, назначение менеджера, фильтры ────── // // Мокаем api/sales.ts, как и SalesWarmingView.spec.ts — экран ходит в сервер // только через эту обёртку. vi.mock('../../resources/js/api/sales', async () => { const actual = await vi.importActual('../../resources/js/api/sales'); return { ...actual, fetchSmsCampaigns: vi.fn(), fetchSmsCampaign: vi.fn(), fetchSmsRecipients: vi.fn(), previewSms: vi.fn(), createSmsCampaign: vi.fn(), listSmsFirms: vi.fn(), assignAdAudienceFirm: vi.fn(), listSalesManagers: vi.fn(), }; }); /** Task 8: строка фирмы несёт нишу/дату запуска/менеджера — минимальный валидный мок. */ function firm(over: Partial): SalesAdAudienceFirmRow { return { id: 1, firm_name: 'Тестовая фирма', city: 'Москва', phones_count: 1, state: 'active', stop_reason: null, resume_at: null, days_in_ads: 1, prospect_id: null, stage: null, assigned: false, ch_yandex: false, ch_vk: false, ch_mts: false, rubric: null, warmup_started_at: null, manager_id: null, manager_name: null, warming_active: true, warming_channels: [], ...over, }; } const vuetify = createVuetify(); // VDialog в JSDOM не рендерит в teleport-цели; стаб делает доступным // внутри корня для wrapper.find() (тот же приём, что в NewProjectDialog.spec.ts). const factory = () => mount(SalesSmsView, { global: { plugins: [vuetify], stubs: { VDialog: { template: '
', props: ['modelValue'], }, }, }, }); beforeEach(() => { vi.clearAllMocks(); vi.mocked(api.fetchSmsCampaigns).mockResolvedValue({ campaigns: [], sandbox: false, senders: [] }); vi.mocked(api.listSmsFirms).mockResolvedValue([]); vi.mocked(api.fetchSmsRecipients).mockResolvedValue([]); }); describe('SalesSmsView — пагинация, ниша/дата, менеджер, фильтры (Task 8)', () => { it('таблица получателей показывает колонки «Ниша» и «Дата запуска»', async () => { vi.mocked(api.listSmsFirms).mockResolvedValue([firm({ id: 1, rubric: 'Стоматологии' })]); const w = factory(); await flushPromises(); const tableText = w.find('[data-testid="sms-firms-table"]').text(); expect(tableText).toContain('Ниша'); expect(tableText).toContain('Дата запуска'); }); // Task D2: список СМС-фирм идёт из своего источника (только firm_channels // channel='sms') и показывает, сколько боевых СМС уже слали — чтобы не // долбить человека повторно. it('таблица получателей показывает колонку «Отправлено» со значением sms_sent_count', async () => { vi.mocked(api.listSmsFirms).mockResolvedValue([ firm({ id: 1, firm_name: 'Стоматология Улыбка', sms_sent_count: 3 }), ]); const w = factory(); await flushPromises(); expect(api.listSmsFirms).toHaveBeenCalled(); const tableText = w.find('[data-testid="sms-firms-table"]').text(); expect(tableText).toContain('Отправлено'); expect(tableText).toContain('3'); }); it('пагинация таблицы предлагает вариант 100 строк на странице', async () => { vi.mocked(api.listSmsFirms).mockResolvedValue([firm({ id: 1 })]); const w = factory(); await flushPromises(); const table = w.findComponent({ name: 'VDataTable' }); expect(table.exists()).toBe(true); expect(table.props('itemsPerPageOptions')).toContain(100); }); it('«Назначить менеджера» открывает диалог, подтверждение зовёт assignAdAudienceFirm', async () => { vi.mocked(api.listSmsFirms).mockResolvedValue([ firm({ id: 3, firm_name: 'Клиника Феникс', assigned: false }), ]); vi.mocked(api.listSalesManagers).mockResolvedValue([ { id: 9, name: 'Иван Иванов', email: 'i.ivanov@liderra.ru', role: 'manager', is_active: true, clients_count: 1, paid_all_time_rub: 0, }, ]); vi.mocked(api.assignAdAudienceFirm).mockResolvedValue({ prospect_id: 77 }); const w = factory(); await flushPromises(); const btn = w.find('[data-testid="warming-assign-btn"]'); expect(btn.exists()).toBe(true); await btn.trigger('click'); await flushPromises(); expect(w.find('[data-testid="sms-assign-dialog"]').exists()).toBe(true); type Exposed = { assignManagerId: number | null; doAssign: () => Promise }; const vm = w.vm as unknown as Exposed; vm.assignManagerId = 9; await vm.doAssign(); await flushPromises(); expect(api.assignAdAudienceFirm).toHaveBeenCalledWith(3, 9); }); it('фильтр по нише сужает filtered до совпадающих фирм', async () => { vi.mocked(api.listSmsFirms).mockResolvedValue([ firm({ id: 1, rubric: 'Стоматологии' }), firm({ id: 2, rubric: 'Медицина' }), ]); const w = factory(); await flushPromises(); type Exposed = { filters: { niche: string | null }; filtered: Array<{ id: number }> }; const vm = w.vm as unknown as Exposed; vm.filters.niche = 'Медицина'; await flushPromises(); expect(vm.filtered.map((f) => f.id)).toEqual([2]); }); // Деньги: фильтр СПРЯТАЛ отмеченную фирму — её номера не должны попасть в // получателей платной рассылки. Галочка при этом не пропадает — сняли // фильтр, выбор вернулся. it('фильтр ниши прячет отмеченную фирму — её номера уходят из получателей, галочка сохраняется', async () => { vi.mocked(api.listSmsFirms).mockResolvedValue([ firm({ id: 1, firm_name: 'Стоматология Улыбка', rubric: 'Стоматологии', phones_count: 2 }), firm({ id: 2, firm_name: 'Клиника Медицина+', rubric: 'Медицина', phones_count: 3 }), ]); vi.mocked(api.fetchSmsRecipients).mockImplementation(async (ids: number[]) => { const map: Record = { 1: [ { phone: '79990000001', firm_name: 'Стоматология Улыбка', city: 'Москва', operator: null, phone_type: null, }, ], 2: [ { phone: '79990000002', firm_name: 'Клиника Медицина+', city: 'Москва', operator: null, phone_type: null, }, ], }; return ids.flatMap((id) => map[id] ?? []); }); const w = factory(); await flushPromises(); type Exposed = { selectedFirmIds: number[]; effectiveSelectedIds: number[]; filters: { niche: string | null }; recipients: Array<{ phone: string }>; }; const vm = w.vm as unknown as Exposed; // Отмечаем обе фирмы галочками. vm.selectedFirmIds = [1, 2]; await flushPromises(); expect(vm.effectiveSelectedIds.sort()).toEqual([1, 2]); expect(api.fetchSmsRecipients).toHaveBeenLastCalledWith([1, 2]); expect(vm.recipients.map((r) => r.phone).sort()).toEqual(['79990000001', '79990000002']); // Фильтр ниши прячет фирму id=1 (Стоматологии). vm.filters.niche = 'Медицина'; await flushPromises(); expect(vm.effectiveSelectedIds).toEqual([2]); // Галочка НЕ снята — только скрытые из выдачи id не считаются получателями. expect(vm.selectedFirmIds.sort()).toEqual([1, 2]); expect(api.fetchSmsRecipients).toHaveBeenLastCalledWith([2]); expect(vm.recipients.map((r) => r.phone)).toEqual(['79990000002']); const counterText = w.find('[data-testid="sms-recipients-count"]').text(); expect(counterText).toContain('1'); expect(counterText).not.toMatch(/Отмечено фирм:\s*2/); // Сняли фильтр — выбор восстанавливается целиком. vm.filters.niche = null; await flushPromises(); expect(vm.effectiveSelectedIds.sort()).toEqual([1, 2]); }); });