Files
portal/app/tests/Frontend/telegram-api.spec.ts
T
Дмитрий 9b9055753b feat(телеграм-реклама): Этап 4 — клиентский опыт + причёсывание экрана под домашний стиль
Закрывает фронтенд Этапа 4 плана docs/superpowers/plans/2026-07-27-telegram-module-hardening.md
(бэкенд 4.3 — уведомление об одобрении — сделан в Этапе 3). Чистый Vue+Vitest, TDD.

## 4.1 — смета и охват ДО запуска
Разделены create/launch в UI: кнопка «Рассчитать» создаёт черновик (createTelegram) и
показывает прогноз стоимости + охват; «Запустить» доступна только после расчёта. Любая
правка формы сбрасывает расчёт (watch), чтобы не запустить по устаревшим данным.

## 4.2 — автообновление статуса
Пока есть «живые» кампании (queued/running/moderating) — экран сам зовёт fetchTelegram по
интервалу (15с), setInterval со снятием в onUnmounted; вне живых статусов не опрашивает.

## 4.4 — экран отказа: пересдача + пустая причина
У rejected-кампании кнопка «Исправить и пересдать» → инлайн-форма правки (текст/ссылка/ОРД +
опц. документ модератору) → новый resubmitTelegram в telegram.ts (multipart POST .../resubmit).
При пустой status_reason — единая заглушка REASON_PLACEHOLDER (действенная, без «круга»).
В интерфейс TelegramCampaign добавлено moderator_file_path.

## 4.5 — подсказки
У moderating — «проверка ~4 часа»; пока песочница — метка «песочница» на каждой кампании.

## Причёсывание под домашний стиль кабинета (осмотр вживую 28.07)
Экран приведён к виду соседних клиентских экранов (эталон DashboardView): обёртка
v-container fluid pa-6 + scoped max-width:1100px (были прижаты к краям во всю ширину);
крупный заголовок + строка-описание; поля density=compact variant=outlined (была «рыхлая»
форма); контент собран в карточки «Новая кампания» / «Мои кампании» (была «полосатая» зона).
Палитра Forest не тронута. STATUS_LABELS дополнен moderating/needs_review/cancelled.
Починена мелочь-логика: «Рассчитать»/«Запустить» больше не активны с пустым списком номеров
в режиме «Свой список».

Миграция не нужна (moderator_file_path добавлена в Этапе 3; новые ярлыки статусов — без БД).

TDD. Приёмка (моя область): фронт-тесты Телеграма 22/22; полный фронт-набор 210 файлов /
1529 зелёные; vue-tsc + ESLint по изменённым файлам чисто. Проверено вживую в браузере
(клиент demo): двухшаговые кнопки, скрытие/показ полей по аудитории, гейт по номерам.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 12:57:57 +03:00

101 lines
4.0 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
vi.mock('../../resources/js/api/client', () => ({
apiClient: {
get: vi.fn(),
post: vi.fn(),
},
ensureCsrfCookie: vi.fn().mockResolvedValue(undefined),
}));
import {
fetchTelegram,
createTelegram,
fetchTelegramCampaign,
launchTelegram,
resubmitTelegram,
} from '../../resources/js/api/telegram';
import { apiClient, ensureCsrfCookie } from '../../resources/js/api/client';
describe('api/telegram', () => {
beforeEach(() => vi.clearAllMocks());
it('fetchTelegram() GETs /api/telegram/campaigns and unwraps', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: { campaigns: [], sandbox: true, balance_rub: '100.00', frozen_rub: '0.00' },
});
const res = await fetchTelegram();
expect(apiClient.get).toHaveBeenCalledWith('/api/telegram/campaigns');
expect(res.sandbox).toBe(true);
expect(res.balance_rub).toBe('100.00');
expect(res.campaigns).toEqual([]);
});
it('createTelegram() ensures csrf then POSTs /api/telegram/campaigns with payload', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: {
id: 1,
status: 'draft',
ad_text: 'Приходите',
ad_link: 'https://t.me/x',
audience_kind: 'list',
planned_count: 2,
estimated_cost_rub: '10.00',
},
});
const payload = {
ad_text: 'Приходите',
ad_link: 'https://t.me/x',
audience_kind: 'list' as const,
budget_cap_rub: '1000.00',
phones: ['79990000001', '79990000002'],
};
const c = await createTelegram(payload);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/telegram/campaigns', payload);
expect(c.id).toBe(1);
expect(c.status).toBe('draft');
expect(c.planned_count).toBe(2);
});
it('fetchTelegramCampaign(id) GETs /api/telegram/campaigns/{id} and returns {campaign}', async () => {
vi.mocked(apiClient.get).mockResolvedValue({
data: { campaign: { id: 3, status: 'draft_ready', ad_text: 'Y', ad_link: 'https://t.me/y' } },
});
const res = await fetchTelegramCampaign(3);
expect(apiClient.get).toHaveBeenCalledWith('/api/telegram/campaigns/3');
expect(res.campaign.id).toBe(3);
expect(res.campaign.status).toBe('draft_ready');
});
it('resubmitTelegram(id, payload) ensures csrf then POSTs multipart /resubmit', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: { id: 9, status: 'queued', ad_text: 'Исправлено', ad_link: 'https://t.me/x' },
});
const res = await resubmitTelegram(9, {
ad_text: 'Исправлено',
ad_link: 'https://t.me/x',
ord_category: 'Размещение рекламы',
});
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
const [url, body] = vi.mocked(apiClient.post).mock.calls[0];
expect(url).toBe('/api/telegram/campaigns/9/resubmit');
expect(body).toBeInstanceOf(FormData);
expect((body as FormData).get('ad_text')).toBe('Исправлено');
expect((body as FormData).get('ad_link')).toBe('https://t.me/x');
expect((body as FormData).get('ord_category')).toBe('Размещение рекламы');
expect(res.status).toBe('queued');
});
it('launchTelegram(id) ensures csrf then POSTs /api/telegram/campaigns/{id}/launch', async () => {
vi.mocked(apiClient.post).mockResolvedValue({
data: { id: 5, status: 'queued', ad_text: 'Z', ad_link: 'https://t.me/z' },
});
const res = await launchTelegram(5);
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
expect(apiClient.post).toHaveBeenCalledWith('/api/telegram/campaigns/5/launch');
expect(res.status).toBe('queued');
});
});