28 KiB
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-valueauf genau einem Nachkommen –HabitSteps.kt:95, 132 - Genau 2 Elemente mit Klasse
habit-warningundrole="status", die"above"/"below"enthalten –HabitSteps.kt:129-131(HabitView.kt:37-40). Die Kachel-Statuszeile darf also nicht zusätzlichhabit-warningmitrole=statustragen, 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 (—), 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
actionButton-Barrierefreiheitsnamen:ui/ActionButton.kt:8-11setzt sichtbaren Text,aria-labelund Tooltip. CSS blendet::part(label)beimax-width:600px, in Containern und in.calendar-toolbaraus. JedesgetByRole(BUTTON, name=…)der mobilen Szenarien überlebt nur wegenaria-label.- Ein-Button-Formulare auf der Login-Seite (BS:132, 140, 389, 394).
- Geschwisterreihenfolge im Habit-Kalender (
following-sibling::p[1]/[2], BS:521/534). data-work-*-Attribute sind ein gemeinsamer App-/Test-Vertrag (work-drafts.js:38, 63, 78, 114-115).- Innere native Elemente müssen erreichbar bleiben:
… inputfür ComboBox/IntegerField/DatePicker und… textareafür TextArea. Kein Komponententyp-Wechsel bei diesen Feldern. - 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). - 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 injournal.todayEntries. role="alert"/role="status"aufWorkForm.error(WorkView.kt:141),HabitCounter-Konflikt (HabitView.kt:55) und Indikatoren (HabitView.kt:42),AdminView-Status (AdminView.kt:41),LoginController.kt:30-31.- Beide Sprachbundles werden geprüft, insbesondere
journal.todayHeadingundsettings.save; WJ-038/062 scheitern, wenn nur eines geändert wird. vaadin-details-Default-Open-Regel inTodayView.kt:56und die eingeklapptenHabitCounter-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.