diff --git a/app/Filament/Resources/Vehicles/Schemas/VehicleForm.php b/app/Filament/Resources/Vehicles/Schemas/VehicleForm.php index 9ffe040..9521003 100644 --- a/app/Filament/Resources/Vehicles/Schemas/VehicleForm.php +++ b/app/Filament/Resources/Vehicles/Schemas/VehicleForm.php @@ -208,42 +208,44 @@ private static function vehicleSection(): Component ->directory('vehicle-images') ->acceptedFileTypes(self::ACCEPTED_IMAGE_MIME_TYPES) ->maxSize(self::MAX_IMAGE_SIZE_KB) - // Zuschnitt fest auf 4:3 (Auftraggeber-Wunsch). Der - // eingebaute Bildeditor (Cropper.js, Teil von Filaments - // eigenem JS-Bundle, kein CDN) oeffnet sich automatisch, - // sobald ein hochgeladenes Bild nicht bereits 4:3 ist - - // siehe automaticallyOpenImageEditorForAspectRatio() in - // vendor/filament/forms/docs/09-file-upload.md, - // Abschnitt "Enforcing a specific aspect ratio". Da hier - // nur ein einziges Seitenverhaeltnis uebergeben wird - // (kein imageEditorAspectRatioOptions() mit Alternativen), - // ist 4:3 die einzig waehlbare Option - der Cropper - // erzwingt sie ueber cropperOptions.aspectRatio (siehe - // file-upload.js, initEditor()). + // Der Zuschneide-Editor (Cropper.js, Teil von Filaments + // eigenem JS-Bundle, kein CDN) bleibt verfuegbar, aber + // freiwillig: kein festes Seitenverhaeltnis mehr. // - // imageAspectRatio() ist zugleich eine echte - // serverseitige Pruefung (Rule::dimensions()->ratio(), - // siehe BaseFileUpload::getRules()) - falls ein - // Bild ohne Browser-JS eingereicht wuerde, weist der - // Server es dennoch zurueck. getimagesize() dahinter - // ist eine PHP-Kernfunktion, keine GD/Imagick-Pflicht. + // Frueher stand hier imageAspectRatio('4:3') zusammen mit + // automaticallyOpenImageEditorForAspectRatio(). Das war + // nicht nur eine Vorgabe fuer den Editor, sondern + // erzeugte serverseitig Rule::dimensions()->ratio() + // (siehe BaseFileUpload::getRules()). Jedes Bild in einem + // anderen Verhaeltnis wurde damit abgewiesen - laut + // Anforderung soll die Bildgroesse aber egal sein, es + // gilt nur noch maxSize() weiter unten. ->imageEditor() - ->imageAspectRatio('4:3') - ->automaticallyOpenImageEditorForAspectRatio() // Verkleinerung auf 2000 Pixel Breite: das laeuft // vollstaendig im Browser (FilePondPluginImageResize, // Canvas-basiert), BEVOR die Datei ueberhaupt hochgeladen // wird - der Server bekommt bereits die verkleinerte // Datei und veraendert sie nicht weiter. Das erfordert - // keine PHP-Bildbibliothek (weder GD noch Imagick); siehe - // Bericht fuer die vollstaendige Herleitung. Ein Client - // ohne JavaScript (oder ein direkter API-Aufruf ohne - // Browser) haelt sich nicht an diese Grenze - anders als - // beim Seitenverhaeltnis oben gibt es dafuer keine - // serverseitige Zweitpruefung, da Filament dafuer keine - // eingebaute Validierungsregel mitbringt. - ->automaticallyResizeImagesMode('cover') + // keine PHP-Bildbibliothek (weder GD noch Imagick). Ein + // Client ohne JavaScript (oder ein direkter API-Aufruf + // ohne Browser) haelt sich nicht an diese Grenze; eine + // serverseitige Zweitpruefung dafuer bringt Filament + // nicht mit. Die Grenze spart Speicher, sie schuetzt + // nichts - das tut maxSize(). + // + // 'contain' statt 'cover': ohne festes Seitenverhaeltnis + // und ohne gesetzte Zielhoehe wuerde 'cover' ein Panorama + // auf ein Quadrat beschneiden. 'contain' skaliert nur + // herunter und schneidet nie. + // + // upscale(false) ist der zweite Teil des behobenen + // Fehlers: Filaments Standard ist true, wodurch ein Bild + // mit 800 Pixel Breite auf 2000 hochgerechnet wurde - + // sichtbar unscharf und um ein Vielfaches groesser als + // das Original. + ->automaticallyResizeImagesMode('contain') ->automaticallyResizeImagesToWidth('2000') + ->automaticallyUpscaleImagesWhenResizing(false) // Deutsche Fehlermeldungen: Laravels eigene // validation.php liegt in diesem Projekt nur auf // Englisch vor (kein lang/de, kein laravel-lang-Paket), @@ -255,7 +257,6 @@ private static function vehicleSection(): Component // Zahl aus der Konstante, siehe Kommentar im // VehicleDocumentsRelationManager. 'max' => 'Das Bild darf höchstens '.(self::MAX_IMAGE_SIZE_KB / 1024).' MB groß sein.', - 'dimensions' => 'Das Bild muss im Seitenverhältnis 4:3 zugeschnitten sein.', ]) ->imagePreviewHeight('160') // Ohne diese Ueberschreibung wuerde die Vorschau eines diff --git a/tests/Feature/Filament/VehicleImageUploadTest.php b/tests/Feature/Filament/VehicleImageUploadTest.php new file mode 100644 index 0000000..abca2fb --- /dev/null +++ b/tests/Feature/Filament/VehicleImageUploadTest.php @@ -0,0 +1,80 @@ +admin()->withTwoFactor()->create()); + Storage::fake('local'); +}); + +// Der Kern des Fehlers: imageAspectRatio('4:3') ist nicht nur eine Vorgabe +// fuer den Zuschneide-Editor im Browser, sondern erzeugt serverseitig +// Rule::dimensions()->ratio(). Ein Bild in einem anderen Verhaeltnis wurde +// dadurch abgewiesen, obwohl die Bildgroesse laut Anforderung egal sein soll. +it('accepts an image that is not in a 4:3 ratio', function () { + $vehicle = Vehicle::factory()->create(['image_path' => null]); + + livewire(EditVehicle::class, ['record' => $vehicle->getRouteKey()]) + ->fillForm(['image_path' => [UploadedFile::fake()->image('panorama.jpg', 1600, 400)]]) + ->call('save') + ->assertHasNoFormErrors(); + + expect($vehicle->fresh()->image_path)->not->toBeNull(); +}); + +it('accepts an image far below the resize width of 2000 pixels', function () { + $vehicle = Vehicle::factory()->create(['image_path' => null]); + + livewire(EditVehicle::class, ['record' => $vehicle->getRouteKey()]) + ->fillForm(['image_path' => [UploadedFile::fake()->image('klein.jpg', 800, 600)]]) + ->call('save') + ->assertHasNoFormErrors(); + + expect($vehicle->fresh()->image_path)->not->toBeNull(); +}); + +// Die einzige Grenze, die bleiben soll. +it('still rejects an image above the ten megabyte limit', function () { + $vehicle = Vehicle::factory()->create(['image_path' => null]); + + livewire(EditVehicle::class, ['record' => $vehicle->getRouteKey()]) + ->fillForm([ + 'image_path' => [UploadedFile::fake()->image('gross.jpg', 4000, 3000)->size(11 * 1024)], + ]) + ->call('save') + ->assertHasFormErrors(['image_path']); +}); + +// Die zweite Haelfte des Fehlers - das Hochrechnen kleiner Bilder auf 2000 +// Pixel - passiert im Browser, in FilePondPluginImageResize. Ein Test im +// PHP-Prozess durchlaeuft diesen Schritt nie und kann das Ergebnis deshalb +// nicht pruefen. Was er pruefen kann: dass die Anweisung, es zu unterlassen, +// ueberhaupt bis zur Komponente gelangt und nicht bei einer spaeteren +// Aenderung still verlorengeht. +it('tells the browser not to upscale and not to crop', function () { + $vehicle = Vehicle::factory()->create(); + + $upload = livewire(EditVehicle::class, ['record' => $vehicle->getRouteKey()]) + ->instance() + ->getSchemaComponent('form.image_path'); + + expect($upload->shouldAutomaticallyUpscaleImagesWhenResizing())->toBeFalse() + ->and($upload->getAutomaticallyResizeImagesMode())->toBe('contain') + ->and($upload->getAutomaticallyResizeImagesWidth())->toBe('2000'); +}); + +it('still rejects a file type that is not an image', function () { + $vehicle = Vehicle::factory()->create(['image_path' => null]); + + livewire(EditVehicle::class, ['record' => $vehicle->getRouteKey()]) + ->fillForm(['image_path' => [UploadedFile::fake()->create('handbuch.pdf', 100, 'application/pdf')]]) + ->call('save') + ->assertHasFormErrors(['image_path']); +}); diff --git a/tests/Feature/Filament/VehicleResourceTest.php b/tests/Feature/Filament/VehicleResourceTest.php index 6af35cd..26ad9f3 100644 --- a/tests/Feature/Filament/VehicleResourceTest.php +++ b/tests/Feature/Filament/VehicleResourceTest.php @@ -396,14 +396,12 @@ }); it('still stores an uploaded image on the private disk, not the public one', function () { - // Belegt, dass ein Upload trotz imageEditor()/imageAspectRatio()/ + // Belegt, dass ein Upload trotz imageEditor() und // automaticallyResizeImagesToWidth() weiterhin funktioniert. Die // Verkleinerung auf 2000 Pixel selbst laeuft ausschliesslich im - // Browser (FilePondPluginImageResize, siehe Bericht) - ein - // fillForm()-Test im PHP-Prozess durchlaeuft diesen JS-Schritt nicht - // und kann seine Wirkung deshalb hier nicht belegen. 800x600 (4:3) - // besteht aber die serverseitige Rule::dimensions()->ratio()-Pruefung - // aus imageAspectRatio('4:3'), die unabhaengig vom Editor greift. + // Browser (FilePondPluginImageResize) - ein fillForm()-Test im + // PHP-Prozess durchlaeuft diesen JS-Schritt nicht und kann seine + // Wirkung deshalb hier nicht belegen. Storage::fake('local'); Storage::fake('public'); @@ -428,34 +426,31 @@ Storage::disk('public')->assertMissing($vehicle->image_path); }); - it('rejects an image that is not cropped to 4:3, even bypassing the browser editor', function () { - // Serverseitige Zweitpruefung fuer den Fall, dass ein Bild ohne - // Browser-JS eingereicht wird (siehe Kommentar in VehicleForm): - // imageAspectRatio('4:3') erzeugt Rule::dimensions()->ratio(4/3), - // unabhaengig vom clientseitigen Bildeditor. + // Dieser Test hat frueher das Gegenteil geprueft: imageAspectRatio('4:3') + // erzeugte Rule::dimensions()->ratio(4/3) und wies jedes andere Format ab. + // Das war der gemeldete Fehler - die Bildgroesse soll ausdruecklich egal + // sein, es gilt nur noch maxSize(). Die Grenzfaelle stehen in + // VehicleImageUploadTest; hier bleibt der Fall stehen, der beim Anlegen + // ueber CreateVehicle laeuft. + it('accepts a square image on creation, since no aspect ratio is enforced', function () { Storage::fake('local'); actingAs(User::factory()->admin()->withTwoFactor()->create()); - $file = UploadedFile::fake()->image('quadratisch.jpg', 800, 800); - - $component = livewire(CreateVehicle::class) + livewire(CreateVehicle::class) ->fillForm([ 'brand' => 'VW', 'model' => 'Passat', 'license_plate' => 'M-IMG 0002', 'fuel_type' => FuelType::Diesel->value, - 'image_path' => $file, + 'image_path' => UploadedFile::fake()->image('quadratisch.jpg', 800, 800), ]) ->call('create') - ->assertHasFormErrors(['image_path']); - - // Rule::dimensions() liefert die generische Laravel-Meldung - // "invalid image dimensions" statt eines einfachen "dimensions"- - // Regelnamens (siehe BaseFileUpload::getRules()) - assertHasFormErrors() - // oben belegt nur, dass ueberhaupt ein Fehler an diesem Feld haengt. - expect($component->instance()->getErrorBag()->first('data.image_path')) - ->toContain('invalid image dimensions'); + ->assertHasNoFormErrors(); + + $vehicle = Vehicle::where('license_plate', 'M-IMG 0002')->firstOrFail(); + + Storage::disk('local')->assertExists($vehicle->image_path); }); });