Save workspace changes

This commit is contained in:
2026-04-18 17:02:56 +02:00
parent f02ea9a711
commit 87d60af5a9
4220 changed files with 1388603 additions and 1554 deletions

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
use App\Models\World;
use Database\Seeders\WorldLaunchSeeder;
it('seeds launch worlds with a featured current world and archived recurrence', function (): void {
$this->seed(WorldLaunchSeeder::class);
$featuredCurrent = World::query()
->where('slug', 'like', 'retro-month-%')
->where('is_featured', true)
->current()
->first();
expect($featuredCurrent)->not->toBeNull();
expect($featuredCurrent?->worldRelations()->count())->toBeGreaterThan(0);
$archivedEdition = World::query()
->where('parent_world_id', $featuredCurrent?->id)
->where('status', World::STATUS_ARCHIVED)
->first();
expect($archivedEdition)->not->toBeNull();
expect(World::query()->count())->toBeGreaterThanOrEqual(6);
});

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
use App\Models\User;
use App\Models\World;
use App\Services\HomepageService;
use Illuminate\Support\Carbon;
use Inertia\Testing\AssertableInertia;
function publicWorld(array $attributes = []): World
{
$creator = $attributes['creator'] ?? User::factory()->create([
'username' => 'publicworlds',
'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.',
'description' => 'Public world description',
'theme_key' => 'summer',
'status' => World::STATUS_PUBLISHED,
'type' => World::TYPE_SEASONAL,
'is_featured' => true,
'starts_at' => Carbon::parse('2026-06-01 00:00:00'),
'ends_at' => Carbon::parse('2026-08-31 23:59:59'),
'published_at' => Carbon::parse('2026-04-01 10:00:00'),
'created_by_user_id' => $creator->id,
], $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('featuredWorld.title', 'Summer Slam 2026')
->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('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('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',
]);
app(HomepageService::class)->clearGuestPayloadCache();
$this->get(route('index'))
->assertOk()
->assertSee(route('worlds.index'), false)
->assertSee('pixel-week-2026')
->assertSee('Pixel Week 2026');
});

View File

@@ -0,0 +1,481 @@
<?php
declare(strict_types=1);
use App\Models\Artwork;
use App\Models\User;
use App\Models\World;
use App\Models\WorldRelation;
use App\Models\WorldSubmission;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
uses(RefreshDatabase::class);
function worldSubmissionCategoryId(): int
{
$contentTypeId = DB::table('content_types')->insertGetId([
'name' => 'World Submission Type',
'slug' => 'world-submission-type-' . Str::lower(Str::random(6)),
'description' => null,
'created_at' => now(),
'updated_at' => now(),
]);
return DB::table('categories')->insertGetId([
'content_type_id' => $contentTypeId,
'parent_id' => null,
'name' => 'World Submission Category',
'slug' => 'world-submission-category-' . Str::lower(Str::random(6)),
'description' => null,
'image' => null,
'is_active' => true,
'sort_order' => 0,
'created_at' => now(),
'updated_at' => now(),
]);
}
function acceptingWorld(?User $creator = null, array $attributes = []): World
{
$creator ??= User::factory()->create([
'role' => 'moderator',
'username' => 'worldmoderator-' . Str::lower(Str::random(6)),
'name' => 'World Moderator',
]);
return World::factory()->create(array_merge([
'created_by_user_id' => $creator->id,
'status' => World::STATUS_PUBLISHED,
'published_at' => now()->subDay(),
'accepts_submissions' => true,
'participation_mode' => World::PARTICIPATION_MODE_MANUAL_APPROVAL,
'submission_note_enabled' => true,
'community_section_enabled' => true,
'allow_readd_after_removal' => true,
'submission_starts_at' => now()->subDay(),
'submission_ends_at' => now()->addDays(7),
], $attributes));
}
it('creates pending world submissions when publishing an artwork draft', function (): void {
$creator = User::factory()->create();
$world = acceptingWorld();
$categoryId = worldSubmissionCategoryId();
$artwork = Artwork::factory()->for($creator)->create([
'title' => 'Draft Upload',
'slug' => 'draft-upload',
'is_public' => false,
'visibility' => Artwork::VISIBILITY_PRIVATE,
'is_approved' => false,
'published_at' => null,
'artwork_status' => 'draft',
]);
$this->actingAs($creator)
->postJson("/api/uploads/{$artwork->id}/publish", [
'title' => 'World Upload',
'category' => $categoryId,
'tags' => ['world', 'submission'],
'world_submissions' => [
['world_id' => $world->id, 'note' => 'Fits the active theme.'],
],
])
->assertOk()
->assertJsonPath('status', 'published');
$this->assertDatabaseHas('world_submissions', [
'world_id' => $world->id,
'artwork_id' => $artwork->id,
'submitted_by_user_id' => $creator->id,
'status' => WorldSubmission::STATUS_PENDING,
'is_featured' => false,
'note' => 'Fits the active theme.',
]);
});
it('creates live world participation immediately for auto-add worlds', function (): void {
$creator = User::factory()->create();
$world = acceptingWorld(attributes: [
'participation_mode' => World::PARTICIPATION_MODE_AUTO_ADD,
]);
$categoryId = worldSubmissionCategoryId();
$artwork = Artwork::factory()->for($creator)->create([
'title' => 'Auto Add Upload',
'slug' => 'auto-add-upload',
'is_public' => false,
'visibility' => Artwork::VISIBILITY_PRIVATE,
'is_approved' => false,
'published_at' => null,
'artwork_status' => 'draft',
]);
$this->actingAs($creator)
->postJson("/api/uploads/{$artwork->id}/publish", [
'title' => 'Auto Add Upload',
'category' => $categoryId,
'tags' => ['world', 'auto-add'],
'world_submissions' => [
['world_id' => $world->id, 'note' => 'Ship it.'],
],
])
->assertOk();
$this->assertDatabaseHas('world_submissions', [
'world_id' => $world->id,
'artwork_id' => $artwork->id,
'submitted_by_user_id' => $creator->id,
'status' => WorldSubmission::STATUS_LIVE,
'is_featured' => false,
]);
});
it('syncs world submissions from the studio artwork editor update flow', function (): void {
$creator = User::factory()->create();
$world = acceptingWorld();
$artwork = Artwork::factory()->for($creator)->create([
'title' => 'Studio Draft',
'slug' => 'studio-draft',
'is_public' => false,
'visibility' => Artwork::VISIBILITY_PRIVATE,
'published_at' => null,
'artwork_status' => 'draft',
]);
$this->actingAs($creator)
->putJson(route('api.studio.artworks.update', ['id' => $artwork->id]), [
'world_submissions' => [
['world_id' => $world->id, 'note' => 'Added after upload.'],
],
])
->assertOk()
->assertJsonPath('world_submission_options.0.id', $world->id)
->assertJsonPath('world_submission_options.0.selected', true)
->assertJsonPath('world_submission_options.0.status', WorldSubmission::STATUS_PENDING);
$this->assertDatabaseHas('world_submissions', [
'world_id' => $world->id,
'artwork_id' => $artwork->id,
'status' => WorldSubmission::STATUS_PENDING,
'note' => 'Added after upload.',
]);
});
it('allows removed submissions to be re-added when the world permits it', function (): void {
$creator = User::factory()->create();
$world = acceptingWorld();
$artwork = Artwork::factory()->for($creator)->create([
'title' => 'Re Add Artwork',
'slug' => 're-add-artwork',
'is_public' => true,
'visibility' => Artwork::VISIBILITY_PUBLIC,
'published_at' => now()->subDay(),
'artwork_status' => 'published',
]);
WorldSubmission::query()->create([
'world_id' => $world->id,
'artwork_id' => $artwork->id,
'submitted_by_user_id' => $creator->id,
'status' => WorldSubmission::STATUS_REMOVED,
'moderation_reason' => 'Needs tighter fit.',
'removed_at' => now()->subHour(),
'reviewed_at' => now()->subHour(),
]);
$this->actingAs($creator)
->putJson(route('api.studio.artworks.update', ['id' => $artwork->id]), [
'world_submissions' => [
['world_id' => $world->id, 'note' => 'Updated to fit the brief.'],
],
])
->assertOk()
->assertJsonPath('world_submission_options.0.can_resubmit', false)
->assertJsonPath('world_submission_options.0.status', WorldSubmission::STATUS_PENDING);
$this->assertDatabaseHas('world_submissions', [
'world_id' => $world->id,
'artwork_id' => $artwork->id,
'status' => WorldSubmission::STATUS_PENDING,
'note' => 'Updated to fit the brief.',
'moderation_reason' => null,
]);
});
it('keeps existing live submissions live when the artwork is updated in a manual approval world', function (): void {
$creator = User::factory()->create();
$moderator = User::factory()->create([
'role' => 'moderator',
'username' => 'livereviewmod-' . Str::lower(Str::random(6)),
'name' => 'Live Review Moderator',
]);
$world = acceptingWorld($moderator);
$artwork = Artwork::factory()->for($creator)->create([
'title' => 'Live World Artwork',
'slug' => 'live-world-artwork',
'is_public' => true,
'visibility' => Artwork::VISIBILITY_PUBLIC,
'published_at' => now()->subDay(),
'artwork_status' => 'published',
]);
$submission = WorldSubmission::query()->create([
'world_id' => $world->id,
'artwork_id' => $artwork->id,
'submitted_by_user_id' => $creator->id,
'status' => WorldSubmission::STATUS_LIVE,
'is_featured' => true,
'note' => 'Original approved note.',
'reviewed_by_user_id' => $moderator->id,
'reviewed_at' => now()->subHour(),
'featured_at' => now()->subHour(),
]);
$this->actingAs($creator)
->putJson(route('api.studio.artworks.update', ['id' => $artwork->id]), [
'world_submissions' => [
['world_id' => $world->id, 'note' => 'Updated creator note after going live.'],
],
])
->assertOk()
->assertJsonPath('world_submission_options.0.status', WorldSubmission::STATUS_LIVE)
->assertJsonPath('world_submission_options.0.selected', true)
->assertJsonPath('world_submission_options.0.note', 'Updated creator note after going live.');
$submission->refresh();
expect($submission->status)->toBe(WorldSubmission::STATUS_LIVE)
->and($submission->note)->toBe('Updated creator note after going live.')
->and($submission->is_featured)->toBeTrue()
->and((int) $submission->reviewed_by_user_id)->toBe($moderator->id)
->and($submission->reviewed_at)->not->toBeNull()
->and($submission->removed_at)->toBeNull()
->and($submission->blocked_at)->toBeNull();
});
it('does not expose closed worlds in creator submission options', function (): void {
$creator = User::factory()->create();
$openWorld = acceptingWorld(attributes: ['title' => 'Open World']);
$closedWorld = acceptingWorld(attributes: [
'title' => 'Closed World',
'accepts_submissions' => false,
'participation_mode' => World::PARTICIPATION_MODE_CLOSED,
]);
$artwork = Artwork::factory()->for($creator)->create([
'title' => 'Selector Artwork',
'slug' => 'selector-artwork',
'is_public' => true,
'visibility' => Artwork::VISIBILITY_PUBLIC,
'published_at' => now()->subDay(),
'artwork_status' => 'published',
]);
$this->actingAs($creator)
->putJson(route('api.studio.artworks.update', ['id' => $artwork->id]), [])
->assertOk()
->assertJsonCount(1, 'world_submission_options')
->assertJsonPath('world_submission_options.0.id', $openWorld->id);
expect($closedWorld->id)->not->toBe($openWorld->id);
});
it('shows and reviews world participation in the studio world editor', function (): void {
$moderator = User::factory()->create([
'role' => 'moderator',
'username' => 'reviewmod',
'name' => 'Review Moderator',
]);
$creator = User::factory()->create([
'username' => 'queueartist',
'name' => 'Queue Artist',
]);
$world = acceptingWorld($moderator);
$artwork = Artwork::factory()->for($creator)->create([
'title' => 'Queue Artwork',
'slug' => 'queue-artwork',
'artwork_status' => 'published',
'published_at' => now()->subDay(),
'is_public' => true,
'visibility' => Artwork::VISIBILITY_PUBLIC,
]);
$submission = WorldSubmission::query()->create([
'world_id' => $world->id,
'artwork_id' => $artwork->id,
'submitted_by_user_id' => $creator->id,
'status' => WorldSubmission::STATUS_PENDING,
'note' => 'Please review this for the world.',
]);
$this->actingAs($moderator)
->get(route('studio.worlds.edit', ['world' => $world->id]))
->assertOk()
->assertInertia(fn (AssertableInertia $page) => $page
->component('Studio/StudioWorldEditor')
->where('world.participation_mode', World::PARTICIPATION_MODE_MANUAL_APPROVAL)
->where('world.submission_review_queue.counts.pending', 1)
->where('world.submission_review_queue.items.0.artwork.title', 'Queue Artwork'));
$this->actingAs($moderator)
->post(route('studio.worlds.submissions.approve', ['world' => $world->id, 'submission' => $submission->id]))
->assertRedirect();
$this->actingAs($moderator)
->post(route('studio.worlds.submissions.feature', ['world' => $world->id, 'submission' => $submission->id]))
->assertRedirect();
$this->assertDatabaseHas('world_submissions', [
'id' => $submission->id,
'status' => WorldSubmission::STATUS_LIVE,
'is_featured' => true,
'reviewed_by_user_id' => $moderator->id,
]);
$this->actingAs($moderator)
->post(route('studio.worlds.submissions.block', ['world' => $world->id, 'submission' => $submission->id]), [
'review_note' => 'Off brief for this world.',
])
->assertRedirect();
$this->assertDatabaseHas('world_submissions', [
'id' => $submission->id,
'status' => WorldSubmission::STATUS_BLOCKED,
'moderation_reason' => 'Off brief for this world.',
'is_featured' => false,
]);
});
it('renders only live community submissions on public world pages and hides pending or blocked ones', function (): void {
$world = acceptingWorld(attributes: [
'title' => 'Public World',
'slug' => 'public-world',
]);
$creator = User::factory()->create();
$featuredArtwork = Artwork::factory()->for($creator)->create([
'title' => 'Featured Community Artwork',
'slug' => 'featured-community-artwork',
'artwork_status' => 'published',
'published_at' => now()->subDay(),
'is_public' => true,
'visibility' => Artwork::VISIBILITY_PUBLIC,
]);
$approvedArtwork = Artwork::factory()->for($creator)->create([
'title' => 'Approved Community Artwork',
'slug' => 'approved-community-artwork',
'artwork_status' => 'published',
'published_at' => now()->subDay(),
'is_public' => true,
'visibility' => Artwork::VISIBILITY_PUBLIC,
]);
$pendingArtwork = Artwork::factory()->for($creator)->create([
'title' => 'Pending Community Artwork',
'slug' => 'pending-community-artwork',
'artwork_status' => 'published',
'published_at' => now()->subDay(),
'is_public' => true,
'visibility' => Artwork::VISIBILITY_PUBLIC,
]);
$matureArtwork = Artwork::factory()->for($creator)->create([
'title' => 'Mature Community Artwork',
'slug' => 'mature-community-artwork',
'artwork_status' => 'published',
'published_at' => now()->subDay(),
'is_public' => true,
'visibility' => Artwork::VISIBILITY_PUBLIC,
'is_mature' => true,
]);
foreach ([
[$featuredArtwork, WorldSubmission::STATUS_LIVE, true],
[$approvedArtwork, WorldSubmission::STATUS_LIVE, false],
[$pendingArtwork, WorldSubmission::STATUS_PENDING, false],
[$matureArtwork, WorldSubmission::STATUS_LIVE, false],
] as [$artwork, $status, $isFeatured]) {
WorldSubmission::query()->create([
'world_id' => $world->id,
'artwork_id' => $artwork->id,
'submitted_by_user_id' => $creator->id,
'status' => $status,
'is_featured' => $isFeatured,
'reviewed_at' => $status === WorldSubmission::STATUS_PENDING ? null : Carbon::now(),
'featured_at' => $isFeatured ? Carbon::now() : null,
]);
}
WorldSubmission::query()->create([
'world_id' => $world->id,
'artwork_id' => Artwork::factory()->for($creator)->create([
'title' => 'Blocked Community Artwork',
'slug' => 'blocked-community-artwork',
'artwork_status' => 'published',
'published_at' => now()->subDay(),
'is_public' => true,
'visibility' => Artwork::VISIBILITY_PUBLIC,
])->id,
'submitted_by_user_id' => $creator->id,
'status' => WorldSubmission::STATUS_BLOCKED,
'reviewed_at' => Carbon::now(),
'blocked_at' => Carbon::now(),
]);
$this->get(route('worlds.show', ['world' => $world->slug]))
->assertOk()
->assertInertia(fn (AssertableInertia $page) => $page
->component('World/WorldShow')
->where('communitySubmissions.items.0.title', 'Featured Community Artwork')
->where('communitySubmissions.items.0.status', WorldSubmission::STATUS_LIVE)
->where('communitySubmissions.items.0.status_label', 'Featured')
->has('communitySubmissions.items', 2)
->where('communitySubmissions.items.1.title', 'Approved Community Artwork'));
});
it('exposes world participation badges on the artwork page for curated and live world placements', function (): void {
$world = acceptingWorld(attributes: [
'title' => 'Retro Month',
'slug' => 'retro-month',
]);
$creator = User::factory()->create();
$artwork = Artwork::factory()->for($creator)->create([
'title' => 'Badge Artwork',
'slug' => 'badge-artwork',
'artwork_status' => 'published',
'published_at' => now()->subDay(),
'is_public' => true,
'visibility' => Artwork::VISIBILITY_PUBLIC,
]);
WorldRelation::query()->create([
'world_id' => $world->id,
'related_type' => WorldRelation::TYPE_ARTWORK,
'related_id' => $artwork->id,
'section_key' => 'featured_artworks',
'context_label' => 'Curated spotlight',
'sort_order' => 1,
'is_featured' => true,
]);
WorldSubmission::query()->create([
'world_id' => $world->id,
'artwork_id' => $artwork->id,
'submitted_by_user_id' => $creator->id,
'status' => WorldSubmission::STATUS_LIVE,
'is_featured' => true,
'reviewed_at' => Carbon::now(),
'featured_at' => Carbon::now(),
]);
$this->get(route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]))
->assertOk()
->assertViewHas('artworkData', function (array $artworkData): bool {
$items = collect($artworkData['world_participation'] ?? []);
return $items->count() === 1
&& $items->contains(fn (array $item): bool => ($item['badge_label'] ?? null) === 'Featured in Retro Month');
});
});