put($path, $contents); return Media::query()->create(['disk' => config('media.disk'), 'path' => $path]); } #[Test] public function it_records_the_dimensions_a_page_needs_to_reserve_the_box(): void { $media = $this->putImage('projects/cover.jpg', 1600, 900); app(ImageProcessor::class)->process($media); $media->refresh(); // Without these the page reflows as each image arrives. $this->assertSame(1600, $media->width); $this->assertSame(900, $media->height); $this->assertSame(1.7778, $media->aspectRatio()); $this->assertSame('image/jpeg', $media->mime); $this->assertNotNull($media->processed_at); $this->assertNull($media->error); } #[Test] public function it_derives_narrower_versions_in_a_modern_format(): void { $media = $this->putImage('projects/cover.jpg', 1600, 900); app(ImageProcessor::class)->process($media); $media->refresh(); $disk = Storage::disk(config('media.disk')); $this->assertNotEmpty($media->variants); foreach ($media->variants as $variant) { $this->assertTrue($disk->exists($variant['path']), "missing {$variant['path']}"); $this->assertLessThan(1600, $variant['width']); } // WebP is the one format every supported PHP can write; AVIF is // produced too wherever the runtime can encode it. $this->assertContains('webp', array_column($media->variants, 'format')); } #[Test] public function it_never_scales_an_image_up(): void { $media = $this->putImage('projects/small.jpg', 500, 500); app(ImageProcessor::class)->process($media); $widths = array_column($media->refresh()->variants, 'width'); // A 500px original must not be republished as a blurry "1600px" // version; only the widths below it are real. $this->assertSame([400], array_values(array_unique($widths))); } #[Test] public function it_builds_a_srcset_the_frontend_can_use_verbatim(): void { $media = $this->putImage('projects/cover.jpg', 1600, 900); app(ImageProcessor::class)->process($media); $srcset = $media->refresh()->srcset(); $this->assertArrayHasKey('webp', $srcset); $this->assertStringContainsString('400w', $srcset['webp']); $this->assertStringContainsString('800w', $srcset['webp']); // Absolute, because the frontend is a different application on a // different host. $this->assertStringStartsWith('http', $srcset['webp']); } #[Test] public function it_produces_a_placeholder_small_enough_to_inline(): void { $media = $this->putImage('projects/cover.jpg'); app(ImageProcessor::class)->process($media); $placeholder = $media->refresh()->placeholder; $this->assertStringStartsWith('data:image/', $placeholder); // It travels inside every JSON response mentioning the image, so its // size is part of the contract, not an implementation detail. $this->assertLessThan(2048, strlen($placeholder)); } #[Test] public function reprocessing_replaces_the_previous_versions_rather_than_piling_up(): void { $media = $this->putImage('projects/cover.jpg'); app(ImageProcessor::class)->process($media); $first = $media->refresh()->variants; app(ImageProcessor::class)->process($media->refresh()); $second = $media->refresh()->variants; $this->assertCount(count($first), $second); $disk = Storage::disk(config('media.disk')); foreach ($second as $variant) { $this->assertTrue($disk->exists($variant['path'])); } } #[Test] public function a_vector_is_copied_through_untouched(): void { $svg = ''; Storage::disk(config('media.disk'))->put('projects/logo.svg', $svg); $media = Media::query()->create(['disk' => config('media.disk'), 'path' => 'projects/logo.svg']); app(ImageProcessor::class)->process($media); $media->refresh(); // Resampling a vector loses the point of it, but the frontend still // needs a size to reserve the box — so the viewBox is read. $this->assertSame(240, $media->width); $this->assertSame(120, $media->height); $this->assertSame([], $media->variants); } #[Test] public function a_missing_file_is_recorded_rather_than_thrown(): void { $media = Media::query()->create([ 'disk' => config('media.disk'), 'path' => 'projects/gone.jpg', ]); app(ImageProcessor::class)->process($media); $media->refresh(); // The upload keeps working as a plain image; a broken derivation must // not take a page down with it. $this->assertNotNull($media->error); $this->assertNotNull($media->processed_at); } #[Test] public function saving_an_image_path_registers_it_and_queues_the_work(): void { Queue::fake(); $project = Project::factory()->create(['cover_image' => 'projects/new.jpg']); $media = Media::query()->where('path', 'projects/new.jpg')->sole(); $this->assertSame(config('media.disk'), $media->disk); Queue::assertPushed(ProcessMedia::class); // An unrelated save must not queue the same work again. Queue::fake(); $project->update(['is_featured' => true]); Queue::assertNotPushed(ProcessMedia::class); } #[Test] public function two_records_pointing_at_one_upload_share_a_single_row(): void { Queue::fake(); Project::factory()->create(['cover_image' => 'projects/shared.jpg']); Project::factory()->create(['cover_image' => 'projects/shared.jpg']); // The file is the identity, so the second one costs no extra // processing and no second set of derived versions. $this->assertSame(1, Media::query()->where('path', 'projects/shared.jpg')->count()); Queue::assertPushed(ProcessMedia::class, 1); } #[Test] public function the_command_picks_up_whatever_the_queue_did_not(): void { Queue::fake(); $this->putImage('projects/pending.jpg'); $this->assertSame(1, Media::query()->pending()->count()); $this->artisan('media:process')->assertSuccessful(); // The scheduler runs this, so a worker that is down costs a delay // rather than an image that stays half-done forever. $this->assertSame(0, Media::query()->pending()->count()); } }