Files
portal/app/tests/Frontend/admin-advertising-view.spec.ts
T
Дмитрий 3917fb1344 feat(реклама показы): админ-экран ввода номера креатива Яндекса оператором
Задача 8e. SaaS-оператор видит кампании в статусе queued по всем тенантам и
вписывает номер оформленного в конструкторе Яндекса адаптивного креатива
yandex_creative_id, после чего кампанию можно запускать. Два эндпоинта
AdminAdvertisingController campaignsAwaiting и setCampaignCreative под
saas-admin+admin-db, карточка в AdminAdvertisingView, api-функции и типы.

🔴 Запись строго через сырой DB::table update только колонки yandex_creative_id
Eloquent-билдер добавил бы updated_at, а колоночный GRANT у crm_admin_user
разрешает писать лишь эту колонку тесты идут под суперюзером и это не ловят.
RLS не трогаем ad_campaigns уже покрыт srv_bypass и грантами.

Бэкенд AdminAd 19/19, реклама-модуль 195/195, фронт админки 11/11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 09:55:41 +03:00

272 lines
12 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 AdminAdvertisingView from '../../resources/js/views/admin/AdminAdvertisingView.vue';
import type { AdminAdvertisingSpendResponse, AdminAwaitingCampaign } 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,
fetchAdvertisingSpend: vi.fn(),
fetchAdSettings: vi.fn(),
updateAdSettings: vi.fn(),
fetchAdAwaitingCampaigns: vi.fn(),
setAdCampaignCreative: vi.fn(),
};
});
const adminApi = await import('../../resources/js/api/admin');
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(adminApi.fetchAdSettings).mockResolvedValue({ client_cpm_rub: '150.00', ad_margin_percent: '40.00' });
vi.mocked(adminApi.fetchAdAwaitingCampaigns).mockResolvedValue([]);
});
function makeAwaitingCampaign(overrides: Partial<AdminAwaitingCampaign> = {}): AdminAwaitingCampaign {
return {
id: 1,
tenant_id: 1,
tenant_name: 'Окна Москва',
name: 'Кампания на запуск',
status: 'queued',
yandex_creative_id: null,
landing_url: 'https://example.ru/landing',
estimated_impressions: 12000,
...overrides,
};
}
function makeResponse(overrides: Partial<AdminAdvertisingSpendResponse> = {}): AdminAdvertisingSpendResponse {
return {
data: [
{
tenant_id: 1,
tenant_name: 'Окна Москва',
client_spend_rub: '50000.00',
yandex_cost_rub: '40000.00',
our_margin_rub: '10000.00',
},
{
tenant_id: 2,
tenant_name: 'Двери СПб',
client_spend_rub: '20000.00',
yandex_cost_rub: '16000.00',
our_margin_rub: '4000.00',
},
],
totals: {
client_spend_rub: '70000.00',
yandex_cost_rub: '56000.00',
our_margin_rub: '14000.00',
},
ad_margin_percent: '40.00',
...overrides,
};
}
const mountView = () =>
mount(AdminAdvertisingView, {
global: { plugins: [createVuetify()] },
});
describe('AdminAdvertisingView ↔ GET /api/admin/advertising/spend', () => {
it('на mount зовёт fetchAdvertisingSpend(current_month) и рендерит строки + итоговую маржу', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockResolvedValueOnce(makeResponse());
const wrapper = mountView();
await flushPromises();
expect(adminApi.fetchAdvertisingSpend).toHaveBeenCalledTimes(1);
expect(adminApi.fetchAdvertisingSpend).toHaveBeenCalledWith('current_month');
expect(wrapper.findAll('[data-testid="ad-spend-row"]')).toHaveLength(2);
const marginEl = wrapper.find('[data-testid="ad-spend-total-margin"]');
expect(marginEl.exists()).toBe(true);
expect(marginEl.text()).toMatch(/14.000/);
});
it('смена периода вызывает перезагрузку с новым period', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockResolvedValue(makeResponse());
const wrapper = mountView();
await flushPromises();
expect(adminApi.fetchAdvertisingSpend).toHaveBeenCalledTimes(1);
const vm = wrapper.vm as unknown as { period: string };
vm.period = 'last_month';
await flushPromises();
expect(adminApi.fetchAdvertisingSpend).toHaveBeenCalledTimes(2);
expect(adminApi.fetchAdvertisingSpend).toHaveBeenLastCalledWith('last_month');
});
it('пустые данные → сообщение "За период рекламных расходов нет"', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockResolvedValueOnce(
makeResponse({
data: [],
totals: { client_spend_rub: '0.00', yandex_cost_rub: '0.00', our_margin_rub: '0.00' },
}),
);
const wrapper = mountView();
await flushPromises();
expect(wrapper.findAll('[data-testid="ad-spend-row"]')).toHaveLength(0);
expect(wrapper.text()).toContain('За период рекламных расходов нет');
});
it('reject → alert с ошибкой виден', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockRejectedValueOnce(new Error('500'));
const wrapper = mountView();
await flushPromises();
expect(wrapper.find('[data-testid="fetch-error-alert"]').exists()).toBe(true);
});
});
describe('AdminAdvertisingView ↔ GET/PUT /api/admin/advertising/settings (client_cpm_rub + ad_margin_percent)', () => {
it('на mount зовёт fetchAdSettings и показывает загруженные значения в полях', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockResolvedValueOnce(makeResponse());
vi.mocked(adminApi.fetchAdSettings).mockResolvedValueOnce({
client_cpm_rub: '220.50',
ad_margin_percent: '35.00',
});
const wrapper = mountView();
await flushPromises();
expect(adminApi.fetchAdSettings).toHaveBeenCalledTimes(1);
const input = wrapper.find('[data-testid="ad-cpm-input"] input');
expect((input.element as HTMLInputElement).value).toBe('220.50');
const marginInput = wrapper.find('[data-testid="ad-margin-input"] input');
expect((marginInput.element as HTMLInputElement).value).toBe('35.00');
});
it('клик «Сохранить» зовёт updateAdSettings с обоими полями и показывает "Цена сохранена"', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockResolvedValueOnce(makeResponse());
vi.mocked(adminApi.fetchAdSettings).mockResolvedValueOnce({
client_cpm_rub: '150.00',
ad_margin_percent: '40.00',
});
vi.mocked(adminApi.updateAdSettings).mockResolvedValueOnce({
client_cpm_rub: '300.00',
ad_margin_percent: '45.00',
});
const wrapper = mountView();
await flushPromises();
const input = wrapper.find('[data-testid="ad-cpm-input"] input');
await input.setValue('300.00');
const marginInput = wrapper.find('[data-testid="ad-margin-input"] input');
await marginInput.setValue('45.00');
await wrapper.find('[data-testid="ad-cpm-save"]').trigger('click');
await flushPromises();
expect(adminApi.updateAdSettings).toHaveBeenCalledTimes(1);
expect(adminApi.updateAdSettings).toHaveBeenCalledWith({
client_cpm_rub: '300.00',
ad_margin_percent: '45.00',
});
const saved = wrapper.find('[data-testid="ad-cpm-saved"]');
expect(saved.exists()).toBe(true);
expect(saved.text()).toContain('Цена сохранена');
});
it('updateAdSettings reject (422) → показывает ошибку, не "Цена сохранена"', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockResolvedValueOnce(makeResponse());
vi.mocked(adminApi.fetchAdSettings).mockResolvedValueOnce({
client_cpm_rub: '150.00',
ad_margin_percent: '40.00',
});
vi.mocked(adminApi.updateAdSettings).mockRejectedValueOnce(new Error('422'));
const wrapper = mountView();
await flushPromises();
await wrapper.find('[data-testid="ad-cpm-save"]').trigger('click');
await flushPromises();
expect(wrapper.find('[data-testid="ad-cpm-error"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="ad-cpm-saved"]').exists()).toBe(false);
});
});
describe('AdminAdvertisingView ↔ кампании, ждущие номер креатива (8e)', () => {
it('на mount зовёт fetchAdAwaitingCampaigns и рендерит строки', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockResolvedValueOnce(makeResponse());
vi.mocked(adminApi.fetchAdAwaitingCampaigns).mockResolvedValueOnce([
makeAwaitingCampaign({ id: 1, name: 'Кампания 1' }),
makeAwaitingCampaign({ id: 2, name: 'Кампания 2' }),
]);
const wrapper = mountView();
await flushPromises();
expect(adminApi.fetchAdAwaitingCampaigns).toHaveBeenCalledTimes(1);
expect(wrapper.findAll('[data-testid="ad-awaiting-row"]')).toHaveLength(2);
expect(wrapper.text()).toContain('Кампания 1');
expect(wrapper.text()).toContain('Кампания 2');
});
it('пустой список → сообщение "Кампаний, ждущих номер креатива, нет"', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockResolvedValueOnce(makeResponse());
vi.mocked(adminApi.fetchAdAwaitingCampaigns).mockResolvedValueOnce([]);
const wrapper = mountView();
await flushPromises();
expect(wrapper.findAll('[data-testid="ad-awaiting-row"]')).toHaveLength(0);
expect(wrapper.find('[data-testid="ad-awaiting-empty"]').exists()).toBe(true);
expect(wrapper.text()).toContain('Кампаний, ждущих номер креатива, нет');
});
it('клик «Сохранить» зовёт setAdCampaignCreative с id кампании и введённым номером', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockResolvedValueOnce(makeResponse());
vi.mocked(adminApi.fetchAdAwaitingCampaigns).mockResolvedValueOnce([
makeAwaitingCampaign({ id: 42, yandex_creative_id: null }),
]);
vi.mocked(adminApi.setAdCampaignCreative).mockResolvedValueOnce({ id: 42, yandex_creative_id: 111222333 });
const wrapper = mountView();
await flushPromises();
const input = wrapper.find('[data-testid="ad-creative-input"] input');
await input.setValue('111222333');
await wrapper.find('[data-testid="ad-creative-save"]').trigger('click');
await flushPromises();
expect(adminApi.setAdCampaignCreative).toHaveBeenCalledTimes(1);
expect(adminApi.setAdCampaignCreative).toHaveBeenCalledWith(42, 111222333);
const saved = wrapper.find('[data-testid="ad-creative-saved"]');
expect(saved.exists()).toBe(true);
expect(saved.text()).toContain('Сохранено');
});
it('setAdCampaignCreative reject → показывает ошибку по строке, не "Сохранено"', async () => {
vi.mocked(adminApi.fetchAdvertisingSpend).mockResolvedValueOnce(makeResponse());
vi.mocked(adminApi.fetchAdAwaitingCampaigns).mockResolvedValueOnce([makeAwaitingCampaign({ id: 7 })]);
vi.mocked(adminApi.setAdCampaignCreative).mockRejectedValueOnce(new Error('422'));
const wrapper = mountView();
await flushPromises();
const input = wrapper.find('[data-testid="ad-creative-input"] input');
await input.setValue('5');
await wrapper.find('[data-testid="ad-creative-save"]').trigger('click');
await flushPromises();
expect(wrapper.find('[data-testid="ad-creative-error"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="ad-creative-saved"]').exists()).toBe(false);
});
});