Files
werkjournal/docs/plans/ui-refresh-selectors.md
2026-10-23 13:31:12 +02:00

28 KiB
Raw Permalink Blame History

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
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
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')
Text "No connection — please try again when you are online." (exakt, Geviertstrich) im frameLocator("iframe") oder auf der Seite offline.html (&mdash;), IDs #de/#en
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.