create([ 'username' => 'publicworlds-' . Str::lower(Str::random(6)), 'name' => 'Public Worlds', ]); unset($attributes['creator']); return World::query()->create(array_merge([ 'title' => 'Summer Slam 2026', 'slug' => 'summer-slam-2026', 'tagline' => 'Sunlit publishing and warm-color campaigns.', 'summary' => 'A bright world for summer culture across the platform.', 'teaser_title' => 'Now live: Summer Slam', 'teaser_summary' => 'A bright world for summer culture across the platform.', 'description' => 'Public world description', 'theme_key' => 'summer', 'status' => World::STATUS_PUBLISHED, 'type' => World::TYPE_SEASONAL, 'is_featured' => true, 'is_active_campaign' => true, 'is_homepage_featured' => true, 'campaign_priority' => 250, 'campaign_label' => 'Seasonal spotlight', 'starts_at' => Carbon::now()->subDays(2), 'ends_at' => Carbon::now()->addDays(14), 'promotion_starts_at' => Carbon::now()->subDay(), 'promotion_ends_at' => Carbon::now()->addDays(7), 'published_at' => Carbon::now()->subDays(10), 'created_by_user_id' => $creator->id, ], $attributes)); } function worldNewsCategory(array $attributes = []): NewsCategory { return NewsCategory::query()->create(array_merge([ 'name' => 'World Updates', 'slug' => 'world-updates-' . Str::lower(Str::random(6)), 'description' => 'Editorial context for worlds and linked campaigns.', 'position' => 0, 'is_active' => true, ], $attributes)); } function publishedWorldNews(User $author, NewsCategory $category, array $attributes = []): NewsArticle { return NewsArticle::query()->create(array_merge([ 'title' => 'World challenge update', 'slug' => 'world-challenge-update-' . Str::lower(Str::random(6)), 'excerpt' => 'An editorial update for the linked world challenge.', 'content' => "# World challenge update\n\nEditorial context for the linked challenge.", 'author_id' => $author->id, 'category_id' => $category->id, 'type' => NewsArticle::TYPE_ANNOUNCEMENT, 'status' => 'published', 'editorial_status' => NewsArticle::EDITORIAL_STATUS_PUBLISHED, 'published_at' => now()->subHour(), 'is_featured' => true, 'is_pinned' => false, ], $attributes)); } it('renders public worlds index and detail pages', function (): void { $world = publicWorld(); $this->get(route('worlds.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldIndex') ->where('spotlightWorld.title', 'Summer Slam 2026') ->where('spotlightWorld.campaign_state_label', 'Live now') ->has('activeWorlds')); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('world.title', 'Summer Slam 2026') ->where('world.slug', 'summer-slam-2026')); }); it('includes rewarded contributors on public world pages', function (): void { $creator = User::factory()->create([ 'username' => 'rewardedcreator-' . Str::lower(Str::random(6)), ]); $world = publicWorld(); $artwork = \App\Models\Artwork::factory()->for($creator)->create([ 'title' => 'World Winner Artwork', 'slug' => 'world-winner-artwork', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => \App\Models\Artwork::VISIBILITY_PUBLIC, ]); WorldRewardGrant::query()->create([ 'user_id' => $creator->id, 'world_id' => $world->id, 'artwork_id' => $artwork->id, 'reward_type' => 'winner', 'grant_source' => 'manual', 'granted_at' => now()->subHour(), ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('rewardedContributors.count', 1) ->where('rewardedContributors.creator_count', 1) ->where('rewardedContributors.counts.winner', 1) ->where('rewardedContributors.items.0.badge_label', $world->title . ' Winner')); }); it('renders recap payloads for ended worlds with published recaps', function (): void { $creator = User::factory()->create([ 'username' => 'recapcreator-' . Str::lower(Str::random(6)), 'name' => 'Recap Creator', ]); $world = publicWorld([ 'creator' => $creator, 'title' => 'Summer Slam 2025', 'slug' => 'summer-slam-2025', 'status' => World::STATUS_ARCHIVED, 'is_active_campaign' => false, 'is_homepage_featured' => false, 'accepts_submissions' => true, 'community_section_enabled' => true, 'starts_at' => now()->subDays(30), 'ends_at' => now()->subDays(5), 'recap_status' => World::RECAP_STATUS_PUBLISHED, 'recap_title' => 'Summer Slam 2025 recap', 'recap_summary' => 'A tighter archive-facing summary for the edition.', 'recap_intro' => '
The edition closed with standout artworks, community participation, and a published editorial recap.
', 'recap_cover_path' => 'worlds/recaps/summer-slam-2025-cover.jpg', 'recap_published_at' => now()->subDay(), 'recap_stats_snapshot_json' => [ 'captured_at' => now()->subDay()->toIso8601String(), 'summary' => [ 'views' => 1200, 'unique_visitors' => 640, 'submissions' => 18, 'live_participations' => 18, 'featured_participations' => 3, 'reward_grants' => 1, 'challenge_clicks' => 42, 'winner_count' => 1, 'finalist_count' => 0, 'featured_artwork_count' => 2, ], ], ]); $category = worldNewsCategory([ 'name' => 'Recap Stories', 'slug' => 'recap-stories-' . Str::lower(Str::random(6)), ]); $article = publishedWorldNews($creator, $category, [ 'title' => 'Summer Slam 2025 closing recap', 'slug' => 'summer-slam-2025-closing-recap-' . Str::lower(Str::random(6)), 'excerpt' => 'The final recap story for Summer Slam 2025.', ]); $world->update(['recap_article_id' => $article->id]); $featuredArtwork = Artwork::factory()->for($creator)->create([ 'title' => 'Curated Edition Highlight', 'slug' => 'curated-edition-highlight-' . Str::lower(Str::random(6)), 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $communityArtwork = Artwork::factory()->for($creator)->create([ 'title' => 'Community Spotlight Piece', 'slug' => 'community-spotlight-piece-' . Str::lower(Str::random(6)), 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $group = Group::factory()->for($creator, 'owner')->create([ 'name' => 'Recap Crew', 'slug' => 'recap-crew-' . Str::lower(Str::random(6)), ]); $world->worldRelations()->create([ 'section_key' => 'featured_artworks', 'related_type' => 'artwork', 'related_id' => $featuredArtwork->id, 'context_label' => 'Editorial highlight', 'sort_order' => 0, 'is_featured' => true, ]); $world->worldRelations()->create([ 'section_key' => 'featured_creators', 'related_type' => 'user', 'related_id' => $creator->id, 'context_label' => 'Edition lead', 'sort_order' => 0, 'is_featured' => true, ]); $world->worldRelations()->create([ 'section_key' => 'featured_groups', 'related_type' => 'group', 'related_id' => $group->id, 'context_label' => 'Community group', 'sort_order' => 0, 'is_featured' => true, ]); $world->worldRelations()->create([ 'section_key' => 'news', 'related_type' => 'news', 'related_id' => $article->id, 'context_label' => 'Closing story', 'sort_order' => 0, 'is_featured' => true, ]); $submission = WorldSubmission::query()->create([ 'world_id' => $world->id, 'artwork_id' => $communityArtwork->id, 'submitted_by_user_id' => $creator->id, 'status' => WorldSubmission::STATUS_LIVE, 'is_featured' => true, 'featured_at' => now()->subHours(12), 'created_at' => now()->subDay(), 'updated_at' => now()->subDay(), ]); WorldRewardGrant::query()->create([ 'user_id' => $creator->id, 'world_id' => $world->id, 'world_submission_id' => $submission->id, 'artwork_id' => $communityArtwork->id, 'reward_type' => 'winner', 'grant_source' => 'manual', 'granted_at' => now()->subHours(6), ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('world.title', 'Summer Slam 2025') ->where('world.has_recap', true) ->where('world.cta_label', 'Read full recap') ->where('world.cta_url', route('news.show', ['slug' => $article->slug])) ->where('recap.status', 'published') ->where('recap.title', 'Summer Slam 2025 recap') ->where('recap.summary', 'A tighter archive-facing summary for the edition.') ->where('recap.cover_url', rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/') . '/worlds/recaps/summer-slam-2025-cover.jpg') ->where('recap.article.title', 'Summer Slam 2025 closing recap') ->where('recap.featured_artworks.items.0.title', 'Curated Edition Highlight') ->where('recap.community_highlights.items.0.title', 'Community Spotlight Piece') ->where('recap.creators.items.0.title', 'Recap Creator') ->where('recap.creators.rewarded.0.badge_label', 'Summer Slam 2025 Winner') ->where('recap.stats.source', 'snapshot') ->where('recap.stats.items.0.key', 'views') ->where('sections', [])); }); it('exposes linked challenge panels, derived entries, and challenge backlinks', function (): void { $owner = User::factory()->create([ 'username' => 'challenge-owner-' . Str::lower(Str::random(6)), ]); $group = Group::factory()->for($owner, 'owner')->create(); $world = publicWorld([ 'linked_challenge_id' => null, 'show_linked_challenge_section' => true, 'show_linked_challenge_entries' => true, 'show_linked_challenge_winners' => true, 'challenge_teaser_override' => 'World-specific framing for the linked challenge.', ]); $winner = Artwork::factory()->for($owner)->create([ 'group_id' => $group->id, 'title' => 'Challenge Champion', 'slug' => 'challenge-champion', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $entry = Artwork::factory()->for($owner)->create([ 'group_id' => $group->id, 'title' => 'Challenge Entry Two', 'slug' => 'challenge-entry-two', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $finalist = Artwork::factory()->for($owner)->create([ 'group_id' => $group->id, 'title' => 'Challenge Finalist', 'slug' => 'challenge-finalist', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $challenge = GroupChallenge::query()->create([ 'group_id' => $group->id, 'title' => 'World Challenge Finals', 'slug' => 'world-challenge-finals-' . Str::lower(Str::random(6)), 'summary' => 'Challenge summary', 'description' => 'Challenge description', 'visibility' => GroupChallenge::VISIBILITY_PUBLIC, 'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC, 'status' => GroupChallenge::STATUS_ACTIVE, 'start_at' => now()->subDay(), 'end_at' => now()->addDay(), 'created_by_user_id' => $owner->id, 'featured_artwork_id' => null, ]); $challenge->artworks()->attach($winner->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 0]); $challenge->artworks()->attach($finalist->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 1]); $challenge->artworks()->attach($entry->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 2]); $challenge->outcomes()->create([ 'artwork_id' => $winner->id, 'user_id' => $owner->id, 'outcome_type' => 'winner', 'position' => 1, 'sort_order' => 0, 'title_override' => 'Grand Winner', 'awarded_by_user_id' => $owner->id, 'awarded_at' => now(), ]); $challenge->outcomes()->create([ 'artwork_id' => $finalist->id, 'user_id' => $owner->id, 'outcome_type' => 'finalist', 'sort_order' => 1, 'note' => 'Outstanding finalist selection.', 'awarded_by_user_id' => $owner->id, 'awarded_at' => now(), ]); $world->update([ 'linked_challenge_id' => $challenge->id, ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('linkedChallenge.title', 'World Challenge Finals') ->where('linkedChallenge.summary', 'World-specific framing for the linked challenge.') ->where('linkedChallenge.state_label', 'Winners announced') ->where('linkedChallengeEntries.items.0.title', 'Challenge Champion') ->where('linkedChallengeWinners.item.title', 'Challenge Champion') ->where('linkedChallengeFinalists.items.0.title', 'Challenge Finalist') ->where('world.challenge_cta_label', 'See results')); $this->get(route('groups.challenges.show', ['group' => $group, 'challenge' => $challenge])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Group/GroupChallengeShow') ->where('linkedWorld.title', $world->title) ->where('linkedWorld.public_url', route('worlds.show', ['world' => $world->slug])) ->where('linkedWorld.campaign_label', 'Seasonal spotlight') ->where('linkedWorld.challenge_cta_label', 'See results')); }); it('hides selected linked challenge entries from the derived world feed', function (): void { $owner = User::factory()->create([ 'username' => 'challenge-hide-owner-' . Str::lower(Str::random(6)), ]); $group = Group::factory()->for($owner, 'owner')->create(); $world = publicWorld(); $hiddenEntry = Artwork::factory()->for($owner)->create([ 'group_id' => $group->id, 'title' => 'Hidden Challenge Entry', 'slug' => 'hidden-challenge-entry', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $visibleEntry = Artwork::factory()->for($owner)->create([ 'group_id' => $group->id, 'title' => 'Visible Challenge Entry', 'slug' => 'visible-challenge-entry', 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $challenge = GroupChallenge::query()->create([ 'group_id' => $group->id, 'title' => 'Hidden Entries Challenge', 'slug' => 'hidden-entries-challenge-' . Str::lower(Str::random(6)), 'summary' => 'Challenge summary', 'description' => 'Challenge description', 'visibility' => GroupChallenge::VISIBILITY_PUBLIC, 'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC, 'status' => GroupChallenge::STATUS_ACTIVE, 'start_at' => now()->subDay(), 'end_at' => now()->addDay(), 'created_by_user_id' => $owner->id, ]); $challenge->artworks()->attach($hiddenEntry->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 0]); $challenge->artworks()->attach($visibleEntry->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 1]); $world->update([ 'linked_challenge_id' => $challenge->id, 'hidden_linked_challenge_artwork_ids_json' => [$hiddenEntry->id], ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('linkedChallengeEntries.hidden_count', 1) ->where('linkedChallengeEntries.items.0.title', 'Visible Challenge Entry') ->has('linkedChallengeEntries.items', 1)); }); it('maps community-vote challenges to a voting state on the world page', function (): void { $owner = User::factory()->create([ 'username' => 'challenge-vote-owner-' . Str::lower(Str::random(6)), ]); $group = Group::factory()->for($owner, 'owner')->create(); $world = publicWorld(); $challenge = GroupChallenge::query()->create([ 'group_id' => $group->id, 'title' => 'Vote Live Challenge', 'slug' => 'vote-live-challenge-' . Str::lower(Str::random(6)), 'summary' => 'Vote for the best entry.', 'description' => 'Challenge description', 'visibility' => GroupChallenge::VISIBILITY_PUBLIC, 'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC, 'status' => GroupChallenge::STATUS_ENDED, 'judging_mode' => 'community_vote', 'start_at' => now()->subDays(7), 'end_at' => now()->subHour(), 'created_by_user_id' => $owner->id, ]); $world->update([ 'linked_challenge_id' => $challenge->id, ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('linkedChallenge.state', 'voting') ->where('linkedChallenge.state_label', 'Voting live') ->where('linkedChallenge.cta_label', 'View entries') ->where('world.challenge_cta_label', 'View entries')); }); it('shifts linked challenge CTAs into recap mode for archived worlds', function (): void { $owner = User::factory()->create([ 'username' => 'challenge-archive-owner-' . Str::lower(Str::random(6)), ]); $group = Group::factory()->for($owner, 'owner')->create(); $world = publicWorld([ 'status' => World::STATUS_ARCHIVED, 'is_active_campaign' => false, 'is_homepage_featured' => false, 'starts_at' => now()->subDays(20), 'ends_at' => now()->subDays(3), ]); $challenge = GroupChallenge::query()->create([ 'group_id' => $group->id, 'title' => 'Archive Recap Challenge', 'slug' => 'archive-recap-challenge-' . Str::lower(Str::random(6)), 'summary' => 'Challenge summary', 'description' => 'Challenge description', 'visibility' => GroupChallenge::VISIBILITY_PUBLIC, 'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC, 'status' => GroupChallenge::STATUS_ACTIVE, 'start_at' => now()->subDays(5), 'end_at' => now()->addDays(2), 'created_by_user_id' => $owner->id, ]); $world->update([ 'linked_challenge_id' => $challenge->id, ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('linkedChallenge.state', 'closed') ->where('linkedChallenge.cta_label', 'View challenge recap') ->where('world.challenge_cta_label', 'View challenge recap')); }); it('surfaces linked challenge recap stories and keeps derived sections in recap mode for archived worlds', function (): void { $owner = User::factory()->create([ 'username' => 'challenge-recap-owner-' . Str::lower(Str::random(6)), ]); $group = Group::factory()->for($owner, 'owner')->create(); $world = publicWorld([ 'status' => World::STATUS_ARCHIVED, 'is_active_campaign' => false, 'is_homepage_featured' => false, 'starts_at' => now()->subDays(30), 'ends_at' => now()->subDays(5), 'show_linked_challenge_section' => true, 'show_linked_challenge_entries' => true, 'show_linked_challenge_winners' => true, 'show_linked_challenge_finalists' => true, ]); $winner = Artwork::factory()->for($owner)->create([ 'group_id' => $group->id, 'title' => 'Recap Winner', 'slug' => 'recap-winner-' . Str::lower(Str::random(6)), 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $finalist = Artwork::factory()->for($owner)->create([ 'group_id' => $group->id, 'title' => 'Recap Finalist', 'slug' => 'recap-finalist-' . Str::lower(Str::random(6)), 'artwork_status' => 'published', 'published_at' => now()->subDay(), 'is_public' => true, 'visibility' => Artwork::VISIBILITY_PUBLIC, ]); $challenge = GroupChallenge::query()->create([ 'group_id' => $group->id, 'title' => 'World Recap Challenge', 'slug' => 'world-recap-challenge-' . Str::lower(Str::random(6)), 'summary' => 'Challenge summary', 'description' => 'Challenge description', 'visibility' => GroupChallenge::VISIBILITY_PUBLIC, 'participation_scope' => GroupChallenge::PARTICIPATION_PUBLIC, 'status' => GroupChallenge::STATUS_ACTIVE, 'start_at' => now()->subDays(10), 'end_at' => now()->subDays(2), 'created_by_user_id' => $owner->id, 'featured_artwork_id' => null, ]); $challenge->artworks()->attach($winner->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 0]); $challenge->artworks()->attach($finalist->id, ['submitted_by_user_id' => $owner->id, 'sort_order' => 1]); $challenge->outcomes()->create([ 'artwork_id' => $winner->id, 'user_id' => $owner->id, 'outcome_type' => 'winner', 'position' => 1, 'sort_order' => 0, 'awarded_by_user_id' => $owner->id, 'awarded_at' => now(), ]); $challenge->outcomes()->create([ 'artwork_id' => $finalist->id, 'user_id' => $owner->id, 'outcome_type' => 'finalist', 'sort_order' => 1, 'awarded_by_user_id' => $owner->id, 'awarded_at' => now(), ]); $category = worldNewsCategory([ 'name' => 'Challenge Recaps', 'slug' => 'challenge-recaps-' . Str::lower(Str::random(6)), ]); $recap = publishedWorldNews($owner, $category, [ 'title' => 'World Recap Challenge results recap', 'slug' => 'world-recap-challenge-results-' . Str::lower(Str::random(6)), 'excerpt' => 'Winner highlights and finalist recap from the linked challenge.', 'content' => "# Results recap\n\nWinner highlights and finalist recap.", ]); $world->update([ 'linked_challenge_id' => $challenge->id, ]); $world->worldRelations()->create([ 'section_key' => 'news', 'related_type' => 'news', 'related_id' => $recap->id, 'context_label' => 'Challenge recap', 'sort_order' => 0, 'is_featured' => true, ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('linkedChallenge.state', 'closed') ->where('linkedChallenge.cta_label', 'View challenge recap') ->where('linkedChallenge.cta_url', route('news.show', ['slug' => $recap->slug])) ->where('linkedChallenge.challenge_url', route('groups.challenges.show', ['group' => $group, 'challenge' => $challenge])) ->where('linkedChallenge.story.title', 'World Recap Challenge results recap') ->where('linkedChallenge.story.intent', 'recap') ->where('linkedChallengeEntries.description', 'Entries from the linked challenge remain visible here so the world recap preserves the full field of work.') ->where('linkedChallengeWinners.description', 'This world is carrying the linked challenge result forward so the campaign recap stays visible here too.') ->where('linkedChallengeFinalists.description', 'Finalists from the linked challenge remain visible here so the archived world keeps the complete recap in view.') ->where('world.challenge_cta_label', 'View challenge recap') ->where('world.challenge_cta_url', route('news.show', ['slug' => $recap->slug]))); $this->get(route('groups.challenges.show', ['group' => $group, 'challenge' => $challenge])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Group/GroupChallengeShow') ->where('linkedWorld.title', $world->title) ->where('linkedWorld.challenge_cta_label', 'View challenge recap') ->where('linkedWorld.challenge_cta_url', route('news.show', ['slug' => $recap->slug]))); }); it('falls back to the theme icon when the stored world icon is blank whitespace', function (): void { $world = publicWorld([ 'title' => 'Spring Vibes', 'slug' => 'spring-vibes', 'theme_key' => 'summer', 'icon_name' => ' ', ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('world.title', 'Spring Vibes') ->where('world.icon_name', 'fa-solid fa-sun') ->where('world.theme.icon_name', 'fa-solid fa-sun')); }); it('omits disabled sections from the public world payload', function (): void { $world = publicWorld([ 'title' => 'Curated Autumn 2026', 'slug' => 'curated-autumn-2026', 'section_visibility_json' => [ 'featured_creators' => false, ], ]); $world->worldRelations()->create([ 'section_key' => 'featured_creators', 'related_type' => 'user', 'related_id' => $world->created_by_user_id, 'context_label' => 'Editorial spotlight', 'sort_order' => 0, 'is_featured' => true, ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('world.title', 'Curated Autumn 2026') ->where('sections', [])); }); it('keeps archived worlds publicly visible', function (): void { $world = publicWorld([ 'title' => 'Halloween World 2025', 'slug' => 'halloween-world-2025', 'theme_key' => 'halloween', 'status' => World::STATUS_ARCHIVED, 'starts_at' => Carbon::parse('2025-10-01 00:00:00'), 'ends_at' => Carbon::parse('2025-11-01 00:00:00'), 'published_at' => Carbon::parse('2025-09-20 10:00:00'), ]); $this->get(route('worlds.show', ['world' => $world->slug])) ->assertOk() ->assertSee('Halloween World 2025'); }); it('resolves recurring family and archived edition routes to the correct edition', function (): void { publicWorld([ 'title' => 'Spring Vibes 2025', 'slug' => 'spring-vibes-2025', 'is_recurring' => true, 'recurrence_key' => 'spring-vibes', 'edition_year' => 2025, 'status' => World::STATUS_ARCHIVED, 'is_active_campaign' => false, 'is_homepage_featured' => false, 'starts_at' => Carbon::parse('2025-03-01 00:00:00'), 'ends_at' => Carbon::parse('2025-04-01 00:00:00'), 'published_at' => Carbon::parse('2025-02-20 10:00:00'), ]); $currentEdition = publicWorld([ 'title' => 'Spring Vibes 2026', 'slug' => 'spring-vibes-2026', 'is_recurring' => true, 'recurrence_key' => 'spring-vibes', 'edition_year' => 2026, 'campaign_priority' => 600, ]); $this->get(route('worlds.show', ['world' => 'spring-vibes'])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('world.title', 'Spring Vibes 2026') ->where('world.public_url', route('worlds.show', ['world' => 'spring-vibes'])) ->has('archiveEditions', 1) ->where('archiveEditions.0.title', 'Spring Vibes 2025')); $this->get(route('worlds.editions.show', ['world' => 'spring-vibes', 'year' => 2025])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('world.title', 'Spring Vibes 2025') ->where('currentEdition.title', 'Spring Vibes 2026') ->where('archiveNotice.current_edition.title', 'Spring Vibes 2026')); $this->get(route('worlds.show', ['world' => $currentEdition->slug])) ->assertRedirect(route('worlds.show', ['world' => 'spring-vibes'])); $this->get(route('worlds.show', ['world' => 'spring-vibes-2025'])) ->assertRedirect(route('worlds.editions.show', ['world' => 'spring-vibes', 'year' => 2025])); }); it('exposes adjacent previous and next editions inside the archive payload', function (): void { publicWorld([ 'title' => 'Retro Month 2024', 'slug' => 'retro-month-2024', 'is_recurring' => true, 'recurrence_key' => 'retro-month', 'edition_year' => 2024, 'status' => World::STATUS_ARCHIVED, 'is_active_campaign' => false, 'is_homepage_featured' => false, 'starts_at' => Carbon::parse('2024-04-01 00:00:00'), 'ends_at' => Carbon::parse('2024-04-30 00:00:00'), 'published_at' => Carbon::parse('2024-03-20 10:00:00'), ]); publicWorld([ 'title' => 'Retro Month 2025', 'slug' => 'retro-month-2025', 'is_recurring' => true, 'recurrence_key' => 'retro-month', 'edition_year' => 2025, 'status' => World::STATUS_ARCHIVED, 'is_active_campaign' => false, 'is_homepage_featured' => false, 'starts_at' => Carbon::parse('2025-04-01 00:00:00'), 'ends_at' => Carbon::parse('2025-04-30 00:00:00'), 'published_at' => Carbon::parse('2025-03-20 10:00:00'), ]); publicWorld([ 'title' => 'Retro Month 2026', 'slug' => 'retro-month-2026', 'is_recurring' => true, 'recurrence_key' => 'retro-month', 'edition_year' => 2026, 'campaign_priority' => 200, ]); $this->get(route('worlds.editions.show', ['world' => 'retro-month', 'year' => 2025])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('world.title', 'Retro Month 2025') ->where('previousEdition.title', 'Retro Month 2024') ->where('nextEdition.title', 'Retro Month 2026')); }); it('exposes a homepage world spotlight when a featured world exists', function (): void { publicWorld([ 'title' => 'Pixel Week 2026', 'slug' => 'pixel-week-2026', 'theme_key' => 'pixel-week', 'teaser_title' => 'Pixel Week is open for submissions', ]); app(HomepageService::class)->clearGuestPayloadCache(); $this->get(route('index')) ->assertOk() ->assertSee(route('worlds.index'), false) ->assertSee('pixel-week-2026') ->assertSee('Pixel Week 2026'); }); it('splits live, upcoming, and archived worlds on the public index', function (): void { publicWorld([ 'title' => 'Spring Vibes', 'slug' => 'spring-vibes', 'is_recurring' => true, 'recurrence_key' => 'spring-vibes', 'edition_year' => 2026, 'campaign_priority' => 500, ]); publicWorld([ 'title' => 'Spring Vibes 2025', 'slug' => 'spring-vibes-2025', 'is_recurring' => true, 'recurrence_key' => 'spring-vibes', 'edition_year' => 2025, 'status' => World::STATUS_ARCHIVED, 'is_active_campaign' => false, 'is_homepage_featured' => false, 'starts_at' => Carbon::now()->subDays(400), 'ends_at' => Carbon::now()->subDays(365), 'promotion_starts_at' => null, 'promotion_ends_at' => null, 'published_at' => Carbon::now()->subDays(420), ]); publicWorld([ 'title' => 'Retro Month 2026', 'slug' => 'retro-month-2026', 'starts_at' => Carbon::now()->addDays(10), 'ends_at' => Carbon::now()->addDays(24), 'promotion_starts_at' => Carbon::now()->addDays(8), 'promotion_ends_at' => Carbon::now()->addDays(18), 'teaser_title' => 'Retro Month is coming up', ]); publicWorld([ 'title' => 'Halloween World 2025', 'slug' => 'halloween-world-2025', 'theme_key' => 'halloween', 'status' => World::STATUS_ARCHIVED, 'is_active_campaign' => false, 'is_homepage_featured' => false, 'starts_at' => Carbon::now()->subDays(40), 'ends_at' => Carbon::now()->subDays(20), 'promotion_starts_at' => null, 'promotion_ends_at' => null, 'published_at' => Carbon::now()->subDays(50), ]); $this->get(route('worlds.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldIndex') ->where('spotlightWorld.title', 'Spring Vibes') ->has('upcomingWorlds', 1) ->has('recurringWorldFamilies', 1) ->where('recurringWorldFamilies.0.title', 'Spring Vibes') ->has('archivedWorlds', 2)); });