Files
portal/app/tests/Frontend/admin-sms-view.spec.ts
T
Дмитрий df70a78e17 merge: ветка Билайна и Мотива сведена с главной — 347 коммитов, один затык
Затык был ровно один — routes/web.php: главная добавила ручку суточного итога
сторожей с машины робота, ветка добавила приёмник статусов Билайна. Ручки
независимые, оставлены обе.

Попутно поправлено примечание в config/services.php: оно говорило «заявка подана,
срок до десяти рабочих дней», а имя `Liderra.ru` было ОДОБРЕНО обеими сетями
10.08.2026 в тот же день — проверено глазами в кабинете. Примечание, которое
врёт, хуже отсутствующего: следующая смена прочла бы его и не стала поднимать
выключатель.

Замер на сведённом коде:
- модуль СМС 575/575, 7151 проверка (опора ветки была 548 — главная принесла 27);
- фронтенд 2205 из 2210. Пять падений — ЧУЖИЕ и приехали из главной:
  KoshelyokZakladkiPoKanalam.spec.ts, экран AdWalletMoneyColumn.vue спотыкается
  об отсутствующее поле frozen_breakdown. Доказано отпечатками: и тест, и экран
  байт в байт совпадают с gitea/main, моя правка их не касалась;
- Pint и PHPStan — чисто.
2026-08-10 21:39:33 +03:00

614 lines
27 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(),
// Этап 5 Task 8: сверка расчётов с оператором (строка листа 5.7).
fetchSmsReconcile: 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,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 60,
max_upload_phones: 50000,
allowed_operators: null,
},
...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,
// Оба поля задаём явно: без них Partial<> подмешивал сюда undefined,
// и проверка типов считала образец неполным именем отправителя.
doc_original_name: null,
consent_doc_original_name: 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,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 60,
max_upload_phones: 50000,
allowed_operators: null,
});
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="sms-settings-save"]').trigger('click');
await flushPromises();
expect(adminApi.updateSmsSettings).toHaveBeenCalledWith({
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 60,
max_upload_phones: 50000,
// 10.08.2026 сетей стало ПЯТЬ: добавился Мотив. Это умолчание экрана,
// когда сервер списка не прислал, — оно обязано совпадать с
// AllowedSmsOperators::DEFAULT, иначе первое же сохранение настроек
// молча ВЫЧЕРКНУЛО бы Мотива обратно.
allowed_operators: ['mts', 'beeline', 'megafon', 'tele2', 'motiv'],
});
});
it('показывает границы окна отправки и сохраняет изменённые (строка листа 3.7)', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(
makeTariffs({
settings: {
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 11,
quiet_to_hour: 19,
stuck_after_minutes: 60,
max_upload_phones: 50000,
allowed_operators: null,
},
}),
);
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValue([]);
vi.mocked(adminApi.updateSmsSettings).mockResolvedValueOnce({
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 12,
quiet_to_hour: 18,
stuck_after_minutes: 60,
max_upload_phones: 50000,
allowed_operators: null,
});
const wrapper = mountView();
await flushPromises();
// Пришедшие с сервера границы видны человеку в полях.
const from = wrapper.find('[data-testid="sms-settings-quiet-from"] input')
.element as HTMLInputElement;
const to = wrapper.find('[data-testid="sms-settings-quiet-to"] input').element as HTMLInputElement;
expect(from.value).toBe('11');
expect(to.value).toBe('19');
// Владелец правит окно и сохраняет — уходит именно то, что он ввёл.
await wrapper.find('[data-testid="sms-settings-quiet-from"] input').setValue('12');
await wrapper.find('[data-testid="sms-settings-quiet-to"] input').setValue('18');
await wrapper.find('[data-testid="sms-settings-save"]').trigger('click');
await flushPromises();
expect(adminApi.updateSmsSettings).toHaveBeenCalledWith({
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 12,
quiet_to_hour: 18,
stuck_after_minutes: 60,
max_upload_phones: 50000,
allowed_operators: ['mts', 'beeline', 'megafon', 'tele2', 'motiv'],
});
});
it('4.3 — показывает срок «рассылка зависла» и сохраняет изменённый', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(
makeTariffs({
settings: {
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 120,
max_upload_phones: 50000,
allowed_operators: null,
},
}),
);
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValue([]);
vi.mocked(adminApi.updateSmsSettings).mockResolvedValueOnce({
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 180,
max_upload_phones: 50000,
allowed_operators: null,
});
const wrapper = mountView();
await flushPromises();
// Пришедший с сервера срок виден человеку в поле — экран его не выдумывает.
const field = wrapper.find('[data-testid="sms-settings-stuck-minutes"] input')
.element as HTMLInputElement;
expect(field.value).toBe('120');
// Владелец правит срок и сохраняет — уходит именно то, что он ввёл.
await wrapper.find('[data-testid="sms-settings-stuck-minutes"] input').setValue('180');
await wrapper.find('[data-testid="sms-settings-save"]').trigger('click');
await flushPromises();
expect(adminApi.updateSmsSettings).toHaveBeenCalledWith({
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 180,
max_upload_phones: 50000,
allowed_operators: ['mts', 'beeline', 'megafon', 'tele2', 'motiv'],
});
});
it('4.6 — показывает потолок загрузки базы и сохраняет изменённый', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(
makeTariffs({
settings: {
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 60,
max_upload_phones: 20000,
allowed_operators: null,
},
}),
);
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValue([]);
vi.mocked(adminApi.updateSmsSettings).mockResolvedValueOnce({
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 60,
max_upload_phones: 30000,
allowed_operators: null,
});
const wrapper = mountView();
await flushPromises();
// Пришедший с сервера потолок виден человеку в поле — экран его не выдумывает.
const field = wrapper.find('[data-testid="sms-settings-max-upload"] input')
.element as HTMLInputElement;
expect(field.value).toBe('20000');
// Владелец правит потолок и сохраняет — уходит именно то, что он ввёл.
await wrapper.find('[data-testid="sms-settings-max-upload"] input').setValue('30000');
await wrapper.find('[data-testid="sms-settings-save"]').trigger('click');
await flushPromises();
expect(adminApi.updateSmsSettings).toHaveBeenCalledWith({
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 60,
max_upload_phones: 30000,
allowed_operators: ['mts', 'beeline', 'megafon', 'tele2', 'motiv'],
});
});
it('4.14 — показывает список «кому шлём» с сервера и сохраняет изменённый', async () => {
// Строка листа 4.14: список — настройка, а не код. Владелец снимает
// галочку, и отбор получателей меняется без выката.
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(
makeTariffs({
settings: {
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 60,
max_upload_phones: 50000,
allowed_operators: ['mts', 'megafon'],
},
}),
);
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValue([]);
vi.mocked(adminApi.updateSmsSettings).mockResolvedValueOnce({
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 60,
max_upload_phones: 50000,
allowed_operators: ['mts'],
});
const wrapper = mountView();
await flushPromises();
// Пришедший с сервера список виден в галочках — экран его не выдумывает.
const megafon = wrapper.find('[data-testid="sms-settings-operator-megafon"] input')
.element as HTMLInputElement;
const beeline = wrapper.find('[data-testid="sms-settings-operator-beeline"] input')
.element as HTMLInputElement;
expect(megafon.checked).toBe(true);
expect(beeline.checked).toBe(false);
// Владелец снимает Мегафон — уходит ровно то, что осталось отмеченным.
await wrapper.find('[data-testid="sms-settings-operator-megafon"] input').setValue(false);
await wrapper.find('[data-testid="sms-settings-save"]').trigger('click');
await flushPromises();
expect(adminApi.updateSmsSettings).toHaveBeenCalledWith({
name_fee_rub_per_operator: '500.00',
name_debt_grace_days: 5,
quiet_from_hour: 10,
quiet_to_hour: 20,
stuck_after_minutes: 60,
max_upload_phones: 50000,
allowed_operators: ['mts'],
});
});
it('4.3 — человеку объясняют, что будет с зависшей рассылкой и с деньгами', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(makeTariffs());
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValue([]);
const wrapper = mountView();
await flushPromises();
const text = wrapper.text();
expect(text).toContain('сначала попробует её дожать');
expect(text).toContain('вернёт клиенту замороженные деньги');
// И что честно ждущую утра рассылку это не трогает (строка 4.2).
expect(text).toContain('честно ждёт утра');
});
it('человеку объясняют, что окно общее и в местном времени получателя', async () => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValue(makeTariffs());
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValue([]);
const wrapper = mountView();
await flushPromises();
expect(wrapper.text()).toContain('местному времени');
expect(wrapper.text()).toContain('Клиент это время не настраивает');
});
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);
});
});
/**
* Строка листа 5.7 — сверка расчётов, экран владельца.
*
* 🔴 Главное, что здесь охраняется: экран НИКОГДА не выдаёт отсутствие числа
* за ноль. Живая проба 31.07 (В-235) шесть раз получила от МТС цену 0, потому
* что он ничего не отправил, а наша цена канала на бою вообще не задана
* (В-234) — нарисуй мы «0 ₽ против 0 ₽», владелец увидел бы идеальную
* сходимость там, где сверять нечем.
*
* 🪤 Целимся в КЛЕТКУ строки, а не в текст всей страницы: «не сообщил»
* содержится и в других подписях, и проверка по всей странице зеленела бы зря.
*/
describe('AdminSmsView — сверка расчётов с оператором', () => {
function makeReconcileRow(
overrides: Partial<import('../../resources/js/api/admin').AdminSmsReconcileRow> = {},
): import('../../resources/js/api/admin').AdminSmsReconcileRow {
return {
campaign_id: 7,
tenant_id: 3,
title: 'Летняя рассылка',
created_at: '2026-07-31T09:00:00Z',
messages: 2,
messages_reported: 2,
our_parts: 3,
provider_parts: 3,
parts_mismatch: 0,
our_cost: '9.0000',
our_cost_reported: '9.0000',
our_price_known: true,
provider_cost: '10.0000',
provider_cost_known: true,
difference: '1.0000',
...overrides,
};
}
const mountWithReconcile = async (
rows: import('../../resources/js/api/admin').AdminSmsReconcileRow[],
) => {
vi.mocked(adminApi.fetchSmsTariffs).mockResolvedValueOnce(makeTariffs());
vi.mocked(adminApi.fetchSmsSenders).mockResolvedValueOnce([]);
vi.mocked(adminApi.fetchSmsGlobalOptouts).mockResolvedValueOnce([]);
vi.mocked(adminApi.fetchSmsReconcile).mockResolvedValueOnce(rows);
const wrapper = mountView();
await flushPromises();
return wrapper;
};
it('на mount загружает сверку и показывает оба счёта частей и разницу', async () => {
const wrapper = await mountWithReconcile([makeReconcileRow()]);
expect(adminApi.fetchSmsReconcile).toHaveBeenCalled();
const row = wrapper.find('[data-testid="sms-reconcile-row-7"]');
expect(row.exists()).toBe(true);
expect(row.text()).toContain('Летняя рассылка');
expect(row.text()).toContain('9.00 ₽');
expect(row.text()).toContain('10.00 ₽');
expect(wrapper.find('[data-testid="sms-reconcile-diff-7"]').text()).toBe('1.00 ₽');
});
it('расхождение по частям выделено отдельно, а совпадение — нет', async () => {
const wrapper = await mountWithReconcile([
makeReconcileRow({ campaign_id: 7, parts_mismatch: 2 }),
makeReconcileRow({ campaign_id: 8, parts_mismatch: 0 }),
]);
expect(wrapper.find('[data-testid="sms-reconcile-mismatch-7"]').text()).toBe('2');
expect(wrapper.find('[data-testid="sms-reconcile-mismatch-8"]').exists()).toBe(false);
});
it('оператор цену не сообщил — экран пишет причину, а не 0 ₽', async () => {
const wrapper = await mountWithReconcile([
makeReconcileRow({
provider_cost: null,
provider_cost_known: false,
difference: null,
}),
]);
const row = wrapper.find('[data-testid="sms-reconcile-row-7"]');
expect(row.text()).toContain('оператор цену не сообщил');
expect(row.text()).not.toContain('0.00 ₽');
expect(wrapper.find('[data-testid="sms-reconcile-diff-7"]').text()).toBe('сверять нечем');
});
it('наша цена канала не задана — экран пишет причину, а не 0 ₽', async () => {
const wrapper = await mountWithReconcile([
makeReconcileRow({
our_cost: null,
our_price_known: false,
difference: null,
}),
]);
const row = wrapper.find('[data-testid="sms-reconcile-row-7"]');
expect(row.text()).toContain('цена канала не задана');
expect(wrapper.find('[data-testid="sms-reconcile-diff-7"]').text()).toBe('сверять нечем');
});
// Находка приёмки Этапа 5: без охвата число «разница» непонятно — человек не
// видит, посчитана она по всей рассылке или по десятой её части.
it('оператор отчитался не по всем — рядом с разницей видно, по скольким', async () => {
const wrapper = await mountWithReconcile([
makeReconcileRow({
messages: 120,
messages_reported: 10,
our_cost: '360.0000',
our_cost_reported: '30.0000',
provider_cost: '30.0000',
difference: '0.0000',
}),
]);
expect(wrapper.find('[data-testid="sms-reconcile-coverage-7"]').text()).toContain(
'оператор отчитался по 10 из 120',
);
});
it('оператор отчитался по всем — про охват не пишем, чтобы не шуметь', async () => {
const wrapper = await mountWithReconcile([
makeReconcileRow({
messages: 120,
messages_reported: 120,
our_cost: '360.0000',
our_cost_reported: '360.0000',
provider_cost: '360.0000',
difference: '0.0000',
}),
]);
expect(wrapper.find('[data-testid="sms-reconcile-coverage-7"]').exists()).toBe(false);
});
it('оператора ещё не спрашивали — вместо нуля частей так и написано', async () => {
const wrapper = await mountWithReconcile([
makeReconcileRow({ provider_parts: null, parts_mismatch: 0 }),
]);
expect(wrapper.find('[data-testid="sms-reconcile-row-7"]').text()).toContain('не спрашивали');
});
it('отправленных рассылок нет — экран говорит об этом словами', async () => {
const wrapper = await mountWithReconcile([]);
expect(wrapper.find('[data-testid="sms-reconcile-empty"]').exists()).toBe(true);
});
});