diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 8692e51..c958a85 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -11,6 +11,8 @@ reviews: enabled: true labels: - "release" + base_branches: + - "main" drafts: false request_changes_workflow: false high_level_summary: true diff --git a/app/Providers/ConverterServiceProvider.php b/app/Providers/ConverterServiceProvider.php new file mode 100644 index 0000000..42feda6 --- /dev/null +++ b/app/Providers/ConverterServiceProvider.php @@ -0,0 +1,29 @@ +app->singleton(ConverterRegistry::class, function ($app) { + return new ConverterRegistry([ + $app->make(PngToJpgConverter::class), + $app->make(PngToWebpConverter::class), + $app->make(PngToPdfConverter::class), + $app->make(JpgToPngConverter::class), + $app->make(JpgToWebpConverter::class), + $app->make(JpgToPdfConverter::class), + ]); + }); + } +} diff --git a/app/Support/Converters/Contracts/Converter.php b/app/Support/Converters/Contracts/Converter.php index 93e0529..d7094c2 100644 --- a/app/Support/Converters/Contracts/Converter.php +++ b/app/Support/Converters/Contracts/Converter.php @@ -17,4 +17,6 @@ public function description(): string; public function optionsSchema(): array; public function validateOptions(array $options): array; + + public function isRecommended(): bool; } diff --git a/app/Support/Converters/ConverterRegistry.php b/app/Support/Converters/ConverterRegistry.php index 453da71..34f288c 100644 --- a/app/Support/Converters/ConverterRegistry.php +++ b/app/Support/Converters/ConverterRegistry.php @@ -5,6 +5,7 @@ use App\Enums\FileFormat; use App\Support\Converters\Contracts\Converter; use App\Support\Converters\DTO\ConverterTarget; +use App\Support\Converters\Exceptions\UnsupportedFormatException; final class ConverterRegistry { @@ -22,7 +23,11 @@ public function all(): array /** @return list */ public function forSource(string $source): array { - $source = FileFormat::normalize($source); + try { + $source = FileFormat::normalize($source); + } catch (UnsupportedFormatException) { + return []; + } return array_values(array_filter( $this->converters, @@ -32,8 +37,12 @@ public function forSource(string $source): array public function find(string $source, string $target): ?Converter { - $source = FileFormat::normalize($source); - $target = FileFormat::normalize($target); + try { + $source = FileFormat::normalize($source); + $target = FileFormat::normalize($target); + } catch (UnsupportedFormatException) { + return null; + } foreach ($this->converters as $converter) { if ($converter->sourceFormat() === $source && $converter->targetFormat() === $target) { @@ -53,7 +62,7 @@ public function targetsFor(string $source): array label: $converter->label(), description: $converter->description(), converterKey: $converter->key(), - recommended: false, + recommended: $converter->isRecommended(), ), $this->forSource($source), ); diff --git a/app/Support/Converters/Image/JpgToPdfConverter.php b/app/Support/Converters/Image/JpgToPdfConverter.php new file mode 100644 index 0000000..a75d14b --- /dev/null +++ b/app/Support/Converters/Image/JpgToPdfConverter.php @@ -0,0 +1,109 @@ + 'page_size', + 'type' => 'segmented', + 'label' => 'Page size', + 'default' => 'auto', + 'options' => [ + ['value' => 'auto', 'label' => 'Auto'], + ['value' => 'a4', 'label' => 'A4'], + ['value' => 'letter', 'label' => 'Letter'], + ], + ], + [ + 'key' => 'orientation', + 'type' => 'segmented', + 'label' => 'Orientation', + 'default' => 'auto', + 'options' => [ + ['value' => 'auto', 'label' => 'Auto'], + ['value' => 'portrait', 'label' => 'Portrait'], + ['value' => 'landscape', 'label' => 'Landscape'], + ], + ], + [ + 'key' => 'margin', + 'type' => 'segmented', + 'label' => 'Margin', + 'default' => 'small', + 'options' => [ + ['value' => 'none', 'label' => 'None'], + ['value' => 'small', 'label' => 'Small'], + ['value' => 'medium', 'label' => 'Medium'], + ], + ], + [ + 'key' => 'fit_mode', + 'type' => 'segmented', + 'label' => 'Fit mode', + 'default' => 'contain', + 'options' => [ + ['value' => 'contain', 'label' => 'Contain'], + ['value' => 'cover', 'label' => 'Cover'], + ['value' => 'original', 'label' => 'Original'], + ], + ], + [ + 'key' => 'compression', + 'type' => 'segmented', + 'label' => 'Compression', + 'default' => 'balanced', + 'options' => [ + ['value' => 'none', 'label' => 'None'], + ['value' => 'balanced', 'label' => 'Balanced'], + ['value' => 'small', 'label' => 'Small'], + ], + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } + + public function isRecommended(): bool + { + return false; + } +} diff --git a/app/Support/Converters/Image/JpgToPngConverter.php b/app/Support/Converters/Image/JpgToPngConverter.php new file mode 100644 index 0000000..d37b9a4 --- /dev/null +++ b/app/Support/Converters/Image/JpgToPngConverter.php @@ -0,0 +1,72 @@ + 'resize', + 'type' => 'select', + 'label' => 'Resize', + 'default' => 'original', + 'options' => [ + ['value' => 'original', 'label' => 'Original'], + ['value' => '1920', 'label' => '1920 px'], + ['value' => '1280', 'label' => '1280 px'], + ['value' => 'custom', 'label' => 'Custom'], + ], + ], + [ + 'key' => 'remove_metadata', + 'type' => 'toggle', + 'label' => 'Remove metadata', + 'default' => true, + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } + + public function isRecommended(): bool + { + return false; + } +} diff --git a/app/Support/Converters/Image/JpgToWebpConverter.php b/app/Support/Converters/Image/JpgToWebpConverter.php new file mode 100644 index 0000000..500030f --- /dev/null +++ b/app/Support/Converters/Image/JpgToWebpConverter.php @@ -0,0 +1,83 @@ + 'quality', + 'type' => 'segmented', + 'label' => 'Quality', + 'default' => 'high', + 'options' => [ + ['value' => 'medium', 'label' => 'Medium'], + ['value' => 'high', 'label' => 'High'], + ['value' => 'best', 'label' => 'Best'], + ], + ], + [ + 'key' => 'resize', + 'type' => 'select', + 'label' => 'Resize', + 'default' => 'original', + 'options' => [ + ['value' => 'original', 'label' => 'Original'], + ['value' => '1920', 'label' => '1920 px'], + ['value' => '1280', 'label' => '1280 px'], + ['value' => 'custom', 'label' => 'Custom'], + ], + ], + [ + 'key' => 'remove_metadata', + 'type' => 'toggle', + 'label' => 'Remove metadata', + 'default' => true, + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } + + public function isRecommended(): bool + { + return true; + } +} diff --git a/app/Support/Converters/Image/PngToJpgConverter.php b/app/Support/Converters/Image/PngToJpgConverter.php new file mode 100644 index 0000000..d197ae5 --- /dev/null +++ b/app/Support/Converters/Image/PngToJpgConverter.php @@ -0,0 +1,89 @@ + 'quality', + 'type' => 'segmented', + 'label' => 'Quality', + 'default' => 'high', + 'options' => [ + ['value' => 'medium', 'label' => 'Medium'], + ['value' => 'high', 'label' => 'High'], + ['value' => 'best', 'label' => 'Best'], + ], + ], + [ + 'key' => 'resize', + 'type' => 'select', + 'label' => 'Resize', + 'default' => 'original', + 'options' => [ + ['value' => 'original', 'label' => 'Original'], + ['value' => '1920', 'label' => '1920 px'], + ['value' => '1280', 'label' => '1280 px'], + ['value' => 'custom', 'label' => 'Custom'], + ], + ], + [ + 'key' => 'background_color', + 'type' => 'color', + 'label' => 'Background color', + 'default' => '#ffffff', + ], + [ + 'key' => 'remove_metadata', + 'type' => 'toggle', + 'label' => 'Remove metadata', + 'default' => true, + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } + + public function isRecommended(): bool + { + return true; + } +} diff --git a/app/Support/Converters/Image/PngToPdfConverter.php b/app/Support/Converters/Image/PngToPdfConverter.php new file mode 100644 index 0000000..a0bdd95 --- /dev/null +++ b/app/Support/Converters/Image/PngToPdfConverter.php @@ -0,0 +1,109 @@ + 'page_size', + 'type' => 'segmented', + 'label' => 'Page size', + 'default' => 'auto', + 'options' => [ + ['value' => 'auto', 'label' => 'Auto'], + ['value' => 'a4', 'label' => 'A4'], + ['value' => 'letter', 'label' => 'Letter'], + ], + ], + [ + 'key' => 'orientation', + 'type' => 'segmented', + 'label' => 'Orientation', + 'default' => 'auto', + 'options' => [ + ['value' => 'auto', 'label' => 'Auto'], + ['value' => 'portrait', 'label' => 'Portrait'], + ['value' => 'landscape', 'label' => 'Landscape'], + ], + ], + [ + 'key' => 'margin', + 'type' => 'segmented', + 'label' => 'Margin', + 'default' => 'small', + 'options' => [ + ['value' => 'none', 'label' => 'None'], + ['value' => 'small', 'label' => 'Small'], + ['value' => 'medium', 'label' => 'Medium'], + ], + ], + [ + 'key' => 'fit_mode', + 'type' => 'segmented', + 'label' => 'Fit mode', + 'default' => 'contain', + 'options' => [ + ['value' => 'contain', 'label' => 'Contain'], + ['value' => 'cover', 'label' => 'Cover'], + ['value' => 'original', 'label' => 'Original'], + ], + ], + [ + 'key' => 'compression', + 'type' => 'segmented', + 'label' => 'Compression', + 'default' => 'balanced', + 'options' => [ + ['value' => 'none', 'label' => 'None'], + ['value' => 'balanced', 'label' => 'Balanced'], + ['value' => 'small', 'label' => 'Small'], + ], + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } + + public function isRecommended(): bool + { + return false; + } +} diff --git a/app/Support/Converters/Image/PngToWebpConverter.php b/app/Support/Converters/Image/PngToWebpConverter.php new file mode 100644 index 0000000..79c5a1c --- /dev/null +++ b/app/Support/Converters/Image/PngToWebpConverter.php @@ -0,0 +1,89 @@ + 'quality', + 'type' => 'segmented', + 'label' => 'Quality', + 'default' => 'high', + 'options' => [ + ['value' => 'medium', 'label' => 'Medium'], + ['value' => 'high', 'label' => 'High'], + ['value' => 'best', 'label' => 'Best'], + ], + ], + [ + 'key' => 'lossless', + 'type' => 'toggle', + 'label' => 'Lossless', + 'default' => false, + ], + [ + 'key' => 'resize', + 'type' => 'select', + 'label' => 'Resize', + 'default' => 'original', + 'options' => [ + ['value' => 'original', 'label' => 'Original'], + ['value' => '1920', 'label' => '1920 px'], + ['value' => '1280', 'label' => '1280 px'], + ['value' => 'custom', 'label' => 'Custom'], + ], + ], + [ + 'key' => 'remove_metadata', + 'type' => 'toggle', + 'label' => 'Remove metadata', + 'default' => true, + ], + ]; + } + + public function validateOptions(array $options): array + { + return $this->optionsValidator->validate($this->optionsSchema(), $options); + } + + public function isRecommended(): bool + { + return false; + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index fc94ae6..c8c0245 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,7 +1,9 @@ [ + 'png:jpg', + 'png:webp', + 'png:pdf', + 'jpg:png', + 'jpg:webp', + 'jpg:pdf', + ], + +]; diff --git a/tests/Fakes/Converters/FakeJpgToWebpConverter.php b/tests/Fakes/Converters/FakeJpgToWebpConverter.php index 0b3356c..469b86f 100644 --- a/tests/Fakes/Converters/FakeJpgToWebpConverter.php +++ b/tests/Fakes/Converters/FakeJpgToWebpConverter.php @@ -40,4 +40,9 @@ public function validateOptions(array $options): array { return $options; } + + public function isRecommended(): bool + { + return false; + } } diff --git a/tests/Fakes/Converters/FakePngToJpgConverter.php b/tests/Fakes/Converters/FakePngToJpgConverter.php index c667bba..21c53c8 100644 --- a/tests/Fakes/Converters/FakePngToJpgConverter.php +++ b/tests/Fakes/Converters/FakePngToJpgConverter.php @@ -40,4 +40,9 @@ public function validateOptions(array $options): array { return $options; } + + public function isRecommended(): bool + { + return false; + } } diff --git a/tests/Fakes/Converters/FakePngToPdfConverter.php b/tests/Fakes/Converters/FakePngToPdfConverter.php index 81bff9b..01a33cd 100644 --- a/tests/Fakes/Converters/FakePngToPdfConverter.php +++ b/tests/Fakes/Converters/FakePngToPdfConverter.php @@ -40,4 +40,9 @@ public function validateOptions(array $options): array { return $options; } + + public function isRecommended(): bool + { + return false; + } } diff --git a/tests/Unit/Converters/ConverterRegistryTargetsTest.php b/tests/Unit/Converters/ConverterRegistryTargetsTest.php new file mode 100644 index 0000000..da0744c --- /dev/null +++ b/tests/Unit/Converters/ConverterRegistryTargetsTest.php @@ -0,0 +1,53 @@ +targetsFor('png')) + ->map(fn ($target) => $target->format) + ->all(); + + expect($targets)->toContain('jpg'); + expect($targets)->toContain('webp'); + expect($targets)->toContain('pdf'); + expect($targets)->not->toContain('mp3'); +}); + +it('lists jpg target capabilities', function () { + $registry = app(ConverterRegistry::class); + + $targets = collect($registry->targetsFor('jpg')) + ->map(fn ($target) => $target->format) + ->all(); + + expect($targets)->toContain('png'); + expect($targets)->toContain('webp'); + expect($targets)->toContain('pdf'); + expect($targets)->not->toContain('mp3'); +}); + +it('finds exact mvp converter pairs', function () { + $registry = app(ConverterRegistry::class); + + expect($registry->find('png', 'jpg'))->not->toBeNull(); + expect($registry->find('png', 'webp'))->not->toBeNull(); + expect($registry->find('png', 'pdf'))->not->toBeNull(); + expect($registry->find('jpg', 'png'))->not->toBeNull(); + expect($registry->find('jpg', 'webp'))->not->toBeNull(); + expect($registry->find('jpg', 'pdf'))->not->toBeNull(); +}); + +it('does not find unsupported converter pairs', function () { + $registry = app(ConverterRegistry::class); + + expect($registry->find('png', 'mp3'))->toBeNull(); + expect($registry->find('pdf', 'docx'))->toBeNull(); +}); + +it('returns empty target list for unsupported source format', function () { + $registry = app(ConverterRegistry::class); + + expect($registry->targetsFor('mp4'))->toBe([]); +}); diff --git a/tests/Unit/Converters/Image/JpgToPdfConverterTest.php b/tests/Unit/Converters/Image/JpgToPdfConverterTest.php new file mode 100644 index 0000000..689ceaf --- /dev/null +++ b/tests/Unit/Converters/Image/JpgToPdfConverterTest.php @@ -0,0 +1,28 @@ +key())->toBe('jpg:pdf'); + expect($converter->sourceFormat())->toBe('jpg'); + expect($converter->targetFormat())->toBe('pdf'); + expect($converter->label())->toBe('PDF'); +}); + +it('provides valid default options for jpg to pdf', function () { + $converter = app(JpgToPdfConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('page_size'); + expect($options)->toHaveKey('orientation'); + expect($options)->toHaveKey('margin'); + expect($options)->toHaveKey('fit_mode'); + expect($options)->toHaveKey('compression'); +}); diff --git a/tests/Unit/Converters/Image/JpgToPngConverterTest.php b/tests/Unit/Converters/Image/JpgToPngConverterTest.php new file mode 100644 index 0000000..4a0ec9c --- /dev/null +++ b/tests/Unit/Converters/Image/JpgToPngConverterTest.php @@ -0,0 +1,27 @@ +key())->toBe('jpg:png'); + expect($converter->sourceFormat())->toBe('jpg'); + expect($converter->targetFormat())->toBe('png'); + expect($converter->label())->toBe('PNG'); + expect($converter->description())->not->toBeEmpty(); +}); + +it('provides valid default options for jpg to png', function () { + $converter = app(JpgToPngConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('resize'); + expect($options)->toHaveKey('remove_metadata'); + expect($options)->not->toHaveKey('transparency'); +}); diff --git a/tests/Unit/Converters/Image/JpgToWebpConverterTest.php b/tests/Unit/Converters/Image/JpgToWebpConverterTest.php new file mode 100644 index 0000000..8e40eb2 --- /dev/null +++ b/tests/Unit/Converters/Image/JpgToWebpConverterTest.php @@ -0,0 +1,26 @@ +key())->toBe('jpg:webp'); + expect($converter->sourceFormat())->toBe('jpg'); + expect($converter->targetFormat())->toBe('webp'); + expect($converter->label())->toBe('WEBP'); +}); + +it('provides valid default options for jpg to webp', function () { + $converter = app(JpgToWebpConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('quality'); + expect($options)->toHaveKey('resize'); + expect($options)->toHaveKey('remove_metadata'); +}); diff --git a/tests/Unit/Converters/Image/PngToJpgConverterTest.php b/tests/Unit/Converters/Image/PngToJpgConverterTest.php new file mode 100644 index 0000000..2edcc5b --- /dev/null +++ b/tests/Unit/Converters/Image/PngToJpgConverterTest.php @@ -0,0 +1,30 @@ +key())->toBe('png:jpg'); + expect($converter->sourceFormat())->toBe('png'); + expect($converter->targetFormat())->toBe('jpg'); + expect($converter->label())->toBe('JPG'); + expect($converter->description())->not->toBeEmpty(); +}); + +it('provides valid default options for png to jpg', function () { + $converter = app(PngToJpgConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('quality'); + expect($options)->toHaveKey('resize'); + expect($options)->toHaveKey('background_color'); + expect($options)->toHaveKey('remove_metadata'); + expect($options['background_color'])->toBe('#ffffff'); + expect($options['remove_metadata'])->toBeTrue(); +}); diff --git a/tests/Unit/Converters/Image/PngToPdfConverterTest.php b/tests/Unit/Converters/Image/PngToPdfConverterTest.php new file mode 100644 index 0000000..231761a --- /dev/null +++ b/tests/Unit/Converters/Image/PngToPdfConverterTest.php @@ -0,0 +1,29 @@ +key())->toBe('png:pdf'); + expect($converter->sourceFormat())->toBe('png'); + expect($converter->targetFormat())->toBe('pdf'); + expect($converter->label())->toBe('PDF'); + expect($converter->description())->not->toBeEmpty(); +}); + +it('provides valid default options for png to pdf', function () { + $converter = app(PngToPdfConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('page_size'); + expect($options)->toHaveKey('orientation'); + expect($options)->toHaveKey('margin'); + expect($options)->toHaveKey('fit_mode'); + expect($options)->toHaveKey('compression'); +}); diff --git a/tests/Unit/Converters/Image/PngToWebpConverterTest.php b/tests/Unit/Converters/Image/PngToWebpConverterTest.php new file mode 100644 index 0000000..f1df8b6 --- /dev/null +++ b/tests/Unit/Converters/Image/PngToWebpConverterTest.php @@ -0,0 +1,29 @@ +key())->toBe('png:webp'); + expect($converter->sourceFormat())->toBe('png'); + expect($converter->targetFormat())->toBe('webp'); + expect($converter->label())->toBe('WEBP'); + expect($converter->description())->not->toBeEmpty(); +}); + +it('provides valid default options for png to webp', function () { + $converter = app(PngToWebpConverter::class); + + $options = app(OptionsValidator::class)->validate( + $converter->optionsSchema(), + [] + ); + + expect($options)->toHaveKey('quality'); + expect($options)->toHaveKey('lossless'); + expect($options)->toHaveKey('resize'); + expect($options)->toHaveKey('remove_metadata'); + expect($options['lossless'])->toBeFalse(); +}); diff --git a/tests/Unit/Converters/MvpConverterCapabilityListTest.php b/tests/Unit/Converters/MvpConverterCapabilityListTest.php new file mode 100644 index 0000000..c0f84d6 --- /dev/null +++ b/tests/Unit/Converters/MvpConverterCapabilityListTest.php @@ -0,0 +1,22 @@ +toBeArray(); + expect($capabilities)->toContain('png:jpg'); + expect($capabilities)->toContain('png:webp'); + expect($capabilities)->toContain('png:pdf'); + expect($capabilities)->toContain('jpg:png'); + expect($capabilities)->toContain('jpg:webp'); + expect($capabilities)->toContain('jpg:pdf'); + expect($capabilities)->toHaveCount(6); +}); + +it('does not include non mvp directions in capability list', function () { + $capabilities = config('converters.mvp_capabilities'); + + expect($capabilities)->not->toContain('mp4:mp3'); + expect($capabilities)->not->toContain('pdf:docx'); + expect($capabilities)->not->toContain('docx:pdf'); +}); diff --git a/tests/Unit/Converters/MvpConverterCatalogSmokeTest.php b/tests/Unit/Converters/MvpConverterCatalogSmokeTest.php new file mode 100644 index 0000000..1690f50 --- /dev/null +++ b/tests/Unit/Converters/MvpConverterCatalogSmokeTest.php @@ -0,0 +1,65 @@ +all()) + ->map(fn ($converter) => $converter->key()) + ->sort() + ->values() + ->all(); + + expect($keys)->toBe([ + 'jpg:pdf', + 'jpg:png', + 'jpg:webp', + 'png:jpg', + 'png:pdf', + 'png:webp', + ]); +}); + +it('has valid schemas and default options for every mvp converter', function () { + $registry = app(ConverterRegistry::class); + $schemaValidator = app(OptionsSchemaValidator::class); + $optionsValidator = app(OptionsValidator::class); + + foreach ($registry->all() as $converter) { + $schemaValidator->validate($converter->optionsSchema()); + + $options = $optionsValidator->validate($converter->optionsSchema(), []); + + expect($options)->toBeArray(); + } +}); + +it('exposes a label and description for every mvp converter', function () { + $registry = app(ConverterRegistry::class); + + foreach ($registry->all() as $converter) { + expect($converter->label())->not->toBeEmpty(); + expect($converter->description())->not->toBeEmpty(); + } +}); + +it('mvp capability list matches registered converter keys', function () { + $registry = app(ConverterRegistry::class); + + $registered = collect($registry->all()) + ->map(fn ($converter) => $converter->key()) + ->sort() + ->values() + ->all(); + + $declared = collect(config('converters.mvp_capabilities')) + ->map(fn ($key) => str_replace(':', ':', $key)) + ->sort() + ->values() + ->all(); + + expect($registered)->toBe($declared); +}); diff --git a/tests/Unit/Converters/RecommendedTargetMetadataTest.php b/tests/Unit/Converters/RecommendedTargetMetadataTest.php new file mode 100644 index 0000000..02f5997 --- /dev/null +++ b/tests/Unit/Converters/RecommendedTargetMetadataTest.php @@ -0,0 +1,37 @@ +targetsFor('png')) + ->filter(fn ($target) => $target->recommended === true); + + expect($recommended)->toHaveCount(1); + expect($recommended->first()->format)->toBe('jpg'); +}); + +it('marks one jpg target as recommended', function () { + $recommended = collect(mvpRegistry()->targetsFor('jpg')) + ->filter(fn ($target) => $target->recommended === true); + + expect($recommended)->toHaveCount(1); + expect($recommended->first()->format)->toBe('webp'); +});