create([ 'username' => 'worldbuilder', 'name' => 'World Builder', ]); unset($attributes['creator']); return World::query()->create(array_merge([ 'title' => 'Halloween World 2026', 'slug' => 'halloween-world-2026', 'tagline' => 'Night drives, haunted pixels, and autumn launches.', 'summary' => 'A curated seasonal destination for Halloween programming.', 'description' => 'World description', 'theme_key' => 'halloween', 'status' => World::STATUS_DRAFT, 'type' => World::TYPE_SEASONAL, 'is_featured' => true, 'created_by_user_id' => $creator->id, ], $attributes)); } it('forbids world studio pages for non moderators', function (): void { $user = User::factory()->create(); $this->actingAs($user) ->get(route('studio.worlds.index')) ->assertForbidden(); $this->actingAs($user) ->get(route('studio.worlds.create')) ->assertRedirect(route('worlds.index')); $this->actingAs($user) ->get('/worlds/create') ->assertRedirect(route('worlds.index')); }); it('sends guests from the public worlds create shortcut to login', function (): void { $this->get('/worlds/create') ->assertRedirect(route('login')); }); it('renders world studio pages for moderators', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'modworlds', 'name' => 'Moderator Worlds', ]); $world = studioWorld([ 'creator' => $moderator, 'status' => World::STATUS_PUBLISHED, 'published_at' => Carbon::parse('2026-10-01 10:00:00'), 'starts_at' => Carbon::parse('2026-10-15 00:00:00'), ]); $this->actingAs($moderator) ->get(route('studio.worlds.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioWorldsIndex') ->where('title', 'Worlds') ->where('listing.items.0.title', 'Halloween World 2026') ->where('createUrl', route('studio.worlds.create'))); $this->actingAs($moderator) ->get('/worlds/create') ->assertRedirect(route('studio.worlds.create')); $this->actingAs($moderator) ->get(route('studio.worlds.create')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioWorldEditor') ->where('title', 'Create world') ->has('themeOptions') ->has('sectionOptions') ->has('relationTypeOptions') ->where('mediaSupport.picker_available', false)); $this->actingAs($moderator) ->get(route('studio.worlds.edit', ['world' => $world->id])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioWorldEditor') ->where('world.title', 'Halloween World 2026') ->where('world.slug', 'halloween-world-2026') ->where('world.section_visibility_json.featured_artworks', true) ->where('duplicateActions.canCreateEdition', false)); $this->actingAs($moderator) ->get(route('studio.worlds.preview', ['world' => $world->id])) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('World/WorldShow') ->where('previewMode', true) ->where('world.title', 'Halloween World 2026')); }); it('renders world studio pages for legacy admin accounts', function (): void { $admin = User::factory()->create([ 'role' => 'user', 'username' => 'legacyadminworlds', 'name' => 'Legacy Admin Worlds', ]); DB::table('users') ->where('id', $admin->id) ->update(['isAdmin' => 1]); $admin->refresh(); $this->actingAs($admin) ->get(route('studio.worlds.index')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioWorldsIndex') ->where('title', 'Worlds') ->where('createUrl', route('studio.worlds.create'))); $this->actingAs($admin) ->get(route('studio.worlds.create')) ->assertOk() ->assertInertia(fn (AssertableInertia $page) => $page ->component('Studio/StudioWorldEditor') ->where('title', 'Create world')); }); it('searches artwork relations by creator and project context in the worlds picker', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'searchworldsmod', 'name' => 'Search Worlds Moderator', ]); $creator = User::factory()->create([ 'username' => 'springartist', 'name' => 'Spring Artist', ]); $group = Group::factory()->create([ 'name' => 'Spring Project', 'slug' => 'spring-project', ]); $contentType = ContentType::query()->create([ 'name' => 'Pixel Art', 'slug' => 'pixel-art', 'description' => 'Pixel art content type', ]); $category = Category::query()->create([ 'content_type_id' => $contentType->id, 'name' => 'Seasonal Spring', 'slug' => 'seasonal-spring', 'description' => 'Spring showcase', 'is_active' => true, 'sort_order' => 1, ]); $artwork = Artwork::factory()->for($creator)->create([ 'group_id' => $group->id, 'title' => 'Morning Dew', 'slug' => 'morning-dew', 'description' => 'A calm scene with no direct spring keyword in the artwork copy.', 'artwork_status' => 'published', 'visibility' => Artwork::VISIBILITY_PUBLIC, 'is_public' => true, ]); $artwork->categories()->attach($category->id); $this->actingAs($moderator) ->getJson(route('studio.worlds.entity-search', ['type' => 'artwork', 'q' => 'spring'])) ->assertOk() ->assertJsonPath('items.0.id', $artwork->id) ->assertJsonPath('items.0.title', 'Morning Dew') ->assertJsonPath('items.0.subtitle', 'Spring Artist'); }); it('stores a world draft through the studio flow', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'editorworlds', 'name' => 'Editor Worlds', ]); $response = $this->actingAs($moderator)->post(route('studio.worlds.store'), [ 'title' => 'Retro Month 2026', 'slug' => 'retro-month-2026', 'tagline' => 'Scanlines, diskmag culture, and old-school launches.', 'summary' => 'A recurring world for retro platform activity.', 'description' => 'World body copy', 'theme_key' => 'retro-month', 'status' => World::STATUS_DRAFT, 'type' => World::TYPE_CAMPAIGN, 'is_featured' => true, 'is_recurring' => true, 'recurrence_key' => 'retro-month', 'recurrence_rule' => 'annual:04', 'edition_year' => 2026, 'cta_label' => 'Explore Retro Month', 'cta_url' => 'https://skinbase.test/worlds/retro-month-2026', 'badge_label' => 'Editorial pick', 'badge_description' => 'Featured by the Nova editorial team.', 'badge_url' => 'https://skinbase.test/badges/retro', 'seo_title' => 'Retro Month 2026 - Skinbase Nova', 'seo_description' => 'Retro Month seasonal campaign', 'published_at' => '2026-03-20T10:00', 'related_tags_json' => ['retro', 'demoscene'], 'section_order_json' => ['featured_artworks', 'featured_collections', 'news'], 'section_visibility_json' => [ 'featured_artworks' => true, 'featured_collections' => true, 'featured_creators' => false, 'featured_groups' => false, 'news' => true, 'challenge' => false, 'events' => false, 'releases' => false, 'cards' => false, ], 'relations' => [], ]); $world = World::query()->where('slug', 'retro-month-2026')->firstOrFail(); $response->assertRedirect(route('studio.worlds.edit', ['world' => $world->id])); $this->assertDatabaseHas('worlds', [ 'id' => $world->id, 'title' => 'Retro Month 2026', 'slug' => 'retro-month-2026', 'status' => World::STATUS_DRAFT, 'type' => World::TYPE_CAMPAIGN, 'is_featured' => true, 'is_recurring' => true, 'recurrence_key' => 'retro-month', 'edition_year' => 2026, 'published_at' => '2026-03-20 10:00:00', 'created_by_user_id' => $moderator->id, ]); expect($world->fresh()->section_visibility_json)->toMatchArray([ 'featured_artworks' => true, 'featured_collections' => true, 'featured_creators' => false, 'news' => true, ]); }); it('rejects reserved world slugs in the studio flow', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'reservedslugmod', 'name' => 'Reserved Slug Moderator', ]); $this->actingAs($moderator) ->from(route('studio.worlds.create')) ->post(route('studio.worlds.store'), [ 'title' => 'Create', 'slug' => 'create', 'summary' => 'Reserved slug attempt', 'description' => 'Should fail validation', 'theme_key' => 'retro-month', 'status' => World::STATUS_DRAFT, 'type' => World::TYPE_CAMPAIGN, 'relations' => [], ]) ->assertRedirect(route('studio.worlds.create')) ->assertSessionHasErrors(['slug']); }); it('requires recurrence metadata and blocks duplicate recurrence editions', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'recurrencemod', 'name' => 'Recurrence Moderator', ]); studioWorld([ 'creator' => $moderator, 'title' => 'Halloween World 2026', 'slug' => 'halloween-world-2026-existing', 'is_recurring' => true, 'recurrence_key' => 'halloween', 'edition_year' => 2026, ]); $this->actingAs($moderator) ->from(route('studio.worlds.create')) ->post(route('studio.worlds.store'), [ 'title' => 'Recurring World Without Metadata', 'status' => World::STATUS_DRAFT, 'type' => World::TYPE_CAMPAIGN, 'is_recurring' => true, 'relations' => [], ]) ->assertRedirect(route('studio.worlds.create')) ->assertSessionHasErrors(['recurrence_key', 'edition_year']); $this->actingAs($moderator) ->from(route('studio.worlds.create')) ->post(route('studio.worlds.store'), [ 'title' => 'Halloween World Clone', 'slug' => 'halloween-world-clone', 'status' => World::STATUS_DRAFT, 'type' => World::TYPE_SEASONAL, 'is_recurring' => true, 'recurrence_key' => 'halloween', 'edition_year' => 2026, 'relations' => [], ]) ->assertRedirect(route('studio.worlds.create')) ->assertSessionHasErrors(['edition_year']); }); it('duplicates worlds and preserves editorial structure in a new draft', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'duplicateworldmod', 'name' => 'Duplicate World Moderator', ]); $world = studioWorld([ 'creator' => $moderator, 'title' => 'Pixel Week 2026', 'slug' => 'pixel-week-2026', 'theme_key' => 'pixel-week', 'section_visibility_json' => [ 'featured_artworks' => true, 'featured_collections' => false, 'events' => true, ], 'starts_at' => Carbon::parse('2026-07-01 09:00:00'), 'published_at' => Carbon::parse('2026-06-28 18:00:00'), ]); $world->worldRelations()->create([ 'section_key' => 'featured_creators', 'related_type' => 'user', 'related_id' => $moderator->id, 'context_label' => 'Lead pixel artist', 'sort_order' => 0, 'is_featured' => true, ]); $response = $this->actingAs($moderator)->post(route('studio.worlds.duplicate', ['world' => $world->id])); $duplicate = World::query()->where('slug', 'like', 'pixel-week-2026-copy%')->latest('id')->firstOrFail(); $response->assertRedirect(route('studio.worlds.edit', ['world' => $duplicate->id])); expect($duplicate->status)->toBe(World::STATUS_DRAFT); expect($duplicate->is_featured)->toBeFalse(); expect($duplicate->starts_at)->toBeNull(); expect($duplicate->published_at)->toBeNull(); expect($duplicate->section_visibility_json)->toMatchArray([ 'featured_artworks' => true, 'featured_collections' => false, 'events' => true, ]); expect($duplicate->worldRelations()->count())->toBe(1); }); it('creates the next edition draft for recurring worlds', function (): void { $moderator = User::factory()->create([ 'role' => 'moderator', 'username' => 'editionworldmod', 'name' => 'Edition World Moderator', ]); $world = studioWorld([ 'creator' => $moderator, 'title' => 'Halloween 2026', 'slug' => 'halloween-2026', 'is_recurring' => true, 'recurrence_key' => 'halloween', 'edition_year' => 2026, ]); $response = $this->actingAs($moderator)->post(route('studio.worlds.new-edition', ['world' => $world->id])); $edition = World::query()->where('recurrence_key', 'halloween')->where('edition_year', 2027)->latest('id')->firstOrFail(); $response->assertRedirect(route('studio.worlds.edit', ['world' => $edition->id])); expect($edition->parent_world_id)->toBe($world->id); expect($edition->status)->toBe(World::STATUS_DRAFT); expect($edition->is_recurring)->toBeTrue(); expect($edition->slug)->toContain('2027'); });