88 lines
3.1 KiB
PHP
88 lines
3.1 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
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);
|
||
|
|
});
|