From 866d63e77911fee8b4a76a47ef3c5d9755247e54 Mon Sep 17 00:00:00 2001 From: Ivan Moroz Date: Sun, 31 May 2026 20:27:03 +0300 Subject: [PATCH] CONV-107: Add settings field validation errors Co-Authored-By: Claude Opus 4.8 --- app/Livewire/Dashboard/DashboardConverter.php | 19 ++++++ .../InvalidConverterOptionsException.php | 25 +++++++- .../partials/dynamic-options-form.blade.php | 3 + ...shboardConverterSettingsValidationTest.php | 59 +++++++++++++++++++ 4 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 tests/Feature/Livewire/DashboardConverterSettingsValidationTest.php diff --git a/app/Livewire/Dashboard/DashboardConverter.php b/app/Livewire/Dashboard/DashboardConverter.php index 75cfef3..1c8e6fa 100644 --- a/app/Livewire/Dashboard/DashboardConverter.php +++ b/app/Livewire/Dashboard/DashboardConverter.php @@ -8,6 +8,8 @@ use App\Models\FileRecord; use App\Support\Converters\ConverterRegistry; use App\Support\Converters\DTO\ConverterTarget; +use App\Support\Converters\Exceptions\InvalidConverterOptionsException; +use App\Support\Converters\OptionsValidator; use App\Support\Files\UploadedFileRules; use App\ViewModels\TargetFormatCardViewModel; use Livewire\Component; @@ -150,6 +152,23 @@ public function selectTargetFormat(string $targetFormat): void $this->step = 'settings'; } + public function validateSettings(): bool + { + $this->resetErrorBag(); + + try { + app(OptionsValidator::class)->validate($this->optionsSchema, $this->options); + } catch (InvalidConverterOptionsException $exception) { + foreach ($exception->fieldErrors() as $field => $message) { + $this->addError("options.{$field}", $message); + } + + return false; + } + + return true; + } + private function initializeOptionsFromSchema(): void { $this->options = []; diff --git a/app/Support/Converters/Exceptions/InvalidConverterOptionsException.php b/app/Support/Converters/Exceptions/InvalidConverterOptionsException.php index 06eb4d6..17093de 100644 --- a/app/Support/Converters/Exceptions/InvalidConverterOptionsException.php +++ b/app/Support/Converters/Exceptions/InvalidConverterOptionsException.php @@ -6,18 +6,37 @@ final class InvalidConverterOptionsException extends DomainException { + public function __construct(string $message, public readonly ?string $optionKey = null) + { + parent::__construct($message); + } + public static function becauseOptionIsUnknown(string $key): self { - return new self("Unknown converter option: {$key}."); + return new self("Unknown converter option: {$key}.", $key); } public static function becauseOptionIsRequired(string $key): self { - return new self("Converter option [{$key}] is required."); + return new self("Converter option [{$key}] is required.", $key); } public static function becauseValueIsNotAllowed(string $key): self { - return new self("Converter option [{$key}] has an invalid value."); + return new self("Converter option [{$key}] has an invalid value.", $key); + } + + /** + * Field-level errors keyed by the offending option key. + * + * @return array + */ + public function fieldErrors(): array + { + if ($this->optionKey === null) { + return []; + } + + return [$this->optionKey => $this->getMessage()]; } } diff --git a/resources/views/livewire/dashboard/dashboard-converter/partials/dynamic-options-form.blade.php b/resources/views/livewire/dashboard/dashboard-converter/partials/dynamic-options-form.blade.php index 71490eb..0efceda 100644 --- a/resources/views/livewire/dashboard/dashboard-converter/partials/dynamic-options-form.blade.php +++ b/resources/views/livewire/dashboard/dashboard-converter/partials/dynamic-options-form.blade.php @@ -13,6 +13,9 @@
@if ($fieldKey !== null && view()->exists($fieldView)) @include($fieldView, ['key' => $fieldKey, 'field' => $field]) + @error('options.'.$fieldKey) +

{{ $message }}

+ @enderror @else

Unsupported field type: {{ $field['type'] ?? 'unknown' }} diff --git a/tests/Feature/Livewire/DashboardConverterSettingsValidationTest.php b/tests/Feature/Livewire/DashboardConverterSettingsValidationTest.php new file mode 100644 index 0000000..026e242 --- /dev/null +++ b/tests/Feature/Livewire/DashboardConverterSettingsValidationTest.php @@ -0,0 +1,59 @@ +create(); + $file = FileRecord::factory()->for($user)->create(['extension' => 'png']); + + Livewire::actingAs($user) + ->test(DashboardConverter::class) + ->set('currentFileId', $file->id) + ->call('selectTargetFormat', 'jpg') + ->set('options.quality', 'invalid-quality') + ->call('validateSettings') + ->assertHasErrors(['options.quality']); +}); + +it('passes validation for the default options', function () { + $user = User::factory()->create(); + $file = FileRecord::factory()->for($user)->create(['extension' => 'png']); + + Livewire::actingAs($user) + ->test(DashboardConverter::class) + ->set('currentFileId', $file->id) + ->call('selectTargetFormat', 'jpg') + ->call('validateSettings') + ->assertHasNoErrors(); +}); + +it('rejects an unknown settings option', function () { + $user = User::factory()->create(); + $file = FileRecord::factory()->for($user)->create(['extension' => 'png']); + + Livewire::actingAs($user) + ->test(DashboardConverter::class) + ->set('currentFileId', $file->id) + ->call('selectTargetFormat', 'jpg') + ->set('options.bogus', 'value') + ->call('validateSettings') + ->assertHasErrors(['options.bogus']); +}); + +it('does not create any extra records during settings validation', function () { + $user = User::factory()->create(); + $file = FileRecord::factory()->for($user)->create(['extension' => 'png']); + + Livewire::actingAs($user) + ->test(DashboardConverter::class) + ->set('currentFileId', $file->id) + ->call('selectTargetFormat', 'jpg') + ->set('options.quality', 'best') + ->call('validateSettings') + ->assertHasNoErrors(); + + expect(FileRecord::query()->where('user_id', $user->id)->count())->toBe(1); +});