b6b8d6e0d6
Воронка подсвечивает красным шаг, где теряем больше всего людей. Гости с VPN и хостингов показаны отдельным числом, а не подмешаны в конверсию. Город при VPN честно помечается как недостоверный. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
3.3 KiB
TypeScript
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('Пока никто не заходил');
|
|
});
|
|
});
|