new test files
This commit is contained in:
306
tests/Feature/Worlds/WorldAnalyticsTest.php
Normal file
306
tests/Feature/Worlds/WorldAnalyticsTest.php
Normal file
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\World;
|
||||
use App\Models\WorldAnalyticsEvent;
|
||||
use App\Models\WorldRewardGrant;
|
||||
use App\Models\WorldSubmission;
|
||||
use App\Models\Artwork;
|
||||
use App\Models\Group;
|
||||
use App\Models\GroupChallenge;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Testing\AssertableInertia;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function analyticsWorld(User $creator, array $attributes = []): World
|
||||
{
|
||||
$slugSuffix = Str::lower(Str::random(6));
|
||||
|
||||
return World::query()->create(array_merge([
|
||||
'title' => 'Analytics World ' . $slugSuffix,
|
||||
'slug' => 'analytics-world-' . $slugSuffix,
|
||||
'tagline' => 'Measured campaign storytelling.',
|
||||
'summary' => 'A world used to verify analytics reporting.',
|
||||
'description' => 'Analytics world description',
|
||||
'theme_key' => 'summer',
|
||||
'status' => World::STATUS_PUBLISHED,
|
||||
'type' => World::TYPE_CAMPAIGN,
|
||||
'is_featured' => true,
|
||||
'published_at' => now()->subDays(5),
|
||||
'starts_at' => now()->subDays(3),
|
||||
'ends_at' => now()->addDays(10),
|
||||
'created_by_user_id' => $creator->id,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
it('records worlds analytics events through the public api', function (): void {
|
||||
$creator = User::factory()->create();
|
||||
$world = analyticsWorld($creator);
|
||||
|
||||
$this->postJson(route('api.worlds.analytics.events.store'), [
|
||||
'world_id' => $world->id,
|
||||
'event_type' => 'world_cta_clicked',
|
||||
'section_key' => 'hero',
|
||||
'cta_key' => 'main_world_cta',
|
||||
'entity_type' => 'world',
|
||||
'entity_id' => $world->id,
|
||||
'entity_title' => $world->title,
|
||||
'source_surface' => 'homepage_spotlight',
|
||||
'source_detail' => 'primary',
|
||||
'visitor_token' => 'guest-analytics-token',
|
||||
])->assertAccepted()->assertJson(['ok' => true]);
|
||||
|
||||
$this->assertDatabaseHas('world_analytics_events', [
|
||||
'world_id' => $world->id,
|
||||
'event_type' => 'world_cta_clicked',
|
||||
'section_key' => 'hero',
|
||||
'cta_key' => 'main_world_cta',
|
||||
'entity_type' => 'world',
|
||||
'entity_id' => $world->id,
|
||||
'entity_title' => $world->title,
|
||||
'source_surface' => 'homepage_spotlight',
|
||||
'source_detail' => 'primary',
|
||||
'viewer_type' => 'guest',
|
||||
'visitor_key' => hash('sha256', 'visitor:guest-analytics-token'),
|
||||
]);
|
||||
|
||||
$this->postJson(route('api.worlds.analytics.events.store'), [
|
||||
'world_id' => $world->id,
|
||||
'event_type' => 'world_source_impression',
|
||||
'section_key' => 'spotlight',
|
||||
'source_surface' => 'homepage_spotlight',
|
||||
'source_detail' => 'primary',
|
||||
'visitor_token' => 'guest-analytics-impression',
|
||||
])->assertAccepted()->assertJson(['ok' => true]);
|
||||
|
||||
$this->assertDatabaseHas('world_analytics_events', [
|
||||
'world_id' => $world->id,
|
||||
'event_type' => 'world_source_impression',
|
||||
'section_key' => 'spotlight',
|
||||
'source_surface' => 'homepage_spotlight',
|
||||
'source_detail' => 'primary',
|
||||
'viewer_type' => 'guest',
|
||||
'visitor_key' => hash('sha256', 'visitor:guest-analytics-impression'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes analytics summaries and edition comparison on studio world pages', function (): void {
|
||||
$moderator = User::factory()->create([
|
||||
'role' => 'moderator',
|
||||
'username' => 'analyticsmod-' . Str::lower(Str::random(6)),
|
||||
]);
|
||||
$groupOwner = User::factory()->create([
|
||||
'username' => 'challenge-owner-' . Str::lower(Str::random(6)),
|
||||
]);
|
||||
$group = Group::factory()->create([
|
||||
'name' => 'Retro Group ' . Str::upper(Str::random(4)),
|
||||
'slug' => 'retro-group-' . Str::lower(Str::random(4)),
|
||||
]);
|
||||
$challenge = GroupChallenge::query()->create([
|
||||
'group_id' => $group->id,
|
||||
'title' => 'Retro Challenge ' . Str::upper(Str::random(4)),
|
||||
'slug' => 'retro-challenge-' . Str::lower(Str::random(4)),
|
||||
'summary' => 'A linked challenge for analytics verification.',
|
||||
'description' => 'Challenge description',
|
||||
'visibility' => GroupChallenge::VISIBILITY_PUBLIC,
|
||||
'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC,
|
||||
'status' => GroupChallenge::STATUS_ACTIVE,
|
||||
'start_at' => now()->subDay(),
|
||||
'end_at' => now()->addDays(7),
|
||||
'created_by_user_id' => $groupOwner->id,
|
||||
]);
|
||||
|
||||
$currentWorld = analyticsWorld($moderator, [
|
||||
'title' => 'Retro Month 2026',
|
||||
'slug' => 'retro-month-2026-' . Str::lower(Str::random(4)),
|
||||
'recurrence_key' => 'retro-month',
|
||||
'edition_year' => 2026,
|
||||
'linked_challenge_id' => $challenge->id,
|
||||
]);
|
||||
$previousWorld = analyticsWorld($moderator, [
|
||||
'title' => 'Retro Month 2025',
|
||||
'slug' => 'retro-month-2025-' . Str::lower(Str::random(4)),
|
||||
'recurrence_key' => 'retro-month',
|
||||
'edition_year' => 2025,
|
||||
'starts_at' => now()->subYear(),
|
||||
'ends_at' => now()->subYear()->addDays(10),
|
||||
'published_at' => now()->subYear()->subDays(2),
|
||||
]);
|
||||
$artwork = Artwork::factory()->for($moderator)->create([
|
||||
'title' => 'Analytics Artwork',
|
||||
'slug' => 'analytics-artwork-' . Str::lower(Str::random(4)),
|
||||
'artwork_status' => 'published',
|
||||
'published_at' => now()->subDay(),
|
||||
'is_public' => true,
|
||||
'visibility' => Artwork::VISIBILITY_PUBLIC,
|
||||
]);
|
||||
|
||||
WorldSubmission::query()->create([
|
||||
'world_id' => $currentWorld->id,
|
||||
'artwork_id' => $artwork->id,
|
||||
'submitted_by_user_id' => $moderator->id,
|
||||
'status' => WorldSubmission::STATUS_LIVE,
|
||||
'is_featured' => true,
|
||||
'created_at' => now()->subDay(),
|
||||
'updated_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
WorldRewardGrant::query()->create([
|
||||
'user_id' => $moderator->id,
|
||||
'world_id' => $currentWorld->id,
|
||||
'artwork_id' => $artwork->id,
|
||||
'reward_type' => 'winner',
|
||||
'grant_source' => 'manual',
|
||||
'granted_at' => now()->subHours(6),
|
||||
]);
|
||||
|
||||
WorldRewardGrant::query()->create([
|
||||
'user_id' => $moderator->id,
|
||||
'world_id' => $previousWorld->id,
|
||||
'artwork_id' => $artwork->id,
|
||||
'reward_type' => 'featured',
|
||||
'grant_source' => 'manual',
|
||||
'granted_at' => now()->subYear(),
|
||||
]);
|
||||
|
||||
collect([
|
||||
[
|
||||
'world_id' => $currentWorld->id,
|
||||
'event_type' => 'world_source_impression',
|
||||
'section_key' => 'spotlight',
|
||||
'source_surface' => 'homepage_spotlight',
|
||||
'source_detail' => 'primary',
|
||||
'visitor_key' => hash('sha256', 'visitor:impression-one'),
|
||||
'occurred_at' => Carbon::now()->subHours(4),
|
||||
],
|
||||
[
|
||||
'world_id' => $currentWorld->id,
|
||||
'event_type' => 'world_source_impression',
|
||||
'section_key' => 'card',
|
||||
'source_surface' => 'worlds_index',
|
||||
'source_detail' => 'featured',
|
||||
'visitor_key' => hash('sha256', 'visitor:impression-two'),
|
||||
'occurred_at' => Carbon::now()->subHours(3),
|
||||
],
|
||||
[
|
||||
'world_id' => $currentWorld->id,
|
||||
'event_type' => 'world_viewed',
|
||||
'source_surface' => 'homepage_spotlight',
|
||||
'source_detail' => 'primary',
|
||||
'visitor_key' => hash('sha256', 'visitor:viewer-one'),
|
||||
'occurred_at' => Carbon::now()->subHours(4),
|
||||
],
|
||||
[
|
||||
'world_id' => $currentWorld->id,
|
||||
'event_type' => 'world_viewed',
|
||||
'source_surface' => 'worlds_index',
|
||||
'source_detail' => 'featured',
|
||||
'visitor_key' => hash('sha256', 'visitor:viewer-two'),
|
||||
'occurred_at' => Carbon::now()->subHours(3),
|
||||
],
|
||||
[
|
||||
'world_id' => $currentWorld->id,
|
||||
'event_type' => 'world_source_clicked',
|
||||
'source_surface' => 'homepage_spotlight',
|
||||
'source_detail' => 'primary',
|
||||
'entity_type' => 'world',
|
||||
'entity_id' => $currentWorld->id,
|
||||
'entity_title' => $currentWorld->title,
|
||||
'visitor_key' => hash('sha256', 'visitor:viewer-one'),
|
||||
'occurred_at' => Carbon::now()->subHours(4),
|
||||
],
|
||||
[
|
||||
'world_id' => $currentWorld->id,
|
||||
'event_type' => 'world_cta_clicked',
|
||||
'section_key' => 'hero',
|
||||
'cta_key' => 'main_world_cta',
|
||||
'source_surface' => 'homepage_spotlight',
|
||||
'visitor_key' => hash('sha256', 'visitor:viewer-one'),
|
||||
'occurred_at' => Carbon::now()->subHours(4),
|
||||
],
|
||||
[
|
||||
'world_id' => $currentWorld->id,
|
||||
'event_type' => 'world_challenge_cta_clicked',
|
||||
'section_key' => 'challenge',
|
||||
'challenge_id' => $challenge->id,
|
||||
'visitor_key' => hash('sha256', 'visitor:challenge-viewer'),
|
||||
'occurred_at' => Carbon::now()->subHours(2),
|
||||
],
|
||||
[
|
||||
'world_id' => $currentWorld->id,
|
||||
'event_type' => 'world_submission_created',
|
||||
'section_key' => 'community_submissions',
|
||||
'source_surface' => 'upload_flow',
|
||||
'entity_type' => 'artwork',
|
||||
'entity_id' => $artwork->id,
|
||||
'entity_title' => $artwork->title,
|
||||
'visitor_key' => hash('sha256', 'system:submission'),
|
||||
'occurred_at' => Carbon::now()->subHours(2),
|
||||
],
|
||||
[
|
||||
'world_id' => $currentWorld->id,
|
||||
'event_type' => 'world_submission_approved',
|
||||
'section_key' => 'community_submissions',
|
||||
'source_surface' => 'upload_flow',
|
||||
'entity_type' => 'artwork',
|
||||
'entity_id' => $artwork->id,
|
||||
'entity_title' => $artwork->title,
|
||||
'visitor_key' => hash('sha256', 'system:approval'),
|
||||
'occurred_at' => Carbon::now()->subHours(2),
|
||||
],
|
||||
[
|
||||
'world_id' => $currentWorld->id,
|
||||
'event_type' => 'world_reward_granted',
|
||||
'section_key' => 'rewards',
|
||||
'entity_type' => 'artwork',
|
||||
'entity_id' => $artwork->id,
|
||||
'entity_title' => $artwork->title,
|
||||
'visitor_key' => hash('sha256', 'system:reward'),
|
||||
'occurred_at' => Carbon::now()->subHours(1),
|
||||
],
|
||||
[
|
||||
'world_id' => $previousWorld->id,
|
||||
'event_type' => 'world_viewed',
|
||||
'source_surface' => 'navigation',
|
||||
'source_detail' => 'archive',
|
||||
'visitor_key' => hash('sha256', 'visitor:archive-viewer'),
|
||||
'occurred_at' => Carbon::now()->subYear(),
|
||||
],
|
||||
])->each(function (array $attributes) use ($currentWorld, $previousWorld): void {
|
||||
$world = (int) $attributes['world_id'] === (int) $currentWorld->id ? $currentWorld : $previousWorld;
|
||||
|
||||
WorldAnalyticsEvent::query()->create(array_merge([
|
||||
'world_slug' => $world->slug,
|
||||
'world_type' => $world->type,
|
||||
'recurrence_key' => $world->recurrence_key,
|
||||
'edition_year' => $world->edition_year,
|
||||
'viewer_type' => 'guest',
|
||||
'user_id' => null,
|
||||
'meta' => null,
|
||||
], $attributes));
|
||||
});
|
||||
|
||||
$this->actingAs($moderator)
|
||||
->get(route('studio.worlds.edit', ['world' => $currentWorld->id]))
|
||||
->assertOk()
|
||||
->assertInertia(fn (AssertableInertia $page) => $page
|
||||
->component('Studio/StudioWorldEditor')
|
||||
->where('world.analytics.ranges.30d.summary.views', 2)
|
||||
->where('world.analytics.ranges.30d.summary.unique_visitors', 2)
|
||||
->where('world.analytics.ranges.30d.summary.promotion_impressions', 2)
|
||||
->where('world.analytics.ranges.30d.summary.cta_clicks', 1)
|
||||
->where('world.analytics.ranges.30d.summary.reward_grants', 1)
|
||||
->where('world.analytics.ranges.30d.participation.live', 1)
|
||||
->where('world.analytics.ranges.30d.sources.0.impressions', 1)
|
||||
->where('world.analytics.ranges.30d.sources.0.clickthrough_rate', 1)
|
||||
->where('world.analytics.ranges.30d.challenge.linked_challenge_id', $challenge->id)
|
||||
->where('world.analytics.ranges.30d.challenge.click_to_submission_conversion', 1)
|
||||
->where('world.analytics.ranges.30d.sources.0.source_surface', 'homepage_spotlight')
|
||||
->where('world.analytics.edition_comparison.recurrence_key', 'retro-month')
|
||||
->has('world.analytics.edition_comparison.editions', 2));
|
||||
});
|
||||
Reference in New Issue
Block a user