Files
portal/app/tests/Feature/AdminVisitorsTest.php
T
Дмитрий 992e62fd2c fix(visitors): карточка «Что смотрят в кабинете» падала — нет колонки t.name
Запрос активности портала просил у tenants колонку name, которой не существует:
в схеме она называется organization_name. База отвечала 42703 Undefined column,
фронт показывал красную плашку «Не удалось загрузить данные» поверх страницы
/admin/visitors. Остальные три запроса страницы работали, поэтому карточки
рисовались пустыми, а не сломанными.

Наружу поле по-прежнему отдаётся как name — контракт API и фронт не менялись.

Добавлен тест на portal-эндпоинт. Из четырёх запросов страницы тестами были
покрыты три; единственный непокрытый и оказался сломанным — тот же класс потери,
что CsvReconcileJobTest: код едет, тест нет.

Проверки: AdminVisitors 5/5, смежные Tracking 14/14, Pint чисто, Larastan 0 ошибок.
Прод не тронут — выката не было.

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

110 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\Concerns\SharesAdminPdo;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
uses(SharesAdminPdo::class);
/** Гость + его шаги. */
function visitor(string $channel, bool $human, array $events, string $device = 'phone'): string
{
$id = (string) Str::uuid();
DB::table('site_visitors')->insert([
'id' => $id, 'first_seen_at' => now(), 'last_seen_at' => now(),
'channel' => $channel, 'device' => $device, 'is_human' => $human, 'is_datacenter' => false,
]);
foreach ($events as $e) {
DB::table('site_events')->insert([
'visitor_id' => $id, 'event' => $e, 'occurred_at' => now(), 'meta' => '{}',
]);
}
return $id;
}
beforeEach(function () {
DB::table('site_events')->delete();
DB::table('site_visitors')->delete();
});
test('воронка считает шаги и конверсии', function () {
visitor('sms', true, ['view', 'alive']);
visitor('sms', true, ['view', 'alive', 'cta_click', 'register_open', 'register_done', 'login_done']);
visitor('direct', false, ['view']); // браузер, но не человек
$r = $this->getJson('/api/admin/visitors/funnel?period=7d');
$r->assertStatus(200);
expect($r->json('funnel.view'))->toBe(3);
expect($r->json('funnel.alive'))->toBe(2);
expect($r->json('funnel.cta_click'))->toBe(1);
expect($r->json('funnel.register_done'))->toBe(1);
expect($r->json('funnel.login_done'))->toBe(1);
});
test('таблица каналов группирует людей по источнику', function () {
visitor('sms', true, ['view', 'alive']);
visitor('sms', true, ['view', 'alive', 'cta_click']);
visitor('hh', true, ['view', 'alive', 'cta_click', 'register_done']);
$r = $this->getJson('/api/admin/visitors/channels?period=7d');
$r->assertStatus(200);
$sms = collect($r->json('channels'))->firstWhere('channel', 'sms');
expect($sms['people'])->toBe(2);
expect($sms['cta_clicks'])->toBe(1);
expect($sms['registrations'])->toBe(0);
$hh = collect($r->json('channels'))->firstWhere('channel', 'hh');
expect($hh['registrations'])->toBe(1);
});
test('список визитов отдаёт цепочку шагов и пометку VPN', function () {
$id = visitor('direct', true, ['view', 'alive', 'cta_click']);
DB::table('site_visitors')->where('id', $id)->update([
'is_datacenter' => true, 'city' => 'Amsterdam', 'asn_org' => 'NordVPN', 'ip' => '1.2.3.4',
]);
$r = $this->getJson('/api/admin/visitors/visits?period=7d');
$r->assertStatus(200);
$v = $r->json('visits.0');
expect($v['is_datacenter'])->toBeTrue();
expect($v['steps'])->toContain('cta_click');
});
test('пустой период — нули, а не ошибка', function () {
$r = $this->getJson('/api/admin/visitors/funnel?period=today');
$r->assertStatus(200);
expect($r->json('funnel.view'))->toBe(0);
});
test('активность в кабинете: экраны и клиенты, привязанные к гостю', function () {
$tenant = Tenant::factory()->create(['organization_name' => 'ООО Ромашка']);
$id = visitor('sms', true, ['view', 'alive', 'login_done']);
DB::table('site_visitors')->where('id', $id)->update(['tenant_id' => $tenant->id]);
DB::table('site_events')->insert([
['visitor_id' => $id, 'event' => 'portal_screen', 'screen' => '/deals', 'occurred_at' => now(), 'meta' => '{}'],
['visitor_id' => $id, 'event' => 'portal_screen', 'screen' => '/deals', 'occurred_at' => now(), 'meta' => '{}'],
['visitor_id' => $id, 'event' => 'portal_screen', 'screen' => '/billing', 'occurred_at' => now(), 'meta' => '{}'],
]);
$r = $this->getJson('/api/admin/visitors/portal?period=7d');
$r->assertStatus(200);
$deals = collect($r->json('screens'))->firstWhere('screen', '/deals');
expect($deals['opens'])->toBe(2);
expect($deals['people'])->toBe(1);
expect(collect($r->json('clients'))->pluck('name'))->toContain('ООО Ромашка');
});