group('slow'); use Core\Media\Image\ImageOptimization; use Core\Media\Image\ImageOptimizer; use Core\Media\Image\OptimizationResult; use Core\Tenant\Models\User; use Core\Tenant\Models\Workspace; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; beforeEach(function () { $this->user = User::factory()->create(); $this->workspace = Workspace::factory()->create(); $this->user->hostWorkspaces()->attach($this->workspace->id, ['is_default' => true]); // Ensure storage directory exists Storage::fake('local'); // Override config for testing config([ 'images.optimization.enabled' => true, 'images.optimization.driver' => 'gd', 'images.optimization.quality' => 80, 'images.optimization.png_compression' => 6, 'images.optimization.min_size_kb' => 10, 'images.optimization.max_size_mb' => 10, ]); }); describe('ImageOptimizer service', function () { it('can be instantiated', function () { $optimizer = new ImageOptimizer; expect($optimizer)->toBeInstanceOf(ImageOptimizer::class); }); it('optimises JPEG images with GD driver', function () { // Create a test JPEG image $testImage = createTestJpegImage(800, 600); $optimizer = new ImageOptimizer; $result = $optimizer->optimize($testImage); expect($result)->toBeInstanceOf(OptimizationResult::class); expect($result->originalSize)->toBeGreaterThan(0); expect($result->optimizedSize)->toBeGreaterThan(0); expect($result->driver)->toBe('gd'); expect($result->path)->toBe($testImage); // Clean up unlink($testImage); }); it('optimises PNG images with compression', function () { // Create a test PNG image $testImage = createTestPngImage(400, 300); $optimizer = new ImageOptimizer; $result = $optimizer->optimize($testImage); expect($result)->toBeInstanceOf(OptimizationResult::class); expect($result->originalSize)->toBeGreaterThan(0); expect($result->optimizedSize)->toBeGreaterThan(0); expect($result->driver)->toBe('gd'); // Clean up unlink($testImage); }); it('skips files smaller than minimum size', function () { // Create a tiny image (less than 10KB) $testImage = createTestJpegImage(50, 50); $optimizer = new ImageOptimizer; $result = $optimizer->optimize($testImage); // Should return no-op result (no optimization) expect($result->percentageSaved)->toBe(0); // Clean up unlink($testImage); }); it('skips files larger than maximum size', function () { // Set a very small max size for testing config(['images.optimization.max_size_mb' => 0.001]); // 1KB $testImage = createTestJpegImage(800, 600); $optimizer = new ImageOptimizer; $result = $optimizer->optimize($testImage); // Should return no-op result expect($result->percentageSaved)->toBe(0); // Clean up unlink($testImage); }); it('throws exception for non-existent file', function () { $optimizer = new ImageOptimizer; $optimizer->optimize('/path/to/nonexistent/file.jpg'); })->throws(InvalidArgumentException::class, 'File not found'); it('handles invalid image files gracefully', function () { // Create a text file pretending to be an image $testFile = sys_get_temp_dir().'/test_invalid.jpg'; file_put_contents($testFile, 'This is not an image'); $optimizer = new ImageOptimizer; $result = $optimizer->optimize($testFile); // Service should return no-op result instead of throwing expect($result)->toBeInstanceOf(OptimizationResult::class); expect($result->percentageSaved)->toBe(0); unlink($testFile); }); it('respects custom quality settings', function () { $testImage1 = createTestJpegImage(800, 600); $testImage2 = createTestJpegImage(800, 600); $optimizer = new ImageOptimizer; // High quality (less compression) $result1 = $optimizer->optimize($testImage1, ['quality' => 95]); // Low quality (more compression) $result2 = $optimizer->optimize($testImage2, ['quality' => 50]); // Lower quality should result in smaller file expect($result2->optimizedSize)->toBeLessThan($result1->optimizedSize); // Clean up unlink($testImage1); unlink($testImage2); }); it('handles WebP images', function () { // Only test if WebP support is available if (! function_exists('imagewebp')) { $this->markTestSkipped('WebP support not available in GD'); } $testImage = createTestWebpImage(400, 300); $optimizer = new ImageOptimizer; $result = $optimizer->optimize($testImage); expect($result)->toBeInstanceOf(OptimizationResult::class); expect($result->driver)->toBe('gd'); // Clean up unlink($testImage); }); it('can be disabled via config', function () { config(['images.optimization.enabled' => false]); $testImage = createTestJpegImage(800, 600); $optimizer = new ImageOptimizer; $result = $optimizer->optimize($testImage); // Should return no-op result expect($result->percentageSaved)->toBe(0); expect($result->originalSize)->toBe($result->optimizedSize); // Clean up unlink($testImage); }); }); describe('Uploaded file optimization', function () { it('optimises uploaded files', function () { // Create a test JPEG $tempPath = createTestJpegImage(800, 600); // Simulate UploadedFile $uploadedFile = new UploadedFile($tempPath, 'test.jpg', 'image/jpeg', null, true); $optimizer = new ImageOptimizer; $result = $optimizer->optimizeUploadedFile($uploadedFile); expect($result)->toBeInstanceOf(OptimizationResult::class); expect($result->originalSize)->toBeGreaterThan(0); // Clean up @unlink($tempPath); }); }); describe('OptimizationResult value object', function () { it('calculates bytes saved correctly', function () { $result = new OptimizationResult( originalSize: 100000, optimizedSize: 65000, percentageSaved: 35, path: '/test/path.jpg', driver: 'gd' ); expect($result->bytesSaved())->toBe(35000); }); it('reports success when savings exist', function () { $result = new OptimizationResult( originalSize: 100000, optimizedSize: 80000, percentageSaved: 20, path: '/test/path.jpg', driver: 'gd' ); expect($result->wasSuccessful())->toBeTrue(); }); it('reports no success when no savings', function () { $result = new OptimizationResult( originalSize: 100000, optimizedSize: 100000, percentageSaved: 0, path: '/test/path.jpg', driver: 'gd' ); expect($result->wasSuccessful())->toBeFalse(); }); it('generates human-readable summary', function () { $result = new OptimizationResult( originalSize: 120000, optimizedSize: 66000, percentageSaved: 45, path: '/test/path.jpg', driver: 'gd' ); $summary = $result->getSummary(); expect($summary)->toContain('45%'); expect($summary)->toContain('KB'); }); it('converts to array', function () { $result = new OptimizationResult( originalSize: 100000, optimizedSize: 80000, percentageSaved: 20, path: '/test/path.jpg', driver: 'gd' ); $array = $result->toArray(); expect($array)->toHaveKeys([ 'original_size', 'optimized_size', 'percentage_saved', 'path', 'driver', 'bytes_saved', ]); expect($array['bytes_saved'])->toBe(20000); }); }); describe('ImageOptimization model', function () { it('stores optimization record in database', function () { $optimization = ImageOptimization::create([ 'path' => '/storage/test.jpg', 'original_path' => null, 'original_size' => 150000, 'optimized_size' => 90000, 'percentage_saved' => 40, 'driver' => 'gd', 'quality' => 80, 'workspace_id' => $this->workspace->id, ]); expect($optimization->exists)->toBeTrue(); expect($optimization->workspace_id)->toBe($this->workspace->id); expect($optimization->percentage_saved)->toBe(40); }); it('generates human-readable savings', function () { $optimization = ImageOptimization::create([ 'path' => '/storage/test.jpg', 'original_size' => 120000, 'optimized_size' => 66000, 'percentage_saved' => 45, 'driver' => 'gd', 'quality' => 80, ]); $savings = $optimization->savings_human; expect($savings)->toContain('45%'); expect($savings)->toContain('KB'); }); it('scopes to workspace', function () { $workspace2 = Workspace::factory()->create(); // Create optimization for workspace 1 ImageOptimization::create([ 'path' => '/storage/test1.jpg', 'original_size' => 100000, 'optimized_size' => 80000, 'percentage_saved' => 20, 'driver' => 'gd', 'quality' => 80, 'workspace_id' => $this->workspace->id, ]); // Create optimization for workspace 2 ImageOptimization::create([ 'path' => '/storage/test2.jpg', 'original_size' => 100000, 'optimized_size' => 80000, 'percentage_saved' => 20, 'driver' => 'gd', 'quality' => 80, 'workspace_id' => $workspace2->id, ]); $workspace1Optimizations = ImageOptimization::forWorkspace($this->workspace)->get(); expect($workspace1Optimizations)->toHaveCount(1); expect($workspace1Optimizations->first()->workspace_id)->toBe($this->workspace->id); }); it('calculates workspace statistics', function () { // Create multiple optimizations ImageOptimization::create([ 'path' => '/storage/test1.jpg', 'original_size' => 100000, 'optimized_size' => 80000, 'percentage_saved' => 20, 'driver' => 'gd', 'quality' => 80, 'workspace_id' => $this->workspace->id, ]); ImageOptimization::create([ 'path' => '/storage/test2.jpg', 'original_size' => 150000, 'optimized_size' => 90000, 'percentage_saved' => 40, 'driver' => 'gd', 'quality' => 80, 'workspace_id' => $this->workspace->id, ]); $stats = ImageOptimization::getWorkspaceStats($this->workspace); expect($stats['count'])->toBe(2); expect($stats['total_original'])->toBe(250000); expect($stats['total_optimized'])->toBe(170000); expect($stats['total_saved'])->toBe(80000); expect($stats['average_percentage'])->toBeGreaterThan(0); }); }); describe('Statistics tracking', function () { it('records optimization in database via service', function () { $testImage = createTestJpegImage(800, 600); $optimizer = new ImageOptimizer; $result = $optimizer->optimize($testImage); // Record the optimization $record = $optimizer->recordOptimization( $result, $this->workspace ); expect($record)->toBeInstanceOf(ImageOptimization::class); expect($record->workspace_id)->toBe($this->workspace->id); expect($record->driver)->toBe('gd'); // Clean up unlink($testImage); }); it('retrieves workspace stats via service', function () { $testImage = createTestJpegImage(800, 600); $optimizer = new ImageOptimizer; $result = $optimizer->optimize($testImage); $optimizer->recordOptimization($result, $this->workspace); $stats = $optimizer->getStats($this->workspace); expect($stats)->toBeArray(); expect($stats['count'])->toBe(1); // Clean up unlink($testImage); }); }); // Helper functions function createTestJpegImage(int $width, int $height): string { $image = imagecreatetruecolor($width, $height); // Fill with some colours to make it realistic $bgColor = imagecolorallocate($image, 100, 150, 200); imagefill($image, 0, 0, $bgColor); // Add some shapes for realistic compression $shapeColor = imagecolorallocate($image, 255, 100, 50); imagefilledrectangle($image, 50, 50, 150, 150, $shapeColor); imagefilledellipse($image, 300, 200, 100, 100, $shapeColor); $tempPath = sys_get_temp_dir().'/test_'.uniqid().'.jpg'; imagejpeg($image, $tempPath, 95); // High quality initially imagedestroy($image); return $tempPath; } function createTestPngImage(int $width, int $height): string { $image = imagecreatetruecolor($width, $height); $bgColor = imagecolorallocate($image, 100, 150, 200); imagefill($image, 0, 0, $bgColor); $shapeColor = imagecolorallocate($image, 255, 100, 50); imagefilledrectangle($image, 20, 20, 80, 80, $shapeColor); $tempPath = sys_get_temp_dir().'/test_'.uniqid().'.png'; imagepng($image, $tempPath, 0); // No compression initially imagedestroy($image); return $tempPath; } function createTestWebpImage(int $width, int $height): string { $image = imagecreatetruecolor($width, $height); $bgColor = imagecolorallocate($image, 100, 150, 200); imagefill($image, 0, 0, $bgColor); $tempPath = sys_get_temp_dir().'/test_'.uniqid().'.webp'; imagewebp($image, $tempPath, 95); imagedestroy($image); return $tempPath; }