301 lines
28 KiB
Markdown
301 lines
28 KiB
Markdown
# Selektor-Inventar der Tests (Schritt 1 des UI-Refresh)
|
||
|
||
Stand 2026-09-12, erstellt aus den Test-Quellen. Alles Folgende ist der Vertrag, an den die automatisierten Suiten gebunden sind. Ein Umbau ist sicher, solange jeder Eintrag in den Spalten „Contract“ erhalten bleibt. Zitate sind `Datei:Zeile`; `BS` = `src/test/kotlin/de/javagil/werkjournal/acceptance/browser/BrowserSteps.kt`.
|
||
|
||
## 0. Wo die UI-Verträge liegen
|
||
|
||
| Suite | Datei | Umfang |
|
||
|---|---|---|
|
||
| Playwright/Cucumber Browser-Abnahme (einzige echte DOM-Suite) | `src/test/kotlin/de/javagil/werkjournal/acceptance/browser/BrowserSteps.kt` (1069 Zeilen, ~223 Locator-/Viewport-/Screenshot-Aufrufe) | alle Seiten |
|
||
| Runner und Tag-Filter | `src/test/kotlin/de/javagil/werkjournal/acceptance/BrowserCucumberTest.kt:15` | `@WJ-038 @WJ-039 @WJ-041 @WJ-059 @WJ-058 @WJ-042 @WJ-001 @WJ-005 @WJ-006 @WJ-007 @WJ-008 @WJ-040 @WJ-054 @WJ-048 @WJ-051 @WJ-015 @WJ-037 @WJ-036 @WJ-061 @WJ-062` |
|
||
| Zweite Engine (Firefox), dieselben Schritte | `build.gradle.kts:163-180` (`firefoxAcceptance`, Tag `@browser`, `-Dwerkjournal.test.browser=firefox`) | dieselben |
|
||
| Vaadin-Komponentenbaum-Assertions (ohne Browser) | `src/test/kotlin/de/javagil/werkjournal/acceptance/habits/HabitSteps.kt` | HabitCounter |
|
||
| Serverseitige HTML-String-Assertions | `LoginControllerTest.kt`, `EmailLoginHttpTest.kt`, `AccountDeletionHttpTest.kt` | Login-/Löschseiten |
|
||
| Node-JS-Unit-Tests (DOM-Stubs) | `tests/frontend/*.test.cjs`, Aufruf über `tools/test-frontend:18` | Login-Skript, Entwürfe, Push, Session |
|
||
| Wartungs-HTML-Quelle | `src/test/kotlin/de/javagil/werkjournal/acceptance/browser/DeploymentProxy.kt:22-23` extrahiert per Regex das Literal aus `scripts/deploy-prototype-remote.py:103` |
|
||
|
||
Nicht DOM-gebunden: `WorkSteps.kt`, `IdentitySteps.kt`, `PushSteps.kt`, `production/*Steps.kt`, `tests/operations/*.py`. `@WJ-033` ist in `gherkin/push-tests.feature:50` als `@browser` markiert, hat aber keine Step-Definitionen und steht in keinem Runner-Filter (derzeit tot; würde an `/work?date=…&new=true` binden).
|
||
|
||
### Globale Harness-Annahmen
|
||
|
||
| Annahme | Vertrag | Wo |
|
||
|---|---|---|
|
||
| Viewport Desktop | `1280 × 900` | BS:153 |
|
||
| Viewport Mobil | `360 × 800`, plus `isMobile` und `hasTouch` (nur Chromium; Firefox testet das schmale Layout ohne Geräteemulation) | BS:153-155 |
|
||
| Zusätzliches Resize mitten im Szenario | `page.setViewportSize(360, 800)` vor WJ-061 | BS:1023 |
|
||
| Login-Screenshots | `build/reports/browser/<chromium|firefox>/login-{request,code}-{360,1280}.png`, ganze Seite | BS:112-118 |
|
||
| Fehlerartefakte | `build/reports/browser/<browser>/<scenario>.html`, `-frames.txt`, `.png`, `-desktop.png` | BS:84-90 |
|
||
| Locale/Zeitzone | `de-DE`, `en-GB`, `fr-FR`; `Europe/Berlin`; `ColorScheme.LIGHT` | BS:102, 152-153 |
|
||
| Standard-Timeout | 15 s pro Seite | BS:103, 156 |
|
||
| Vaadin läuft im Produktionsmodus (`vaadin.productionMode=true`), Debug-DOM fehlt | BS:64 |
|
||
|
||
## 1. Login-Seite (`LoginController`, `/login`)
|
||
|
||
Quelle: `src/main/kotlin/de/javagil/werkjournal/identity/LoginController.kt:33-52`. Texte sind inline `label(de, en)`-Literale, keine Bundle-Schlüssel.
|
||
|
||
### CSS-/Attribut-Selektoren
|
||
|
||
| Selektor | Test | Szenario | Erzeugt in |
|
||
|---|---|---|---|
|
||
| `form[action='/login/start']` | BS:126, 386 | alle Browser-Szenarien (Login-Helfer) | LoginController.kt:35 |
|
||
| `form[action='/login/code']` | BS:137, 390 | alle | LoginController.kt:45 |
|
||
| `input[name='email']` (in jedem Formular) | BS:127, 138, 387, 391 | alle | LoginController.kt:37, 47 |
|
||
| `select[name='kind']`, Optionen `login` / `invitation` / `bootstrap` | BS:129 | alle Erst-Logins, WJ-062 | LoginController.kt:40 |
|
||
| `input[name='credential']` | BS:130, 131 | alle | LoginController.kt:41 |
|
||
| `input[name='code']` | BS:139, 393 | alle | LoginController.kt:48 |
|
||
| `summary` (im Start-Formular, `<details id="access-options">`), wird geklickt, um `kind`/`credential` freizulegen | BS:128 | alle | LoginController.kt:39 |
|
||
| `#access-options` muss nach einem Einladungs-Deep-Link verborgen sein | BS:388 | WJ-042, WJ-040, WJ-062 | LoginController.kt:39 + `login-invitation.js:42` |
|
||
| `html[lang]` = `de` / `en` | BS:106, 145, 177, 184, 361, 949 | WJ-038, WJ-041, WJ-062, WJ-037 | LoginController.kt:33 |
|
||
| Start-Abschnitt muss verborgen sein, sobald der Code-Schritt sichtbar ist | BS:392 | WJ-042/040/062 | LoginController.kt:35 |
|
||
| `class="brand" href="/"` und `class="site-footer"` im Markup | `EmailLoginHttpTest.kt:39-40` | – | LoginController.kt:34, `PageFooter` |
|
||
| Literale Teilstrings `id="request-step" hidden` / `id="code-step" hidden` | `LoginControllerTest.kt:30, 34, 49` | – | LoginController.kt:35, 44 |
|
||
| `name="_csrf"`, `method="post"`, escaptes Token | `LoginControllerTest.kt:19-21` | – | LoginController.kt:32, 36 |
|
||
|
||
### Playwright-Rollen-/Text-Locator
|
||
|
||
| Locator | Literal | Hinweis |
|
||
|---|---|---|
|
||
| `start.getByRole(BUTTON)` (BS:132, 389) | (ohne Namen) | Setzt genau EINEN Button im Formular von `#request-step` voraus. Ein zweiter Button bricht jedes Browser-Szenario. Das `<a class="step-link">` (LoginController.kt:43) ist nur sicher, weil es ein Anker ist. |
|
||
| `codeForm.getByRole(BUTTON)` (BS:140, 394) | (ohne Namen) | Dieselbe Ein-Button-Annahme für `#code-step`. |
|
||
| `page.contains("Anmeldecode anfordern")` | DE-Submit-Label | `LoginControllerTest.kt:23`; LoginController.kt:42 |
|
||
| `page.contains("Continue as guest")` | EN-Gastbutton | `EmailLoginHttpTest.kt:38`; LoginController.kt:51 |
|
||
| Abwesenheit von `"Google"` | – | `EmailLoginHttpTest.kt:51` |
|
||
|
||
### JS-Vertrag (Node-Tests, `tests/frontend/login-invitation.test.cjs`)
|
||
|
||
`#request-step`, `#code-step`, `#access-options`, `#invitation-notice` (`.hidden` wird geschaltet) – `login-invitation.test.cjs:11, 70-73`; `#link-login` (verstecktes Formular, `elements.token`, `requestSubmit()`) – `:13, 19, 59-64`; `#credential-label` (`.hidden` abhängig von `kind`) – `:9, 14, 30`; `document.querySelector('form')` muss das Start-Formular mit `elements.kind/credential/email` liefern – `:19`; `document.body.dataset.signedOut` – `:50`. Erzeugt von `static/login-invitation.js:2, 12-14, 23, 40-43` und `LoginController.kt:34, 38, 39, 41, 52`.
|
||
|
||
Deep-Link-Form: `"$base/login#invitation=<43 Zeichen>&email=<urlencoded>"` – BS:372, 385; die 43-Zeichen-base64url-Regex wird in `login-invitation.test.cjs:35` geprüft.
|
||
|
||
## 2. MainLayout / Shell
|
||
|
||
Kaum berührt: die Suite navigiert per URL (`$base/`, `/work`, `/habits`, `/settings`, `/reports`, `/admin`) statt über die Navigation – BS:146, 181, 241, 365, 431, 493, 558, 611, 630, 695.
|
||
|
||
| Vertrag | Test | Quelle |
|
||
|---|---|---|
|
||
| `html[data-appearance]` ∈ `dark` / `system` | BS:201, 209, 219, 225 (WJ-039) | Theme-Skript |
|
||
| Berechnete `background-color` von `html` muss sich zwischen hell/dunkel unterscheiden, und `color-scheme: dark` | BS:202, 220-221, 226-230 | `styles.css` Root-Theming |
|
||
| `vaadin-notification-card` Anzahl 0 im Offline-Dokument | BS:954 (WJ-037) | Vaadin-Notifications |
|
||
| `page.getByRole(BUTTON)` Anzahl 0 im Offline-Dokument | BS:955 | – |
|
||
| Nav-`aria-label`/`title` aus `nav.*`-Schlüsseln | (heute nicht geprüft) | `MainLayout.kt:92-99` |
|
||
|
||
## 3. Today (`TodayView`, Route `""`)
|
||
|
||
| Selektor / Locator | Literal und Schlüssel | Test | Szenario |
|
||
|---|---|---|---|
|
||
| `.journal-today .journal-heading h2` sichtbar (`atHome()`) | – (Datum des gezeigten Tages, `TodayView.kt:120-121`) | BS:211, 318, 326, 360, 603, 615, 784, 852, 893, 921, 1151, 1197, 1401, 1469 | fast jedes Browser-Szenario (Post-Login-Sentinel) |
|
||
| Überschrift `"Deine Arbeit heute"` / `"Your work, today"` (exakt) als Sprachprobe | `journal.workToday` – `messages_de.properties:142`, `messages_en.properties:142` | BS:214, 246, 580 | WJ-038, WJ-062 |
|
||
| `.journal-hours` → `"3 h"` / `"0 h"` | `today.hours={0} h` – `:23` | BS:573, 757 | WJ-007, WJ-054 |
|
||
| `.journal-total-number` → genau zwei Elemente `["3 h","3 h"]` | `today.hours` | BS:574, 758 | WJ-007, WJ-054 |
|
||
| `.journal-work-column` Anzahl 0 bei deaktiviertem Arbeitstracker | – | BS:620 | WJ-008 |
|
||
| Überschrift `"Reading"` weiterhin sichtbar | Habit-Name (Startvorgabe) | BS:621 | WJ-008 |
|
||
| `getByText("Today's entries", exact)` wird geklickt, um `<vaadin-details>` zu öffnen | `journal.todayEntries=Today''s entries` – `:143` (gerader Apostroph nach MessageFormat) | BS:1028 | WJ-061 |
|
||
| `a.journal-entry` gefiltert nach Beschreibungstext, klickbarer Anker | – | BS:1029 | WJ-061 |
|
||
| `.summary-card` (Habit-Karten, gefiltert nach `H2` = Habit-Name) | – | BS:424-425 | WJ-001, 005, 006 |
|
||
| `.wj-day-nav vaadin-date-picker` → `hasJSProperty("value", ISO-Datum)` | – (`TodayView.kt:44-63`) | BS:1388, 1393 | WJ-076 |
|
||
| `.wj-day-nav` BUTTON `"Previous day"` / `"Today"` / `"Next day"` (exakt, `aria-label`) | `work.previousDay`, `work.todayDay`, `work.nextDay` – `:213-215` | BS:1389, 1410, 1437, 1443-1444 | WJ-076 |
|
||
| Überschrift `"Your work, that day"` (exakt) statt `"Your work, today"` | `journal.workDay` – `messages_en.properties:144` | BS:1416 | WJ-076 |
|
||
| `.journal-totals` → Text `"That week"` (exakt) statt `"This week"` | `journal.dayWeek` – `messages_en.properties:147` | BS:1421 | WJ-076 |
|
||
| `.journal-entries` enthält nur die Einträge des gezeigten Tages | – | BS:1418-1419 | WJ-076 |
|
||
| `.journal-today .journal-heading h2` nennt Tag und Jahr, `.journal-today .journal-eyebrow` Anzahl 0, `"A little, every day."` Anzahl 0 | – (`TodayView.kt:120-121`) | BS:1474-1480 | WJ-077 |
|
||
| `.wj-day-nav > *` → vier Elemente mit gleicher Oberkante, das letzte höchstens 44 px breit | – (`wj-day-jump`, `styles.css:257-260`) | BS:1484-1489 | WJ-077 |
|
||
| `vaadin-date-picker-overlay` sichtbar nach Klick auf `.wj-day-nav vaadin-date-picker`, darin GRIDCELL mit `aria-label` `"1 <Monat> <Jahr>, …"` | – | BS:1494-1499 | WJ-077 |
|
||
|
||
Quellen: `ui/TodayView.kt:38` (`journal-work-column`), `:40` (`journal-total-number`), `:46` (`journal-hours`), `:52` (`journal-entry` auf einem `Anchor`), `:54-57` (`journal-entry-details`, `isOpened = true`, dann JS `opened = !matchMedia('(max-width: 600px)')`).
|
||
|
||
Strukturelle Annahme: WJ-061 setzt 360 px (BS:1023), wodurch Details eingeklappt sind; der Test klickt zuerst die Summary (BS:1028), bevor `a.journal-entry` existiert. Wird die Klappe entfernt (Spezifikation 5.2), muss BS:1028 angepasst werden: der Klick auf „Today's entries“ entfällt, `a.journal-entry` muss direkt sichtbar sein.
|
||
|
||
## 4. Arbeit (`WorkView` / `WorkForm`, Route `/work`)
|
||
|
||
### Attribut-Selektoren (der tragendste Vertrag der Suite)
|
||
|
||
| Selektor | Verwendung | Testzeilen | Szenarien |
|
||
|---|---|---|---|
|
||
| `[data-work-field='customer'] input` | inneres `<input>` der ComboBox; `.fill()`, `.press("Tab")`, `hasValue` | BS:242, 268, 796, 798, 813 | WJ-059, 058, 048, 042, 054, 051, 061, 036, 037, 015 |
|
||
| `[data-work-field='project'] input` | inneres `<input>` der ComboBox | BS:243, 269, 814 | dieselben |
|
||
| `[data-work-field='hours'] input` | inneres `<input>` des `vaadin-integer-field` | BS:244, 266, 282, 328, 401, 820, 962, 1006 | dieselben |
|
||
| `[data-work-field='description'] textarea` | inneres `<textarea>` der `vaadin-text-area`; `.fill()`, `.press("Tab")` | BS:245-246, 262, 267, 283-284, 309, 329, 400, 821, 836, 961, 1005, 1062, 1066 | dieselben |
|
||
| `[data-work-field='date']` | `hasJSProperty("value", "2026-09-01")` auf dem `vaadin-date-picker` selbst | BS:270 | WJ-059 |
|
||
| `[data-work-recovery] button` | Anzahl 0, wenn kein Entwurf angeboten wird | BS:402 | WJ-042 |
|
||
| `[data-work-discard] vaadin-icon` → `hasAttribute("icon", "vaadin:trash")` | Der Verwerfen-Button muss ein `vaadin-icon`-Kind mit `icon="vaadin:trash"` behalten | BS:1063 | WJ-061 |
|
||
| `[data-work-entry]` = UUID des Eintrags auf der gewählten Karte | BS:1033 | WJ-061 |
|
||
| `.work-entry-selected` (Kartenhülle) | `containsText`, gescopte `getByRole` | BS:1033-1034, 1039, 1057 | WJ-061 |
|
||
| `.work-entry-list` | Scope für gespeicherten Eintragstext und Löschen-Button | BS:289, 762, 776 | WJ-059, 058, 054, 061, 051 |
|
||
| `.work-day-summary .journal-hours` → `"12 h"` / `"3 h"` | BS:1050, 1053 | WJ-061 |
|
||
|
||
Quellen: `work/WorkView.kt:150` (Map `date/customer/project/hours/description` → `data-work-field`), `:152` (`data-work-discard`), `:153` (`data-work-recovery`, `.draft-recovery`), `:90` (`work-entry-list`), `:121-124` (`summary-card`, `data-work-entry`, `work-entry-selected`), `:89` (`journal-work-card work-day-summary`), `:87` (`journal-hours`). Komponententypen in `WorkView.kt:137-140`: `DatePicker`, `CatalogPicker` (zwei `ComboBox`), `IntegerField` (`min=1 max=12`, Step-Buttons), `TextArea`. Dieselben Attribute werden von `src/main/frontend/work-drafts.js:38, 63, 78, 114-115` abgefragt: Umbenennen bricht die App, nicht nur die Tests.
|
||
|
||
### Rollen-/Text-Locator
|
||
|
||
| Locator | Schlüssel | Test | Szenario |
|
||
|---|---|---|---|
|
||
| BUTTON `"Save work"` | `work.save` | BS:288, 335, 846, 978 | 059, 058, 054, 051, 061, 036, 015, 007 |
|
||
| BUTTON `"Record work"` | `work.new` | BS:565 | WJ-007 |
|
||
| BUTTON `"Edit"` | `work.edit` | BS:308, 323, 835, 859, 1057 | WJ-058, 051, 061 |
|
||
| BUTTON `"Delete"` (Liste und Dialogbestätigung) | `work.delete` | BS:762, 782 | WJ-054 |
|
||
| BUTTON `"Cancel"` (Dialog) | `work.cancel` | BS:770 | WJ-054 |
|
||
| BUTTON `"Discard draft"` | `work.discard` | BS:1064 | WJ-061 |
|
||
| BUTTON `"Close and keep draft"` | `work.closeKeepDraft` | BS:1060 | WJ-061 |
|
||
| BUTTON `"Add one hour"` / `"Subtract one hour"` (gescoped auf `.work-entry-selected`), `isEnabled`/`isDisabled` an den Grenzen 1 und 12 | `work.increase`, `work.decrease` | BS:1040-1041, 1049, 1052 | WJ-061 |
|
||
| BUTTON `"Continue with my draft"` | `work.useDraft` | BS:338 | WJ-058 |
|
||
| BUTTON `"Open draft: 2026-09-01 · 3 Hours · <description>"` exakt zusammengesetzt | `work.recoverDraft` + `work.hours=Hours`; zusammengesetzt in `work-drafts.js:95` | BS:259-261 | WJ-059 |
|
||
| BUTTON `"Create “Explicit customer”"` mit typografischen Anführungszeichen | `catalog.create=Create “{0}”` | BS:799, 809 | WJ-048 |
|
||
| HEADING `"Currently saved version"` | `work.currentVersion` | BS:324, 851, 861 | WJ-058, 051 |
|
||
| Text `"Delete this work entry?"` (exakt, in `role=dialog`) | `work.confirmDelete` | BS:764 | WJ-054 |
|
||
| Text `"No work recorded for this date."` (exakt) | `work.noEntries` | BS:783 | WJ-054 |
|
||
| Text `"The entry changed elsewhere. Your draft has been kept. Reload the current record."` (exakt) | `work.conflict` | BS:336, 850 | WJ-058, 051 |
|
||
| Text `"Your draft is unchanged. The next save will replace the version shown above."` (exakt) | `work.draftReady` | BS:339 | WJ-058 |
|
||
| Text `"Local draft restored. Review it and save explicitly."` (exakt) | `work.draftRestored` | BS:265 | WJ-059 |
|
||
| Karte `containsText("· 4 Hours")`, Format der Zeile `customer / project · N Hours` | `work.hours`; Format in `WorkView.kt:125` | BS:1043 | WJ-061 |
|
||
| BUTTON `"Mark day off"` / `"Remove day off"`; Text `"Day off. Work entries remain possible."` (exakt) | `dayOff.mark`, `dayOff.remove`, `dayOff.marked` | BS:559-560, 579 | WJ-007 |
|
||
| `getByRole(DIALOG)` Anzahl 0 nach Abbrechen | | BS:771 | WJ-054 |
|
||
|
||
Hinweis für Spezifikation 5.3: Wenn die Eintragszeile kein „Bearbeiten“-Button mehr ist, sondern die Zeile selbst das Sheet öffnet, müssen BS:308, 323, 835, 859, 1057 einen Klick auf die Zeile (oder einen weiterhin vorhandenen, ggf. visuell versteckten Button mit Namen „Edit“) verwenden. Bevorzugt: einen Icon-Button „Edit“ mit `aria-label` in der Zeile behalten. Das Format `customer / project · N Hours` (BS:1043) muss als Text der Zeile erhalten bleiben oder BS:1043 wird angepasst.
|
||
|
||
### URL- und Storage-Verträge
|
||
|
||
`"$base/work?date=2026-09-01&new=true"` (BS:241, 399, 873, 879, 960, 1001, 1018); `"$base/work?date=…"` (BS:306, 558, 752, 833); URL muss nach einem Today-Klick `entry=<uuid>` enthalten (BS:1035). localStorage-Präfix `werkjournal.work.<ownerUuid>.` (BS:866-867, 890, 921), ebenso `account-session.test.cjs:54-55, 66-69` und `login-invitation.test.cjs:44-45`.
|
||
|
||
## 5. Habits (`HabitView` / `HabitCounter` / `HabitCalendarView`, Route `/habits`)
|
||
|
||
### Karte und Zähler
|
||
|
||
| Selektor | Vertrag | Test | Szenario |
|
||
|---|---|---|---|
|
||
| `.summary-card` gefiltert nach `getByRole(HEADING, name="Stretching", exact)` | Habit-Karte muss `.summary-card` mit `H2` = Habit-Name sein | BS:424-425 | WJ-001, 005, 006 |
|
||
| `.summary-value` → `"0"`, `"1"`, `"2"` | BS:438, 443 | WJ-001, 005 |
|
||
| `.summary-value.habit-warning` (beide Klassen auf einem Element) | vorhanden/abwesend genau bei Grenzverletzung | BS:466, 471, 473 | WJ-005 |
|
||
| `.habit-warning` Anzahl 0 / `p.habit-indicator` Anzahl 0 ohne Grenzen | BS:460-461 | WJ-001 |
|
||
| `p.habit-warning` → `"Weekly maximum: 1 · 1 above maximum"`, `<p>`-Tag erforderlich | `habit.weeklyMax` + `habit.above={0} above maximum`, verbunden mit `": " … " · "` in `HabitView.kt:36` | BS:478 | WJ-005 |
|
||
| `.habit-week-total` → `"This week: 1"` / `"This week: 2"` | `habit.week=This week: {0}` | BS:450, 457, 477 | WJ-001, 005 |
|
||
| `vaadin-details-summary` in der Karte wird geklickt, um zu expandieren | BS:449 | WJ-001, 005 |
|
||
| BUTTON `"Stretching: increase by one"` (auf Karte gescoped), `isEnabled` jenseits des Maximums | `habit.increment={0}: increase by one` | BS:442, 479 | WJ-001, 005 |
|
||
| `getByRole(DIALOG)` Anzahl 0 nach Überschreiten der Grenze | BS:470 | WJ-005 |
|
||
|
||
Quellen: `HabitView.kt:24` (`summary-card`), `:36-42` (`habit-indicator`/`habit-warning`, `role="status"`), `:45` (`habit-week-total`), `:66-70` (`summary-value` + warning/indicator), `:76` (`Details(text("habit.details"))` → `vaadin-details-summary`), `:60-63` (increment/decrement `aria-label`, Buttontext `+` / `−`).
|
||
|
||
Diese Szenarien laufen auf Today (BS:418-479). Die Kachelvariante der Spezifikation 5.2 muss daher weiterhin liefern: `.summary-card` mit `H2`-Überschrift = Name, `.summary-value` mit Klasse `habit-warning` bei Grenzverletzung, `.habit-week-total` mit `habit.week`-Text, `p.habit-warning`/`p.habit-indicator` mit `role=status` und den Indikator-Texten, sowie ein `vaadin-details` mit `vaadin-details-summary`, das diese Details enthält. Empfehlung: Kachel = `summary-card wj-tile` mit `H2` (visuell als Name), `.summary-value`, Stepper, und darunter ein `vaadin-details.wj-quiet`, das die bisherigen Info-Absätze und Neu-laden enthält (eingeklappt). So bleibt der Vertrag unverändert.
|
||
|
||
### Anlage-Dialog
|
||
|
||
| Locator | Schlüssel | Test |
|
||
|---|---|---|
|
||
| BUTTON `"Create habit"` | `habit.create` | BS:432 |
|
||
| TEXTBOX `"Name"` | `catalog.name` | BS:433 |
|
||
| `getByLabel("Weekly maximum", exact)`, labelbasiert | `habit.weeklyMax` | BS:434 |
|
||
| BUTTON `"Save changes"` | `catalog.save` | BS:435 |
|
||
| BUTTON mit Habit-Namen, `aria-pressed="true"` nach Klick | Habit-Name | BS:436, 494-496 |
|
||
|
||
### Kalender (WJ-006)
|
||
|
||
| Selektor | Vertrag | Test |
|
||
|---|---|---|
|
||
| `.calendar-period`, klickbarer Button, öffnet den Monatsdialog; Text `"September 2026"` / `"August 2026"` | `HabitCalendarView.kt:59-72`, Muster `MMMM uuuu` | BS:500, 511, 528, 536 |
|
||
| `vaadin-date-picker input` `.fill("9/1/2026")` (en-GB Vaadin-Kurzformat) | | BS:501 |
|
||
| `picker.evaluate("element => element.opened")`, `[part~=toggle-button]`, `hasJSProperty("opened", false)` | DatePicker-Interna | BS:502-503, 509 |
|
||
| `getByRole(GRIDCELL, "1 September 2026, Tuesday", exact)` dann `.focus()` + `.press("Enter")` | Overlay-Grid | BS:504-508 |
|
||
| BUTTON `"Show"` | `reports.show` | BS:510 |
|
||
| `.habit-calendar-week` gefiltert nach `button[aria-label='Aug 31, 2026 · 1'], vaadin-button[aria-label='Aug 31, 2026 · 1']` (`MEDIUM`-Datum + `" · "` + Zähler) | `HabitCalendarView.kt:96` | BS:514-515 |
|
||
| `.calendar-day-count` → Array `["1".."7"]` (7 Zellen je Zeile) | `HabitCalendarView.kt:95` | BS:520 |
|
||
| `.adjacent-month .calendar-day-count` | `HabitCalendarView.kt:100` | BS:526, 530 |
|
||
| XPath-Geschwister: `following-sibling::p[1]` → `"Week total: 28"`; `following-sibling::p[2]` → `"Weekly maximum: 30"` | die beiden `<p>` müssen unmittelbare Folgegeschwister der Wochenzeile bleiben, in dieser Reihenfolge (`HabitCalendarView.kt:107-109`) | BS:521, 534, 538 |
|
||
| BUTTON `"Previous month"` / `"Next month"` | `reports.previous`, `reports.next` | BS:527, 535 |
|
||
|
||
Höchstes Risiko: BS:521/534 hängen an der rohen `<p>`-Geschwisterreihenfolge. Die Wochensumme oder Grenze in einen Container zu packen bricht WJ-006. Die Spezifikation 5.4 verlangt daher keine Umstrukturierung; nur Styling.
|
||
|
||
### Komponentenbaum (ohne Browser), `HabitSteps.kt`, `@WJ-003` / `@WJ-056`
|
||
|
||
- Increment-Button wird über `Button.text == "+"` gefunden – `HabitSteps.kt:75`
|
||
- Konfliktelement: `role="alert"`, sichtbar, Text enthält `"changed elsewhere"` – `HabitSteps.kt:94` (`HabitView.kt:55`, `habit.conflict`)
|
||
- Klasse `summary-value` auf genau einem Nachkommen – `HabitSteps.kt:95, 132`
|
||
- Genau 2 Elemente mit Klasse `habit-warning` und `role="status"`, die `"above"` / `"below"` enthalten – `HabitSteps.kt:129-131` (`HabitView.kt:37-40`). Die Kachel-Statuszeile darf also nicht zusätzlich `habit-warning` mit `role=status` tragen, oder die Zählung im Test wird angepasst.
|
||
|
||
## 6. Berichte (`ReportsView`, `/reports`), WJ-008, WJ-040
|
||
|
||
| Locator | Schlüssel | Test |
|
||
|---|---|---|
|
||
| BUTTON `"Export JSON"` (öffnet `vaadin-dialog`) | `export.title`; `ReportsView.kt:53-54` | BS:631, 696 |
|
||
| LINK `"Export month · 2026-09"` (exakt) | `export.month`; `ReportsView.kt:56` | BS:697-698 |
|
||
| LINK `"Export year · 2026"` (exakt) | `export.year`; `ReportsView.kt:57` | BS:633, 697-698 |
|
||
| Muss ein `<a>` (Rolle LINK) sein, das `waitForDownload` auslöst | | BS:632-634, 698 |
|
||
|
||
## 7. Katalog (`CatalogView`, `/catalog`)
|
||
|
||
Kein eigenes Browser-Szenario. Einziger Vertrag läuft im Arbeitsformular (WJ-048): BUTTON `"Create “Explicit customer”"` aus `CatalogPicker.kt:27` / `catalog.create` – BS:799, 809; sowie `[data-work-field='customer'|'project'] input`. `catalog-customer` + `aria-pressed` sind ungetestet.
|
||
|
||
## 8. Einstellungen (`SettingsView`, `/settings`)
|
||
|
||
| Locator | Literal (EN / DE) | Schlüssel | Test | Szenario |
|
||
|---|---|---|---|---|
|
||
| `getByText("Language", exact)` | `Language` / `Sprache` | `settings.language` | BS:147 | WJ-041 |
|
||
| `getByText("Appearance", exact)` | `Appearance` / `Darstellung` | `settings.theme` | BS:214-215 | WJ-039 |
|
||
| `choose()`-Helfer: `vaadin-select` gefiltert nach `hasText(label)` → Klick `vaadin-select-value-button` → `getByRole(OPTION, name=value, exact)` | Labels `"Sprache"`, `"Appearance"`, `"Email language"`; Werte `"English"`, `"Deutsch"`, `"Dark"`, `"Follow system"` | `SettingsView.kt:32-34`; `settings.dark`, `settings.system` | BS:159-163, 182, 203, 208, 369 | WJ-038, 039, 062 |
|
||
| BUTTON `"Einstellungen speichern"` (exakt, DE) | | `settings.save` | BS:183 | WJ-038 |
|
||
| BUTTON `"Save settings"` (exakt, EN) | | `settings.save` | BS:613 | WJ-008 |
|
||
| Text `"Settings saved."` (exakt), Vaadin-Notification | | `settings.saved` | BS:614 | WJ-008 |
|
||
| CHECKBOX `"Track work"` → `.uncheck()` | | `settings.work`; `SettingsView.kt:33, 112` | BS:612 | WJ-008 |
|
||
| `getByText("Reminder time", exact)` | `Reminder time` / `Erinnerungszeit` | `settings.reminderTime` | BS:272 | WJ-075 |
|
||
| `locator("vaadin-time-picker").locator("input")` → `.fill(...)` + `Enter`; `locator("vaadin-time-picker[invalid]")` | Werte in der Sprache des Kontos, z. B. `"8:00 PM"`, `"5:30 PM"` | `settings.reminderTime`, `settings.reminderInvalid`; `SettingsView.kt:38-42` | BS:266, 273, 278-279 | WJ-075 |
|
||
| Text `"Mon–Fri at <Zeit>,"` (Teilstring) im Geräteabschnitt | | `push-settings.js:9-11` | BS:293 | WJ-075 |
|
||
| BUTTON `"Sign out"` (exakt) | | `settings.logout` | BS:188, 349 | WJ-038, 042, 040 |
|
||
| BUTTON `"Sign out everywhere"` (exakt) | | `settings.logoutAll` | BS:897 | WJ-015 |
|
||
|
||
`choose()` (BS:160) verlangt, dass das Label im `vaadin-select`-Element steht, nicht daneben. Sign-out-Buttons verlassen sich auf `aria-label` (`SettingsView.kt:110`), das die Label-Ausblendung von `responsive-action` überlebt.
|
||
|
||
## 9. Admin (`AdminView`, `/admin`), WJ-062, WJ-042, WJ-040
|
||
|
||
Alle Texte sind `label(de, en)`-Paare in `AdminView.kt`.
|
||
|
||
| Locator | Literal | Quelle | Test |
|
||
|---|---|---|---|
|
||
| BUTTON `"Create invitation"` (Seitenebene) | | `AdminView.kt:89` | BS:366 |
|
||
| `page.getByRole(DIALOG)`, Scope für alles Folgende | `Dialog` in `AdminView.kt:27` | BS:367 |
|
||
| TEXTBOX `"Email address"` (exakt) im Dialog | `E-Mail-Adresse` / `Email address` | `AdminView.kt:29` | BS:368 |
|
||
| `choose(page, "Email language", "Deutsch"|"English")` | `Sprache der E-Mails` / `Email language` | `AdminView.kt:33-37` | BS:369 |
|
||
| Dialog-BUTTON `"Create invitation"` (exakt) | | `AdminView.kt:62` | BS:370 |
|
||
| TEXTBOX `"Invitation link"` (exakt) → `.inputValue()` endet mit `&email=bob%40example.test` | `Einladungslink` / `Invitation link` | `AdminView.kt:40`, URL `:50-51` | BS:371-372 |
|
||
| BUTTON `"Copy link"` (exakt) | `Link kopieren` / `Copy link` | `AdminView.kt:42` | BS:377 |
|
||
| Text `"Link copied."` (exakt) im Dialog | | `AdminView.kt:46` | BS:378 |
|
||
| BUTTON `"Send email"` (exakt) | `E-Mail senden` / `Send email` | `AdminView.kt:51` | BS:380 |
|
||
| Text `"Invitation submitted for delivery."` (Teilstring) | | `AdminView.kt:56` | BS:381 |
|
||
| `navigator.clipboard.writeText` wird vor Copy gepatcht | | `AdminView.kt:43-45` | BS:374-379 |
|
||
|
||
Diese Verträge betreffen das Paket `auth-flow-refresh`; im UI-Refresh bleibt `AdminView` strukturell unverändert.
|
||
|
||
## 10. Offline / Wartung
|
||
|
||
### Offline (WJ-037)
|
||
|
||
| Vertrag | Test |
|
||
|---|---|
|
||
| Echter Service Worker: `navigator.serviceWorker.controller !== null` | BS:929 |
|
||
| `GET /offline-stub.html` → 200, Body enthält `"No connection"`, Header `X-Frame-Options: SAMEORIGIN` | BS:930-933 (`AuthenticatedSecurityConfiguration.kt:66-67`) |
|
||
| `GET /login` → `X-Frame-Options: DENY` | BS:934 |
|
||
| Fallback-Erkennung: `document.querySelector('iframe') || document.getElementById('en')` | BS:944-945 |
|
||
| Text `"No connection — please try again when you are online."` (exakt, Geviertstrich) im `frameLocator("iframe")` oder auf der Seite | `offline.html` (`—`), IDs `#de`/`#en` | BS:943, 946-948 |
|
||
| `html[lang]="en"` | BS:949 |
|
||
| `vaadin-notification-card` Anzahl 0; `getByRole(BUTTON)` Anzahl 0 | BS:954-955 |
|
||
|
||
### Wartung (WJ-036)
|
||
|
||
| Vertrag | Test |
|
||
|---|---|
|
||
| In-App: Text `"Connection interrupted — during maintenance, please try again in a few minutes."` (exakt) | `connection.retry`; BS:982 |
|
||
| Öffentlich: HTTP 503, `Retry-After: 60`, `Cache-Control: no-store` | BS:987-989 |
|
||
| Öffentlicher Seitentext `"Maintenance — please try again in a few minutes."` (exakt, `<p lang="en">`) | `scripts/deploy-prototype-remote.py:103`, extrahiert von `DeploymentProxy.kt:22-23`; BS:992 |
|
||
|
||
`DeploymentProxy.kt:23` nutzt eine Regex über die Deploy-Skript-Quelle (`atomic(site / 'maintenance.html', '<einfach zitiertes Literal>')`). Diese Python-Zeile nicht umformatieren und keinen Apostroph im HTML einführen.
|
||
|
||
## 11. Querschnittliche Strukturannahmen, die ein Umbau erhalten muss
|
||
|
||
1. `actionButton`-Barrierefreiheitsnamen: `ui/ActionButton.kt:8-11` setzt sichtbaren Text, `aria-label` und Tooltip. CSS blendet `::part(label)` bei `max-width:600px`, in Containern und in `.calendar-toolbar` aus. Jedes `getByRole(BUTTON, name=…)` der mobilen Szenarien überlebt nur wegen `aria-label`.
|
||
2. Ein-Button-Formulare auf der Login-Seite (BS:132, 140, 389, 394).
|
||
3. Geschwisterreihenfolge im Habit-Kalender (`following-sibling::p[1]` / `[2]`, BS:521/534).
|
||
4. `data-work-*`-Attribute sind ein gemeinsamer App-/Test-Vertrag (`work-drafts.js:38, 63, 78, 114-115`).
|
||
5. Innere native Elemente müssen erreichbar bleiben: `… input` für ComboBox/IntegerField/DatePicker und `… textarea` für TextArea. Kein Komponententyp-Wechsel bei diesen Feldern.
|
||
6. Zusammengesetzte Strings behalten ihre Trenner: `" · "` im Wiederherstellungs-Button (`work-drafts.js:95`), in der Eintragszeile (`WorkView.kt:125`), im Kalender-`aria-label` (`HabitCalendarView.kt:96`), im Habit-Indikator (`HabitView.kt:36`) und in den Export-Links (`ReportsView.kt:56-57`).
|
||
7. Typografische Zeichen werden literal geprüft: geschweifte Anführungszeichen in `Create “{0}”`, Geviertstrich in Offline-/Wartungshinweisen, `−` (U+2212) als Dekrement-Buttontext (`HabitView.kt:57`), `''`-Escaping in `journal.todayEntries`.
|
||
8. `role="alert"` / `role="status"` auf `WorkForm.error` (`WorkView.kt:141`), `HabitCounter`-Konflikt (`HabitView.kt:55`) und Indikatoren (`HabitView.kt:42`), `AdminView`-Status (`AdminView.kt:41`), `LoginController.kt:30-31`.
|
||
9. Beide Sprachbundles werden geprüft, insbesondere `journal.todayHeading` und `settings.save`; WJ-038/062 scheitern, wenn nur eines geändert wird.
|
||
10. `vaadin-details`-Default-Open-Regel in `TodayView.kt:56` und die eingeklappten `HabitCounter`-Details (`HabitView.kt:77`) sind in Tests kodiert (BS:1028 klickt erstere; BS:449 letztere). Bei Umsetzung von 5.2 (keine Klappe für Einträge) BS:1028 anpassen.
|