'test-sub', 'auth_name' => 'Jens', 'auth_email' => 'jens@ljh31.de', ]; private KernelBrowser $client; public function testIndexRedirectsToLoginWithoutSession(): void { $client = $this->vorbereiten(false); $client->request('GET', '/mahlzeiten'); self::assertResponseStatusCodeSame(302); self::assertResponseRedirects('/login?redirect=/mahlzeiten'); } public function testIndexRendersEmptyListForAuthedUser(): void { $client = $this->vorbereiten(); $client->request('GET', '/mahlzeiten'); self::assertResponseIsSuccessful(); self::assertSelectorTextContains('h1', 'Mahlzeiten'); self::assertSelectorTextContains('p', 'Noch keine Mahlzeiten'); } public function testNeuRendersEmptyForm(): void { $client = $this->vorbereiten(); $client->request('GET', '/mahlzeiten/neu'); self::assertResponseIsSuccessful(); self::assertSelectorExists('input[name="name"]'); self::assertSelectorExists('textarea[name="notiz"]'); self::assertSelectorExists('input[name="wochentage[]"][value="mo"]'); } public function testCreateRedirectsAndPersists(): void { $client = $this->vorbereiten(); $client->request('POST', '/mahlzeiten', [ 'name' => 'Bolognese', 'notiz' => 'mit Basilikum', 'wochentage' => ['mo', 'mi'], ]); self::assertResponseRedirects('/mahlzeiten'); $entity = $this->em()->getRepository(MahlzeitEntity::class)->findOneBy(['name' => 'Bolognese']); self::assertNotNull($entity); self::assertSame('mit Basilikum', $entity->getNotiz()); self::assertSame('mo,mi', $entity->getWochentage()); self::assertSame('geplant', $entity->getStatus()); } public function testCreateWithEmptyNameRendersFormWithError(): void { $client = $this->vorbereiten(); $client->request('POST', '/mahlzeiten', [ 'name' => ' ', 'notiz' => 'x', 'wochentage' => ['di'], ]); self::assertResponseIsSuccessful(); self::assertSelectorTextContains('div', 'Name ist ein Pflichtfeld'); self::assertCount(0, $this->em()->getRepository(MahlzeitEntity::class)->findAll()); } public function testUpdatePersistsChanges(): void { $client = $this->vorbereiten(); $id = $this->anlegen($client, 'Curry', '', []); $client->request('POST', sprintf('/mahlzeiten/%d', $id), [ 'name' => 'Currywurst', 'notiz' => 'scharf', 'wochentage' => ['do'], ]); self::assertResponseRedirects('/mahlzeiten'); $this->em()->clear(); $entity = $this->em()->getRepository(MahlzeitEntity::class)->find($id); self::assertSame('Currywurst', $entity->getName()); self::assertSame('scharf', $entity->getNotiz()); self::assertSame('do', $entity->getWochentage()); } public function testDeleteRemovesMeal(): void { $client = $this->vorbereiten(); $id = $this->anlegen($client, 'Pizza', '', []); $client->request('POST', sprintf('/mahlzeiten/%d/loeschen', $id)); self::assertResponseRedirects('/mahlzeiten'); $this->em()->clear(); self::assertNull($this->em()->getRepository(MahlzeitEntity::class)->find($id)); } public function testBearbeitenUnknownIdReturns404(): void { $client = $this->vorbereiten(); $client->request('GET', '/mahlzeiten/99999/bearbeiten'); self::assertResponseStatusCodeSame(404); } public function testIndexShowsPersistedMealWithActions(): void { $client = $this->vorbereiten(); $this->anlegen($client, 'Lasagne', 'Samstag', ['sa']); $client->request('GET', '/mahlzeiten'); self::assertResponseIsSuccessful(); self::assertSelectorTextContains('strong', 'Lasagne'); self::assertSelectorTextContains('span.tag', 'Sa'); self::assertSelectorExists('a[href$="/bearbeiten"]'); self::assertSelectorExists('form[action$="/loeschen"]'); } // ---- M2 Schritt 2: Status (Check-Off/Toggle), Tagesansicht, Archiv ---- public function testIndexGroupsMealsByWeekday(): void { $client = $this->vorbereiten(); $this->anlegen($client, 'Curry', '', ['di']); $this->anlegen($client, 'Pizza', '', ['sa']); $this->anlegen($client, 'Bolognese', '', ['mo', 'mi']); $client->request('GET', '/mahlzeiten'); $crawler = $client->getCrawler(); self::assertResponseIsSuccessful(); // Curry exakt unter "Di", nicht unter "Sa". $diUl = $this->listeNachHeading($crawler, 'Di'); self::assertStringContainsString('Curry', $diUl->html()); self::assertStringNotContainsString('Pizza', $diUl->html()); // Pizza exakt unter "Sa". $saUl = $this->listeNachHeading($crawler, 'Sa'); self::assertStringContainsString('Pizza', $saUl->html()); self::assertStringNotContainsString('Curry', $saUl->html()); // Bolognese hat zwei Wochentage → erscheint unter "Mo" UND "Mi". self::assertStringContainsString('Bolognese', $this->listeNachHeading($crawler, 'Mo')->html()); self::assertStringContainsString('Bolognese', $this->listeNachHeading($crawler, 'Mi')->html()); } public function testIndexGroupsMealWithoutWeekdayUnderOhneTag(): void { $client = $this->vorbereiten(); $this->anlegen($client, 'Reis', '', []); $client->request('GET', '/mahlzeiten'); self::assertResponseIsSuccessful(); self::assertStringContainsString('Reis', $this->listeNachHeading($client->getCrawler(), 'Ohne Tag')->html()); } public function testCheckOffReturnsGegessenPartial(): void { $client = $this->vorbereiten(); $id = $this->anlegen($client, 'Pasta', '', ['mo']); $client->request('POST', sprintf('/mahlzeiten/%d/checkoff', $id)); self::assertResponseIsSuccessful(); // htmx-Fragment: genau das Mahlzeiten-Partial, keine komplette Seite. self::assertSelectorExists('li#mahlzeit-' . $id); self::assertSelectorTextContains('span.status', 'gegessen'); $content = (string) $client->getResponse()->getContent(); self::assertStringNotContainsString('em()->clear(); $entity = $this->em()->getRepository(MahlzeitEntity::class)->find($id); self::assertSame('gegessen', $entity->getStatus()); self::assertNotNull($entity->getGegessenAm()); } public function testUncheckRestoresGeplantStatus(): void { $client = $this->vorbereiten(); $id = $this->anlegen($client, 'Suppe', '', []); $client->request('POST', sprintf('/mahlzeiten/%d/checkoff', $id)); self::assertResponseIsSuccessful(); $client->request('POST', sprintf('/mahlzeiten/%d/uncheck', $id)); self::assertResponseIsSuccessful(); self::assertSelectorExists('li#mahlzeit-' . $id); self::assertSelectorTextContains('span.status', 'geplant'); // Persistenz: Status zurück auf "geplant" + gegessenAm zurückgesetzt. $this->em()->clear(); $entity = $this->em()->getRepository(MahlzeitEntity::class)->find($id); self::assertSame('geplant', $entity->getStatus()); self::assertNull($entity->getGegessenAm()); } public function testArchivShowsOnlyGegessenMeals(): void { $client = $this->vorbereiten(); $this->anlegen($client, 'GeplanteMahlzeit', '', ['mo']); $gegessenId = $this->anlegen($client, 'GegesseneMahlzeit', '', ['di']); $client->request('POST', sprintf('/mahlzeiten/%d/checkoff', $gegessenId)); self::assertResponseIsSuccessful(); $client->request('GET', '/mahlzeiten/archiv'); self::assertResponseIsSuccessful(); self::assertSelectorTextContains('h1', 'Archiv'); // Nur die Ergebnisliste prüfen (Flash-Meldungen der Anleg-Requests ausblenden). $liste = $client->getCrawler()->filter('ul')->html(); self::assertStringContainsString('GegesseneMahlzeit', $liste); self::assertStringNotContainsString('GeplanteMahlzeit', $liste); } public function testCheckOffUnknownIdReturns404(): void { $client = $this->vorbereiten(); $client->request('POST', '/mahlzeiten/99999/checkoff'); self::assertResponseStatusCodeSame(404); } public function testUncheckUnknownIdReturns404(): void { $client = $this->vorbereiten(); $client->request('POST', '/mahlzeiten/99999/uncheck'); self::assertResponseStatusCodeSame(404); } /** * Liefert die