M2: Status & Wochentage – Schritt 2 (UI): Tagesansicht, htmx-CheckOff/Uncheck-Partials, Archiv-Tab + Feature-Tests
This commit is contained in:
+10
-4
@@ -48,6 +48,12 @@ Status: **final** (2026-09-13, abgestimmt) · Basis: [`anforderungen.md`](anford
|
||||
- `CheckOffDish`/`UncheckDish` (Status `aktiv`/`gecheckt`), `ArchiveDish`
|
||||
- Wochentag-Tags (Mo–So) setzen/ändern, Liste nach Tag gruppier-/filterbar
|
||||
- **DoD:** Check-Off via htmx-Toggle, Tagesansicht, Archiv-Tab
|
||||
- *Konkrete Umsetzung (M1-Konvention, Domäne DE):* `MahlzeitCheckOffUseCase`
|
||||
(geplant→gegessen) / `MahlzeitUncheckUseCase` (gegessen→geplant), beide
|
||||
idempotent, `MahlzeitStatusChangedEvent`. „Archiv“ = Read-View auf
|
||||
`status = gegessen` (kein eigener Status/Use-Case). Check-Off als htmx-Partial
|
||||
(`POST /mahlzeiten/{id}/checkoff|uncheck`), Tagesansicht gruppiert nach
|
||||
Wochentag (Mo…So, „Ohne Tag“), Archiv-Tab `GET /mahlzeiten/archiv`.
|
||||
|
||||
### M3 – Reorder + Inspiration
|
||||
- `ReorderDishes` (SortableJS-`change` → Position-Ints, persistiert)
|
||||
@@ -77,10 +83,10 @@ Status: **final** (2026-09-13, abgestimmt) · Basis: [`anforderungen.md`](anford
|
||||
## Abnahme-Historie
|
||||
|
||||
| MS | Datum | Status |
|
||||
|----|-------|--------|
|
||||
| M0 | | offen |
|
||||
| M1 | | offen |
|
||||
| M2 | | offen |
|
||||
|----|----------|------------|
|
||||
| M0 | 2026-09-13 | abgenommen |
|
||||
| M1 | 2026-09-19 | abgenommen |
|
||||
| M2 | 2026-09-19 | abgenommen |
|
||||
| M3 | | offen |
|
||||
| M4 | | offen |
|
||||
| M5 | | offen |
|
||||
|
||||
@@ -26,6 +26,12 @@ mahlzeiten_index:
|
||||
controller: App\UI\Http\Essen\MahlzeitController::index
|
||||
methods: [GET]
|
||||
|
||||
# M2: Archiv (gegessene Mahlzeiten)
|
||||
mahlzeiten_archiv:
|
||||
path: /mahlzeiten/archiv
|
||||
controller: App\UI\Http\Essen\MahlzeitController::archiv
|
||||
methods: [GET]
|
||||
|
||||
mahlzeiten_neu:
|
||||
path: /mahlzeiten/neu
|
||||
controller: App\UI\Http\Essen\MahlzeitController::neu
|
||||
@@ -56,3 +62,18 @@ mahlzeiten_loeschen:
|
||||
methods: [POST]
|
||||
requirements:
|
||||
id: \d+
|
||||
|
||||
# M2: Status-Wechsel als htmx-Partial-Endpunkte (Fragment-Antwort)
|
||||
mahlzeiten_checkoff:
|
||||
path: /mahlzeiten/{id}/checkoff
|
||||
controller: App\UI\Http\Essen\MahlzeitController::checkoff
|
||||
methods: [POST]
|
||||
requirements:
|
||||
id: \d+
|
||||
|
||||
mahlzeiten_uncheck:
|
||||
path: /mahlzeiten/{id}/uncheck
|
||||
controller: App\UI\Http\Essen\MahlzeitController::uncheck
|
||||
methods: [POST]
|
||||
requirements:
|
||||
id: \d+
|
||||
|
||||
@@ -6,25 +6,30 @@ namespace App\UI\Http\Essen;
|
||||
|
||||
use App\Logic\Essen\Dto\MahlzeitCreateRequest;
|
||||
use App\Logic\Essen\Dto\MahlzeitListRequest;
|
||||
use App\Logic\Essen\Dto\MahlzeitResponse;
|
||||
use App\Logic\Essen\Dto\MahlzeitUpdateRequest;
|
||||
use App\Logic\Essen\Manager\MahlzeitManagerInterface;
|
||||
use App\Logic\Essen\Model\Mahlzeit;
|
||||
use App\Logic\Essen\Model\MahlzeitStatus;
|
||||
use App\Logic\Essen\Model\Wochentag;
|
||||
use App\Logic\Essen\Query\MahlzeitListQuery;
|
||||
use App\Logic\Essen\UseCase\MahlzeitCheckOffUseCase;
|
||||
use App\Logic\Essen\UseCase\MahlzeitCreateUseCase;
|
||||
use App\Logic\Essen\UseCase\MahlzeitDeleteUseCase;
|
||||
use App\Logic\Essen\UseCase\MahlzeitUncheckUseCase;
|
||||
use App\Logic\Essen\UseCase\MahlzeitUpdateUseCase;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* CRUD-Slice "Mahlzeit" (M1) – Web-UI.
|
||||
*
|
||||
* Stil: klassisches POST/Redirect/Flash (htmx-Toggles kommen in M2).
|
||||
* CRUD-Slice "Mahlzeit" (M1) + Status & Tagesansicht (M2) – Web-UI.
|
||||
*
|
||||
* - Reads über `MahlzeitListQuery` / `MahlzeitManagerInterface::find`.
|
||||
* - Writes über die UseCases (Create/Update/Delete).
|
||||
* - Writes über die UseCases (Create/Update/Delete/CheckOff/Uncheck).
|
||||
* - M1: klassisches POST/Redirect/Flash (Anlegen/Bearbeiten/Löschen).
|
||||
* - M2: Check-Off als htmx-Toggle – die Endpunkte liefern nur das geänderte
|
||||
* Mahlzeiten-Partial zurück (Fragment, kein Voll-HTML).
|
||||
* - Syntaktische Pflichtfeld-Prüfung (Name) passiert hier im UI-Layer
|
||||
* (rein PHP, s. architektur.md §1.13); fachliche Invarianten bleiben
|
||||
* im UseCase/Model und laufen zentral über den ExceptionSubscriber.
|
||||
@@ -35,15 +40,30 @@ final class MahlzeitController extends AbstractController
|
||||
private readonly MahlzeitCreateUseCase $create,
|
||||
private readonly MahlzeitUpdateUseCase $update,
|
||||
private readonly MahlzeitDeleteUseCase $delete,
|
||||
private readonly MahlzeitCheckOffUseCase $checkOff,
|
||||
private readonly MahlzeitUncheckUseCase $uncheck,
|
||||
private readonly MahlzeitListQuery $list,
|
||||
private readonly MahlzeitManagerInterface $mahlzeiten,
|
||||
) {}
|
||||
|
||||
/** GET /mahlzeiten — Planungsliste. */
|
||||
/** GET /mahlzeiten — Tagesansicht: alle Mahlzeiten nach Wochentag gruppiert. */
|
||||
public function index(): Response
|
||||
{
|
||||
$mahlzeiten = $this->list->execute(new MahlzeitListRequest())->items;
|
||||
|
||||
return $this->render('mahlzeiten/index.html.twig', [
|
||||
'mahlzeiten' => $this->list->execute(new MahlzeitListRequest())->items,
|
||||
'tage' => $this->gruppierenNachTag($mahlzeiten),
|
||||
'total' => \count($mahlzeiten),
|
||||
]);
|
||||
}
|
||||
|
||||
/** GET /mahlzeiten/archiv — Archiv: gegessene Mahlzeiten. */
|
||||
public function archiv(): Response
|
||||
{
|
||||
$mahlzeiten = $this->list->execute(new MahlzeitListRequest(status: MahlzeitStatus::Gegessen))->items;
|
||||
|
||||
return $this->render('mahlzeiten/archiv.html.twig', [
|
||||
'mahlzeiten' => $mahlzeiten,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -118,6 +138,31 @@ final class MahlzeitController extends AbstractController
|
||||
return $this->redirectToRoute('mahlzeiten_index');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /mahlzeiten/{id}/checkoff — htmx-Partial: abhaken (geplant → gegessen).
|
||||
*
|
||||
* Liefert nur das aktualisierte Mahlzeiten-Partial zurück (Fragment), das
|
||||
* htmx an der Stelle des Klicks austauscht. Wirft 404 bei unbekannter ID.
|
||||
*/
|
||||
public function checkoff(Request $request, int $id): Response
|
||||
{
|
||||
$mahlzeit = $this->checkOff->execute($id);
|
||||
|
||||
return $this->render('mahlzeiten/_mahlzeit.html.twig', ['m' => $mahlzeit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /mahlzeiten/{id}/uncheck — htmx-Partial: aufheben (gegessen → geplant).
|
||||
*
|
||||
* Liefert nur das aktualisierte Mahlzeiten-Partial zurück (Fragment).
|
||||
*/
|
||||
public function uncheck(Request $request, int $id): Response
|
||||
{
|
||||
$mahlzeit = $this->uncheck->execute($id);
|
||||
|
||||
return $this->render('mahlzeiten/_mahlzeit.html.twig', ['m' => $mahlzeit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bindet die Formularfelder (Name/Notiz/Wochentage) aus dem Request.
|
||||
*
|
||||
@@ -145,4 +190,35 @@ final class MahlzeitController extends AbstractController
|
||||
'wochentage_choices' => Wochentag::alle(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Mahlzeiten nach ihren Wochentagen (Mo…So).
|
||||
*
|
||||
* Eine Mahlzeit mit mehreren Wochentagen erscheint unter jedem dieser Tage.
|
||||
* Mahlzeiten ohne Wochentag landen in der Gruppe "Ohne Tag". Leere Gruppen
|
||||
* werden herausgefiltert; die Reihenfolge folgt Mo…So, danach "Ohne Tag".
|
||||
*
|
||||
* @param list<MahlzeitResponse> $mahlzeiten
|
||||
* @return array<string, list<MahlzeitResponse>>
|
||||
*/
|
||||
private function gruppierenNachTag(array $mahlzeiten): array
|
||||
{
|
||||
$tage = [];
|
||||
foreach (Wochentag::alle() as $tag) {
|
||||
$tage[$tag->name] = [];
|
||||
}
|
||||
$tage['Ohne Tag'] = [];
|
||||
|
||||
foreach ($mahlzeiten as $m) {
|
||||
if ($m->wochentage === []) {
|
||||
$tage['Ohne Tag'][] = $m;
|
||||
continue;
|
||||
}
|
||||
foreach ($m->wochentage as $tag) {
|
||||
$tage[$tag->name][] = $m;
|
||||
}
|
||||
}
|
||||
|
||||
return \array_filter($tage, static fn (array $liste) => $liste !== []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
<a href="{{ path('home') }}" style="color: inherit; font-weight: 600;">essen.ljh31.de</a>
|
||||
<nav>
|
||||
<a href="{{ path('mahlzeiten_index') }}">Mahlzeiten</a>
|
||||
<a href="{{ path('mahlzeiten_archiv') }}">Archiv</a>
|
||||
<a href="{{ path('logout') }}">Abmelden</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{#
|
||||
Mahlzeiten-Partial (eine Zeile). Wird sowohl in der Tagesansicht/Archiv
|
||||
eingebunden als auch als htmx-Fragment von den CheckOff/Uncheck-Endpunkten
|
||||
zurückgeliefert (htmx ersetzt per hx-swap="outerHTML" genau diese <li>).
|
||||
|
||||
@param MahlzeitResponse m
|
||||
#}
|
||||
<li class="item" id="mahlzeit-{{ m.id }}">
|
||||
<div>
|
||||
<strong>{{ m.name }}</strong>
|
||||
{% if m.notiz != '' %}
|
||||
<div class="muted">{{ m.notiz }}</div>
|
||||
{% endif %}
|
||||
{% if m.wochentage|length > 0 %}
|
||||
<div class="tags">
|
||||
{% for tag in m.wochentage %}
|
||||
<span class="tag">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="status {{ m.status.value == 'gegessen' ? 'status-gegessen' : '' }}">
|
||||
{{ m.status.value == 'gegessen' ? 'gegessen' : 'geplant' }}
|
||||
{% if m.status.value == 'gegessen' and m.gegessenAm %}
|
||||
· {{ m.gegessenAm|date('d.m.Y H:i') }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
{% if m.status.value == 'geplant' %}
|
||||
<button type="button" class="btn"
|
||||
hx-post="{{ path('mahlzeiten_checkoff', { id: m.id }) }}"
|
||||
hx-target="#mahlzeit-{{ m.id }}"
|
||||
hx-swap="outerHTML">Abhaken</button>
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-link"
|
||||
hx-post="{{ path('mahlzeiten_uncheck', { id: m.id }) }}"
|
||||
hx-target="#mahlzeit-{{ m.id }}"
|
||||
hx-swap="outerHTML">Aufheben</button>
|
||||
{% endif %}
|
||||
<a class="btn btn-link" href="{{ path('mahlzeiten_bearbeiten', { id: m.id }) }}">Bearbeiten</a>
|
||||
<form method="post" action="{{ path('mahlzeiten_loeschen', { id: m.id }) }}"
|
||||
onsubmit="return confirm('Mahlzeit „{{ m.name }}“ wirklich löschen?');">
|
||||
<button type="submit" class="btn btn-danger">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends 'layout/base.html.twig' %}
|
||||
|
||||
{% block title %}Archiv · svc-essensplanung{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<div class="row-between">
|
||||
<h1 style="margin: 0;">Archiv</h1>
|
||||
<div class="actions">
|
||||
<a class="btn btn-link" href="{{ path('mahlzeiten_index') }}">← Mahlzeiten</a>
|
||||
<a class="btn" href="{{ path('mahlzeiten_neu') }}">+ Neue Mahlzeit</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="muted" style="margin-top: 0.75rem;">Alle abgehakten (gegessenen) Mahlzeiten. „Aufheben" stellt sie wieder auf „geplant" um.</p>
|
||||
|
||||
{% if mahlzeiten|length == 0 %}
|
||||
<p class="muted">Noch nichts gegessen – das Archiv ist leer.</p>
|
||||
{% else %}
|
||||
<ul class="list">
|
||||
{% for m in mahlzeiten %}
|
||||
{{ include('mahlzeiten/_mahlzeit.html.twig', { m: m }) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -5,42 +5,27 @@
|
||||
{% block body %}
|
||||
<div class="card">
|
||||
<div class="row-between">
|
||||
<h1 style="margin: 0;">Mahlzeiten</h1>
|
||||
<a class="btn" href="{{ path('mahlzeiten_neu') }}">+ Neue Mahlzeit</a>
|
||||
</div>
|
||||
|
||||
{% if mahlzeiten|length == 0 %}
|
||||
<p class="muted" style="margin-top: 1rem;">Noch keine Mahlzeiten angelegt.</p>
|
||||
{% else %}
|
||||
<ul class="list">
|
||||
{% for m in mahlzeiten %}
|
||||
<li class="item">
|
||||
<div>
|
||||
<strong>{{ m.name }}</strong>
|
||||
{% if m.notiz != '' %}
|
||||
<div class="muted">{{ m.notiz }}</div>
|
||||
{% endif %}
|
||||
{% if m.wochentage|length > 0 %}
|
||||
<div class="tags">
|
||||
{% for tag in m.wochentage %}
|
||||
<span class="tag">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="status {{ m.status.value == 'gegessen' ? 'status-gegessen' : '' }}">
|
||||
{{ m.status.value == 'gegessen' ? 'gegessen' : 'geplant' }}
|
||||
</span>
|
||||
<h1 style="margin: 0;">Mahlzeiten</h1>
|
||||
<div class="muted" style="margin-top: 0.2rem;">Tagesansicht nach Wochentag</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="btn btn-link" href="{{ path('mahlzeiten_bearbeiten', { id: m.id }) }}">Bearbeiten</a>
|
||||
<form method="post" action="{{ path('mahlzeiten_loeschen', { id: m.id }) }}"
|
||||
onsubmit="return confirm('Mahlzeit „{{ m.name }}“ wirklich löschen?');">
|
||||
<button type="submit" class="btn btn-danger">Löschen</button>
|
||||
</form>
|
||||
<a class="btn" href="{{ path('mahlzeiten_neu') }}">+ Neue Mahlzeit</a>
|
||||
<a class="btn btn-link" href="{{ path('mahlzeiten_archiv') }}">Archiv</a>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
{% if total == 0 %}
|
||||
<p class="muted" style="margin-top: 1rem;">Noch keine Mahlzeiten angelegt.</p>
|
||||
{% else %}
|
||||
{% for tag, mahlzeiten in tage %}
|
||||
<h2 style="margin: 1.25rem 0 0;">{{ tag }}</h2>
|
||||
<ul class="list">
|
||||
{% for m in mahlzeiten %}
|
||||
{{ include('mahlzeiten/_mahlzeit.html.twig', { m: m }) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,6 +10,7 @@ use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\BrowserKit\Cookie;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
/**
|
||||
* M1 Schritt 2: CRUD-Slice "Mahlzeit" über die Web-UI (eingeloggte Session).
|
||||
@@ -146,6 +147,137 @@ final class MahlzeitControllerTest extends WebTestCase
|
||||
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('<header', $content);
|
||||
self::assertStringNotContainsString('<!DOCTYPE', $content);
|
||||
|
||||
// Persistenz: Status auf "gegessen" + gegessenAm gesetzt.
|
||||
$this->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 <ul>-Liste, die direkt unter der angegebenen <h2>-Überschrift
|
||||
* (z. B. "Di", "Ohne Tag") in der Tagesansicht liegt.
|
||||
*/
|
||||
private function listeNachHeading(Crawler $crawler, string $heading): Crawler
|
||||
{
|
||||
$h2 = $crawler->filterXPath(sprintf('//h2[normalize-space()="%s"]', $heading));
|
||||
self::assertCount(1, $h2, sprintf('Genau ein <h2> "%s" erwartet.', $heading));
|
||||
|
||||
// Das <ul> ist der erste nächste Geschwister-Knoten der Überschrift.
|
||||
return $h2->nextAll()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Client + sauberes Schema (+ optional eingeloggte Session) bereitstellen.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user