9b9055753b
Закрывает фронтенд Этапа 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>
101 lines
4.0 KiB
TypeScript
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');
|
|
});
|
|
});
|