Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 30 additions & 29 deletions app/Filament/Resources/Vehicles/Schemas/VehicleForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
Expand Down
80 changes: 80 additions & 0 deletions tests/Feature/Filament/VehicleImageUploadTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

use App\Filament\Resources\Vehicles\Pages\EditVehicle;
use App\Models\User;
use App\Models\Vehicle;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

use function Pest\Laravel\actingAs;
use function Pest\Livewire\livewire;

beforeEach(function () {
actingAs(User::factory()->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']);
});
41 changes: 18 additions & 23 deletions tests/Feature/Filament/VehicleResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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);
});
});

Expand Down