# 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//login-{request,code}-{360,1280}.png`, ganze Seite | BS:112-118 | | Fehlerartefakte | `build/reports/browser//.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, `
`), 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 `` (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="` – 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 `` 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 , …"` | – | 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 `` 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 `` der ComboBox | BS:243, 269, 814 | dieselben | | `[data-work-field='hours'] input` | inneres `` des `vaadin-integer-field` | BS:244, 266, 282, 328, 401, 820, 962, 1006 | dieselben | | `[data-work-field='description'] textarea` | inneres `