fix: correct edit reload dirty-state

This commit is contained in:
Björn Fromme
2026-07-10 14:33:09 +02:00
parent 99b6876ce2
commit 0ea43d0c6b
2 changed files with 231 additions and 5 deletions
@@ -94,7 +94,13 @@ class IndexController extends AbstractController
return $this->redirectToRoute('app_bookings');
}
$wasDirtyBeforePreselection = $this->fingerprintService->isDirty($bookingDto);
$this->bookingConfigurator->preselectDefaultServices($bookingDto);
if (false === $wasDirtyBeforePreselection) {
$bookingDto->originalFingerprint = $this->fingerprintService->generateFingerprint($bookingDto);
}
$this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT);
// Show flash message if draft was restored
@@ -142,10 +148,13 @@ class IndexController extends AbstractController
return $this->redirectToRoute('app_booking_edit', ['bookingId' => $bookingId]);
}
$isDirty = $wasDirtyBeforePreselection
|| (true === $isOverviewSubmitted && $this->fingerprintService->isDirty($bookingDto));
$bookingEditContext = $this->editContextFactory->createOverviewContext(
$bookingDto,
$bookingData,
$this->fingerprintService->isDirty($bookingDto),
$isDirty,
$isOverviewSubmitted,
$hasBlockingValidationErrors,
$missingValueLabelsByParticipantIndex,
@@ -215,6 +224,7 @@ class IndexController extends AbstractController
/** @var User $user */
$user = $this->getUser();
$this->draftService->deleteDraft($user, $bookingId);
$this->dataLoader->invalidateBookingCache($bookingId, $user);
$this->addFlash('success', 'Änderungen verworfen, Daten neu geladen');
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Tests\Controller\Booking\Edit;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Service;
use App\BusProNet\Model\Travel;
use App\BusProNet\XmlLoader\AgencyLoader;
use App\Controller\Booking\Edit\IndexController;
@@ -68,6 +69,8 @@ class IndexControllerTest extends TestCase
->method('isDirty')
->with($bookingDto)
->willReturn(true);
$fingerprintService->expects($this->never())
->method('generateFingerprint');
$dataLoader = $this->createMock(BookingEditDataLoader::class);
$dataLoader->expects($this->once())
@@ -151,6 +154,10 @@ class IndexControllerTest extends TestCase
->method('isDirty')
->with($bookingDto)
->willReturn(false);
$fingerprintService->expects($this->once())
->method('generateFingerprint')
->with($bookingDto)
->willReturn('normalized-fingerprint');
$dataLoader = $this->createMock(BookingEditDataLoader::class);
$dataLoader->expects($this->once())
@@ -225,8 +232,12 @@ class IndexControllerTest extends TestCase
->willReturn(false);
$fingerprintService = $this->createMock(BookingChangeTracker::class);
$fingerprintService->expects($this->once())
->method('isDirty')
->with($bookingDto)
->willReturn(true);
$fingerprintService->expects($this->never())
->method('isDirty');
->method('generateFingerprint');
$dataLoader = $this->createMock(BookingEditDataLoader::class);
$dataLoader->expects($this->once())
@@ -310,10 +321,14 @@ class IndexControllerTest extends TestCase
->willReturn(false);
$fingerprintService = $this->createMock(BookingChangeTracker::class);
$fingerprintService->expects($this->once())
$fingerprintService->expects($this->exactly(2))
->method('isDirty')
->with($bookingDto)
->willReturn(false);
->willReturnOnConsecutiveCalls(false, false);
$fingerprintService->expects($this->once())
->method('generateFingerprint')
->with($bookingDto)
->willReturn('normalized-fingerprint');
$dataLoader = $this->createMock(BookingEditDataLoader::class);
$dataLoader->expects($this->once())
@@ -365,6 +380,184 @@ class IndexControllerTest extends TestCase
$this->assertSame([0 => ['E-Mail']], $controller->renderParameters['bookingEditContext']->missingValueLabelsByParticipantIndex);
}
public function testIndexTreatsDefaultPreselectionOnCleanDtoAsBaselineState(): void
{
$request = Request::create('/bookings/42/edit', 'GET');
$user = $this->createUser();
$bookingDto = $this->createBookingDto();
$bookingDto->originalFingerprint = 'api-fingerprint';
$bookingDto->participants = [$this->createParticipant()];
$bookingData = $this->createBooking();
$form = $this->createMock(FormInterface::class);
$form->method('isSubmitted')->willReturn(false);
$fingerprintService = $this->createMock(BookingChangeTracker::class);
$fingerprintService->expects($this->once())
->method('isDirty')
->with($bookingDto)
->willReturn(false);
$fingerprintService->expects($this->once())
->method('generateFingerprint')
->with($bookingDto)
->willReturn('normalized-fingerprint');
$dataLoader = $this->createMock(BookingEditDataLoader::class);
$dataLoader->expects($this->once())
->method('loadFormData')
->with($request, 42, $user)
->willReturn($bookingDto);
$dataLoader->expects($this->once())
->method('fetchBookingData')
->with(42, $user)
->willReturn($bookingData);
$dataLoader->expects($this->once())
->method('isDraftRestored')
->willReturn(false);
$preselectedService = $this->createService(1001);
$bookingConfigurator = $this->createMock(BookingConfigurator::class);
$bookingConfigurator->expects($this->once())
->method('preselectDefaultServices')
->with($bookingDto)
->willReturnCallback(static function (BookingDto $dto) use ($preselectedService): void {
$dto->participants[0]->additionalServices[] = $preselectedService;
});
$contextFactory = $this->createMock(BookingEditContextFactory::class);
$contextFactory->expects($this->once())
->method('createOverviewContext')
->with($bookingDto, $bookingData, false, false, false, [])
->willReturn($this->createOverviewContext($bookingDto, $bookingData, false, false, false));
$controller = new TestableIndexController(
$dataLoader,
$this->createMock(BookingEditDraftManager::class),
$this->createMock(BookingSessionManager::class),
$fingerprintService,
$bookingConfigurator,
$contextFactory,
$this->createPreflightChecker($bookingDto),
$this->createMock(BookingEditSubmitter::class),
$user,
$form,
);
$response = $controller->index(42, $request);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame('normalized-fingerprint', $bookingDto->originalFingerprint);
$this->assertFalse($controller->renderParameters['bookingEditContext']->isDirty);
}
public function testIndexKeepsDirtyStateWhenDtoWasDirtyBeforeDefaultPreselection(): void
{
$request = Request::create('/bookings/42/edit', 'GET');
$user = $this->createUser();
$bookingDto = $this->createBookingDto();
$bookingDto->originalFingerprint = 'api-fingerprint';
$bookingDto->participants = [$this->createParticipant()];
$bookingData = $this->createBooking();
$form = $this->createMock(FormInterface::class);
$form->method('isSubmitted')->willReturn(false);
$fingerprintService = $this->createMock(BookingChangeTracker::class);
$fingerprintService->expects($this->once())
->method('isDirty')
->with($bookingDto)
->willReturn(true);
$fingerprintService->expects($this->never())
->method('generateFingerprint');
$dataLoader = $this->createMock(BookingEditDataLoader::class);
$dataLoader->expects($this->once())
->method('loadFormData')
->with($request, 42, $user)
->willReturn($bookingDto);
$dataLoader->expects($this->once())
->method('fetchBookingData')
->with(42, $user)
->willReturn($bookingData);
$dataLoader->expects($this->once())
->method('isDraftRestored')
->willReturn(false);
$preselectedService = $this->createService(1001);
$bookingConfigurator = $this->createMock(BookingConfigurator::class);
$bookingConfigurator->expects($this->once())
->method('preselectDefaultServices')
->with($bookingDto)
->willReturnCallback(static function (BookingDto $dto) use ($preselectedService): void {
$dto->participants[0]->additionalServices[] = $preselectedService;
});
$contextFactory = $this->createMock(BookingEditContextFactory::class);
$contextFactory->expects($this->once())
->method('createOverviewContext')
->with($bookingDto, $bookingData, true, false, false, [])
->willReturn($this->createOverviewContext($bookingDto, $bookingData, true, false, false));
$controller = new TestableIndexController(
$dataLoader,
$this->createMock(BookingEditDraftManager::class),
$this->createMock(BookingSessionManager::class),
$fingerprintService,
$bookingConfigurator,
$contextFactory,
$this->createPreflightChecker($bookingDto),
$this->createMock(BookingEditSubmitter::class),
$user,
$form,
);
$response = $controller->index(42, $request);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame('api-fingerprint', $bookingDto->originalFingerprint);
$this->assertTrue($controller->renderParameters['bookingEditContext']->isDirty);
}
public function testReloadFromApiClearsSessionDeletesDraftInvalidatesCacheAndRedirectsToEdit(): void
{
$request = Request::create('/bookings/42/edit/reload', 'POST');
$user = $this->createUser();
$bookingSessionService = $this->createMock(BookingSessionManager::class);
$bookingSessionService->expects($this->once())
->method('clearBookingDto')
->with($request, BookingDto::MODE_EDIT);
$draftService = $this->createMock(BookingEditDraftManager::class);
$draftService->expects($this->once())
->method('deleteDraft')
->with($user, 42);
$dataLoader = $this->createMock(BookingEditDataLoader::class);
$dataLoader->expects($this->once())
->method('invalidateBookingCache')
->with(42, $user);
$controller = new TestableIndexController(
$dataLoader,
$draftService,
$bookingSessionService,
$this->createMock(BookingChangeTracker::class),
$this->createMock(BookingConfigurator::class),
$this->createMock(BookingEditContextFactory::class),
$this->createMock(BookingEditPreFlightChecker::class),
$this->createMock(BookingEditSubmitter::class),
$user,
$this->createMock(FormInterface::class),
);
$response = $controller->reloadFromApi(42, $request);
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertSame('/bookings/42/edit', $response->headers->get('Location'));
$this->assertSame([['success', 'Änderungen verworfen, Daten neu geladen']], $controller->flashes);
}
/**
* @dataProvider submissionResultProvider
*
@@ -448,6 +641,25 @@ class IndexControllerTest extends TestCase
return new \App\Form\Model\ParticipantDto();
}
private function createService(int $id): Service
{
$service = new Service();
$service->id = $id;
return $service;
}
private function createPreflightChecker(BookingDto $bookingDto): BookingEditPreFlightChecker
{
$preflightChecker = $this->createMock(BookingEditPreFlightChecker::class);
$preflightChecker->expects($this->once())
->method('findMissingValueLabelsByParticipantIndex')
->with($bookingDto)
->willReturn([]);
return $preflightChecker;
}
private function createOverviewContext(
BookingDto $bookingDto,
Booking $bookingData,
@@ -499,8 +711,12 @@ class IndexControllerTest extends TestCase
$bookingData = $this->createBooking();
$form = $this->createMock(FormInterface::class);
$fingerprintService = $this->createMock(BookingChangeTracker::class);
$fingerprintService->expects($this->once())
->method('isDirty')
->with($bookingDto)
->willReturn(true);
$fingerprintService->expects($this->never())
->method('isDirty');
->method('generateFingerprint');
$form->expects($this->once())
->method('handleRequest')