Files
portal/app/tests/Frontend/AdminVisitorsView.spec.ts
T
Дмитрий b6b8d6e0d6 feat(visitors): раздел «Посетители» в админке — воронка, каналы, визиты, кабинет
Воронка подсвечивает красным шаг, где теряем больше всего людей. Гости с VPN
и хостингов показаны отдельным числом, а не подмешаны в конверсию. Город при
VPN честно помечается как недостоверный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 05:03:38 +03:00

81 lines
3.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import AdminVisitorsView from '../../resources/js/views/admin/AdminVisitorsView.vue';
vi.mock('../../resources/js/api/adminVisitors', () => ({
getFunnel: vi.fn(),
getChannels: vi.fn(),
getVisits: vi.fn(),
getPortalActivity: vi.fn(),
}));
const api = await import('../../resources/js/api/adminVisitors');
const vuetify = createVuetify();
const mountView = () => mount(AdminVisitorsView, { global: { plugins: [vuetify] } });
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(api.getFunnel).mockResolvedValue({
period: '7d',
funnel: { view: 10, alive: 6, cta_click: 2, register_open: 1, register_done: 1, login_done: 1 },
vpn_or_hosting: 4,
});
vi.mocked(api.getChannels).mockResolvedValue([
{ channel: 'sms', people: 5, cta_clicks: 1, registrations: 0, logins: 0 },
{ channel: 'hh', people: 2, cta_clicks: 1, registrations: 1, logins: 1 },
]);
vi.mocked(api.getVisits).mockResolvedValue([]);
vi.mocked(api.getPortalActivity).mockResolvedValue({ screens: [], clients: [] });
});
describe('AdminVisitorsView', () => {
it('показывает шаги воронки с процентами перехода', async () => {
const w = mountView();
await flushPromises();
expect(w.text()).toContain('Открыли сайт');
expect(w.text()).toContain('Вошли в кабинет');
expect(w.text()).toContain('60% от предыдущего'); // 6 живых из 10 открывших
});
it('честно показывает, сколько гостей отсеяно как VPN/хостинг', async () => {
const w = mountView();
await flushPromises();
expect(w.text()).toContain('Гостей с хостингов и VPN за период: 4');
});
it('называет каналы по-человечески и считает конверсию в клиента', async () => {
const w = mountView();
await flushPromises();
expect(w.text()).toContain('Смс-рассылка');
expect(w.text()).toContain('Вакансия на hh.ru');
expect(w.text()).toContain('50%'); // hh: 1 регистрация из 2 человек
});
it('пустой список визитов — человеческий текст, а не пустая таблица', async () => {
const w = mountView();
await flushPromises();
expect(w.text()).toContain('Пока никто не заходил');
expect(w.text()).toContain('За период в кабинет никто не заходил');
});
it('когда никто не заходил — воронка не рисуется', async () => {
vi.mocked(api.getFunnel).mockResolvedValue({
period: 'today',
funnel: { view: 0, alive: 0, cta_click: 0, register_open: 0, register_done: 0, login_done: 0 },
vpn_or_hosting: 0,
});
vi.mocked(api.getChannels).mockResolvedValue([]);
const w = mountView();
await flushPromises();
expect(w.text()).toContain('Пока никто не заходил');
});
});