Files
portal/app/tests/Frontend/admin-sms-view.spec.ts
T
Дмитрий 37493b7d02 feat(смс-клиент): экраны Этапа 1 — «Не писать этим», «Остановить», ключ заказа, общий стоп-лист в админке
Вкладка «Не писать этим» отдельной панелью SmsOptoutsPanel.vue: номер руками,
пачкой из файла, удаление; непонятые строки показаны образцами, а не молча.

Кнопка «Остановить» видна, пока рассылка в очереди, идёт или ждёт утра; после
остановки строка показывает честный итог «Ушло N из M, списано X ₽» — деньги
фактические, а не смета.

Ключ заказа crypto.randomUUID() уходит с каждой отправкой и меняется после
успеха. На ответ сервера «похоже, это повтор» экран задаёт вопрос словами
сервера и повторяет только по согласию человека.

В админке раздел «Общий стоп-лист»: список, внесение с причиной, удаление.

Отказ получателя (страница по ссылке и приписка в тексте) в экранах
отсутствует — отменён владельцем, см. В-30 приёмочного листа.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 19:45:47 +03:00

203 lines
8.5 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 AdminSmsView from '../../resources/js/views/admin/AdminSmsView.vue';
import type { AdminSmsSender, AdminSmsTariffsResponse } from '../../resources/js/api/admin';
vi.mock('../../resources/js/api/admin', async (importOriginal) => {
const orig = await importOriginal<typeof import('../../resources/js/api/admin')>();
return {
...orig,
fetchSmsTariffs: vi.fn(),
updateSmsTariffs: vi.fn(),
updateSmsSettings: vi.fn(),
fetchSmsSenders: vi.fn(),
approveSmsSender: vi.fn(),
rejectSmsSender: vi.fn(),
disableSmsSender: vi.fn(),
// Этап 1: общий стоп-лист портала (строки листа 1.4, 1.5).
fetchSmsGlobalOptouts: vi.fn(),
addSmsGlobalOptout: vi.fn(),
deleteSmsGlobalOptout: vi.fn(),
};
});
const adminApi = await import('../../resources/js/api/admin');
beforeEach(() => {
vi.clearAllMocks();
});
function makeTariffs(overrides: Partial<AdminSmsTariffsResponse> = {}): AdminSmsTariffsResponse {
return {
tariffs: [
{ id: 1, min_qty: 1, price_rub: '3.50' },
{ id: 2, min_qty: 1000, price_rub: '3.00' },
],
settings: { name_fee_rub_per_operator: '500.00', name_debt_grace_days: 5 },
...overrides,
};
}
function makeSender(overrides: Partial<AdminSmsSender> = {}): AdminSmsSender {
return {
id: 1,
tenant_id: 5,
name: 'ОКНА МСК',
name_type: 'company',
status: 'pending',
monthly_fee_rub: '500.00',
note: null,
...overrides,
};
}
const mountView = () =>
mount(AdminSmsView, {
global: { plugins: [createVuetify()] },
});
describe('AdminSmsView', () => {
it('на mount загружает тарифы, настройки и заявки на согласовании', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValueOnce(makeTariffs());
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([makeSender()]);
const wrapper = mountView();
await flushPromises();
expect(adminApi.fetchSmsTariffs).toHaveBeenCalledTimes(1);
expect(adminApi.fetchSmsSenders).toHaveBeenCalledWith('pending');
expect(wrapper.text()).toContain('ОКНА МСК');
expect(wrapper.find('[data-testid="sms-tariffs-save"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="sms-settings-save"]').exists()).toBe(true);
});
it('«Сохранить тарифы» вызывает updateSmsTariffs с текущими строками', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(makeTariffs());
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValue([]);
vi.mocked(adminApi.updateSmsTariffs).mockResolvedValueOnce({ tariffs: makeTariffs().tariffs });
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="sms-tariffs-save"]').trigger('click');
await flushPromises();
expect(adminApi.updateSmsTariffs).toHaveBeenCalledTimes(1);
const rows = vi.mocked(adminApi.updateSmsTariffs).mock.calls[0][0];
expect(rows).toEqual([
{ min_qty: 1, price_rub: '3.50' },
{ min_qty: 1000, price_rub: '3.00' },
]);
});
it('«Сохранить настройки» вызывает updateSmsSettings с полями', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(makeTariffs());
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValue([]);
vi.mocked(adminApi.updateSmsSettings).mockResolvedValueOnce({
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
});
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="sms-settings-save"]').trigger('click');
await flushPromises();
expect(adminApi.updateSmsSettings).toHaveBeenCalledWith('500.00', 5);
});
it('«Подтвердить» вызывает approveSmsSender(id) и перезагружает список', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(makeTariffs());
vi.mocked(adminApi.fetchSmsSenders)
.mockResolvedValueOnce([makeSender({ id: 7 })])
.mockResolvedValueOnce([]);
vi.mocked(adminApi.approveSmsSender).mockResolvedValueOnce(makeSender({ id: 7, status: 'active' }));
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="sms-sender-approve-7"]').trigger('click');
await flushPromises();
expect(adminApi.approveSmsSender).toHaveBeenCalledWith(7);
expect(adminApi.fetchSmsSenders).toHaveBeenCalledTimes(2);
});
// ─── Общий стоп-лист портала (строки листа 1.4, 1.5) ────────────────────
it('1.5 — показывает внесённые номера с причиной', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValueOnce(makeTariffs());
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([]);
vi.mocked(adminApi.fetchSmsGlobalOptouts).mockResolvedValueOnce([
{ phone: '79991234567', reason: 'жалоба в МТС', created_at: '2026-07-27T10:00:00Z' },
]);
const wrapper = mountView();
await flushPromises();
expect(adminApi.fetchSmsGlobalOptouts).toHaveBeenCalledTimes(1);
expect(wrapper.text()).toContain('79991234567');
expect(wrapper.text()).toContain('жалоба в МТС');
});
it('1.4 — вносит номер с комментарием и перечитывает список', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValueOnce(makeTariffs());
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([]);
vi.mocked(adminApi.fetchSmsGlobalOptouts)
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{ phone: '79991234567', reason: 'жалоба в МТС', created_at: '2026-07-27T10:00:00Z' },
]);
vi.mocked(adminApi.addSmsGlobalOptout).mockResolvedValueOnce({
phone: '79991234567',
reason: 'жалоба в МТС',
created_at: '2026-07-27T10:00:00Z',
});
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="global-optout-phone"] input').setValue('79991234567');
await wrapper.find('[data-testid="global-optout-reason"] input').setValue('жалоба в МТС');
await wrapper.find('[data-testid="global-optout-add"]').trigger('click');
await flushPromises();
expect(adminApi.addSmsGlobalOptout).toHaveBeenCalledWith('79991234567', 'жалоба в МТС');
expect(adminApi.fetchSmsGlobalOptouts).toHaveBeenCalledTimes(2);
expect(wrapper.text()).toContain('79991234567');
});
it('1.5 — убирает номер из общего стоп-листа', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValueOnce(makeTariffs());
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([]);
vi.mocked(adminApi.fetchSmsGlobalOptouts)
.mockResolvedValueOnce([
{ phone: '79991234567', reason: null, created_at: '2026-07-27T10:00:00Z' },
])
.mockResolvedValueOnce([]);
vi.mocked(adminApi.deleteSmsGlobalOptout).mockResolvedValueOnce(undefined);
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="global-optout-delete-79991234567"]').trigger('click');
await flushPromises();
expect(adminApi.deleteSmsGlobalOptout).toHaveBeenCalledWith('79991234567');
expect(wrapper.text()).not.toContain('79991234567');
});
it('ошибка загрузки тарифов показывает человеческое сообщение', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockRejectedValueOnce(new Error('500'));
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([]);
const wrapper = mountView();
await flushPromises();
expect(wrapper.find('[data-testid="sms-tariffs-error"]').exists()).toBe(true);
});
});