Parity-Regel: Uebersicht-Service-Block in companion-app portiert
Nutzer stellte klar: Panel und companion-app sollen immer denselben
funktionalen Stand haben, auch wenn eine Anfrage nur das Panel nennt -
das ist reiner Testkomfort (Docker-Instanz laesst sich am schnellsten
ansehen), keine Scope-Entscheidung. Regel als verbindlicher Absatz in
AGENTS.md verankert, direkt unter der bestehenden Maintenance-Regel.
Erste Anwendung im selben Zug: die eben im Panel gebaute
"Naechster Service"-Kachel nach companion-app/ portiert.
- daten/service.ts: neues naechsterService() als Gegenstueck zu
naechsterTermin() im Panel - waehlt ueber Oelwechsel-/Inspektions-
Prognose und die von Hand gepflegte Hauptuntersuchung hinweg den
zeitlich naechsten Termin. bisText() liefert denselben Artikel wie
ART_BIS im Panel ("bis zum Oelwechsel" / "bis zur Inspektion").
- screens/Uebersicht.tsx: die bisherigen Kacheln "Reichweite"/
"Kilometerstand" nebeneinander plus eine separate "Service"-Kachel
mit Werteliste weichen einer Reichweiten-Kachel plus einer
Service-Kachel mit .dm-serviceblock (Knopf, nur der obere Teil) und
.dm-servicezeile (reine Anzeige) darunter - dieselbe Control/Content-
Trennung wie im Panel.
- stile/screens.css: .dm-serviceblock/.dm-servicezeile ergaenzt.
Tests: 4 neue in service.test.ts (naechsterService waehlt das frueher
faellige Datum, nimmt die Hauptuntersuchung auf, liefert nichts ohne
Servicebuch/HU, bisText-Artikel je Art), 1 neuer in screens.test.tsx,
der mit einem vi.fn() als geheZu wirklich belegt, dass ein Klick auf
den Serviceblock navigiert und ein Klick auf die Kilometerstand-Zeile
es nicht tut - dafuer bekam zeige() in screens.test.tsx erst einen
injizierbaren geheZu-Parameter (vorher hart auf () => {} verdrahtet).
npm run typecheck sauber, npm run test 100/100 (von 95), npm run build
erfolgreich.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -58,6 +58,18 @@ the linked documents — this file points, it does not duplicate.
|
|||||||
> adjust the status table, bump the "Last updated" date). This file must always be written in
|
> adjust the status table, bump the "Last updated" date). This file must always be written in
|
||||||
> **English**, even though the rest of the project is German.
|
> **English**, even though the rest of the project is German.
|
||||||
|
|
||||||
|
> **Parity rule (binding, stated by the owner 2026-08-17):** the HA panel
|
||||||
|
> (`homeassistant/www/audi-dashboard-app.js`) and the companion app (`companion-app/`) must
|
||||||
|
> **always be at the same functional state**. When a request names only the panel, that is *not* a
|
||||||
|
> scoping decision — the owner simply tests there because the Docker instance makes it quick to see.
|
||||||
|
> Port every user-visible change to both in the **same session**, and say so in the report. The two
|
||||||
|
> are independent implementations (vanilla JS + shadow DOM vs. React + `@audi-dash/ui`), so a port
|
||||||
|
> is a re-implementation, not a copy: keep the *behaviour and wording* identical, let each side use
|
||||||
|
> its own idiom, and skip only what genuinely cannot apply (documented per case — e.g. panel-only
|
||||||
|
> CSS bugs, or HA-panel-only concepts like the entity Setup menu). If a port is impossible or
|
||||||
|
> unreasonably large, say so explicitly instead of leaving it silently undone. This rule exists
|
||||||
|
> because the two had already drifted six days apart once (see section A).
|
||||||
|
|
||||||
**How this file is loaded:** Claude Code does not read `AGENTS.md` natively; the root `CLAUDE.md`
|
**How this file is loaded:** Claude Code does not read `AGENTS.md` natively; the root `CLAUDE.md`
|
||||||
imports it via `@AGENTS.md` (official recommended pattern). Other agents (Codex, Cursor, Copilot)
|
imports it via `@AGENTS.md` (official recommended pattern). Other agents (Codex, Cursor, Copilot)
|
||||||
read `AGENTS.md` directly. Edit content here, not in `CLAUDE.md`.
|
read `AGENTS.md` directly. Edit content here, not in `CLAUDE.md`.
|
||||||
@@ -1459,11 +1471,29 @@ wraps the web app for iPhone; a PWA home-screen install is the accepted intermed
|
|||||||
jede Odometer-Aktualisierung verschiebt sie), das Tagesdatum also mehr Genauigkeit verspricht,
|
jede Odometer-Aktualisierung verschiebt sie), das Tagesdatum also mehr Genauigkeit verspricht,
|
||||||
als der Wert hat. Live geprüft bei 375px und 1280px: Block und Wertezeile fluchten mit der
|
als der Wert hat. Live geprüft bei 375px und 1280px: Block und Wertezeile fluchten mit der
|
||||||
Kachelkante (16→359 bzw. 564→1236), Chevron bündig rechts, Tippfläche 88px hoch, keine neuen
|
Kachelkante (16→359 bzw. 564→1236), Chevron bündig rechts, Tippfläche 88px hoch, keine neuen
|
||||||
Konsolenfehler. Deployed als Version `1787006000`, synchron in `installationspaket/`.
|
Konsolenfehler. Deployed als Version `1787007000` (zwischenzeitlich auf Nutzerwunsch von
|
||||||
Entwurf und Begründung als Artefakt: „Zwei Kacheln, drei Antworten“.
|
`.tile.flat` auf eine normale `.tile` mit Hintergrund umgestellt — `.serviceblock:active
|
||||||
**Noch offen:** die Änderung existiert nur im Panel, nicht in `companion-app/` — dort steht
|
{opacity:.55}` als Tipp-Rückmeldung ergänzt, da der übliche `.tilebtn`-Hintergrundwechsel
|
||||||
die Übersicht weiter mit dem alten Aufbau (siehe den Drift-Eintrag weiter oben; die beiden
|
hier nicht geht), synchron in `installationspaket/`. Entwurf und Begründung als Artefakt:
|
||||||
Codebasen laufen sonst wieder auseinander).
|
„Zwei Kacheln, drei Antworten“.
|
||||||
|
- [x] **Parity rule eingeführt (2026-08-17):** der Nutzer stellte klar, dass Panel und
|
||||||
|
`companion-app/` immer denselben funktionalen Stand haben sollen — auch wenn eine Anfrage nur
|
||||||
|
das Panel nennt, das ist reiner Testkomfort (Docker-Instanz), keine Scope-Entscheidung. Regel
|
||||||
|
als verbindlicher Absatz direkt unter der Maintenance-Regel oben in dieser Datei verankert.
|
||||||
|
Als erste Anwendung sofort im selben Zug die Übersicht-Neugestaltung von oben nach
|
||||||
|
`companion-app/` portiert (siehe `Uebersicht.tsx`): `naechsterService()` in `daten/service.ts`
|
||||||
|
neu — Gegenstück zu `naechsterTermin()` im Panel, wählt über Ölwechsel-/Inspektions-Prognose
|
||||||
|
und die von Hand gepflegte Hauptuntersuchung hinweg den zeitlich nächsten Termin, `bisText()`
|
||||||
|
liefert denselben Artikel wie `ART_BIS` im Panel. Die beiden bisherigen Kacheln
|
||||||
|
"Reichweite"/"Kilometerstand" nebeneinander plus eine separate "Service"-Kachel mit
|
||||||
|
Werteliste wichen einer Reichweiten-Kachel plus einer Service-Kachel mit `.dm-serviceblock`
|
||||||
|
(Knopf, nur der obere Teil) und `.dm-servicezeile` (reine Anzeige) darunter — dieselbe
|
||||||
|
Control/Content-Trennung wie im Panel. `npm run typecheck` blieb sauber, `npm run test` auf
|
||||||
|
100/100 (von 95 — vier neue Tests für `naechsterService()`/`bisText()` in `service.test.ts`,
|
||||||
|
einer in `screens.test.tsx`, der mit einem `vi.fn()` als `geheZu` wirklich belegt, dass ein
|
||||||
|
Klick auf den Serviceblock navigiert und ein Klick auf die Kilometerstand-Zeile es nicht tut
|
||||||
|
— dafür musste `zeige()` in `screens.test.tsx` erst einen injizierbaren `geheZu`-Parameter
|
||||||
|
bekommen, vorher war er hart auf `() => {}` verdrahtet), `npm run build` erfolgreich.
|
||||||
- [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) —
|
- [ ] Fix remaining documentation drift (statistics claim, README gaps, obsolete TODO comment) —
|
||||||
text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed
|
text-only changes; INSTALL.md's WLAN/TommiG1 drift and stale variable names were fixed
|
||||||
2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md`
|
2026-08-12 (see section B); `DESIGN_REVIEW_2026-08-13.md` and `REVIEW_main_2026-08-13.md`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest"
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
import { inspektionPrognose, letzterOelwechsel, oelwechselPrognose } from "./service"
|
import { bisText, inspektionPrognose, letzterOelwechsel, naechsterService, oelwechselPrognose } from "./service"
|
||||||
import type { Fahrzeug } from "./profilAdapter"
|
import type { Fahrzeug } from "./profilAdapter"
|
||||||
|
|
||||||
function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
|
function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
|
||||||
@@ -112,3 +112,40 @@ describe("inspektionPrognose", () => {
|
|||||||
expect(inspektionPrognose(fahrzeug(46000), [], jetzt)).toBeNull()
|
expect(inspektionPrognose(fahrzeug(46000), [], jetzt)).toBeNull()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("naechsterService", () => {
|
||||||
|
const jetzt = new Date("2026-08-11T12:00:00")
|
||||||
|
|
||||||
|
it("wählt über Ölwechsel und Inspektion hinweg das zeitlich frühere Datum", () => {
|
||||||
|
// Beide unabhängig mit derselben Formel berechnet, statt ein erwartetes
|
||||||
|
// Datum von Hand vorzurechnen - so bleibt der Test robust gegen spätere
|
||||||
|
// Änderungen an den Intervallen, prüft aber trotzdem die Auswahllogik.
|
||||||
|
const oel = oelwechselPrognose(fahrzeug(46000), BUCH, jetzt)
|
||||||
|
const insp = inspektionPrognose(fahrzeug(46000), BUCH, jetzt)
|
||||||
|
const erwarteteArt = oel!.datum <= insp!.datum ? "Ölwechsel" : "Inspektion"
|
||||||
|
|
||||||
|
const s = naechsterService(fahrzeug(46000), BUCH, jetzt)
|
||||||
|
expect(s).not.toBeNull()
|
||||||
|
expect(s!.art).toBe(erwarteteArt)
|
||||||
|
expect(s!.datum).toBe(erwarteteArt === "Ölwechsel" ? oel!.datum : insp!.datum)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("nimmt die Hauptuntersuchung auf, obwohl sie keine eigene Prognose hat", () => {
|
||||||
|
const f = { ...fahrzeug(46000), hu: "2026-09-01" }
|
||||||
|
const s = naechsterService(f, [], jetzt)
|
||||||
|
expect(s).not.toBeNull()
|
||||||
|
expect(s!.art).toBe("Hauptuntersuchung")
|
||||||
|
expect(s!.restKm).toBeNull()
|
||||||
|
expect(s!.datum).toBe("2026-09-01")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("liefert nichts ohne Servicebuch und ohne gepflegte Hauptuntersuchung", () => {
|
||||||
|
expect(naechsterService(fahrzeug(46000), [], jetzt)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("bisText nennt den richtigen Artikel je Art", () => {
|
||||||
|
expect(bisText("Ölwechsel")).toBe("bis zum Ölwechsel")
|
||||||
|
expect(bisText("Inspektion")).toBe("bis zur Inspektion")
|
||||||
|
expect(bisText("Hauptuntersuchung")).toBe("bis zur Hauptuntersuchung")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -120,3 +120,66 @@ export function inspektionPrognose(
|
|||||||
): Oelprognose | null {
|
): Oelprognose | null {
|
||||||
return servicePrognose(fahrzeug, letzterEintrag(buch, /inspektion/i), 30000, 24, jetzt)
|
return servicePrognose(fahrzeug, letzterEintrag(buch, /inspektion/i), 30000, 24, jetzt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------ Nächster Service */
|
||||||
|
|
||||||
|
export type Serviceart = "Ölwechsel" | "Inspektion" | "Hauptuntersuchung"
|
||||||
|
|
||||||
|
export interface NaechsterService {
|
||||||
|
art: Serviceart
|
||||||
|
/** Restkilometer bis zur Fälligkeit. null bei der Hauptuntersuchung — die
|
||||||
|
hat kein Kilometerziel, nur einen Termin. */
|
||||||
|
restKm: number | null
|
||||||
|
/** ISO-Tagesdatum der voraussichtlichen Fälligkeit. */
|
||||||
|
datum: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "der Ölwechsel", aber "die Inspektion"/"die Hauptuntersuchung" - ohne diese
|
||||||
|
Zuordnung stünde auf der Übersicht "bis zum Inspektion". Gleiche Tabelle wie
|
||||||
|
ART_BIS im Panel (audi-dashboard-app.js). */
|
||||||
|
const BIS_TEXT: Record<Serviceart, string> = {
|
||||||
|
"Ölwechsel": "bis zum Ölwechsel",
|
||||||
|
"Inspektion": "bis zur Inspektion",
|
||||||
|
"Hauptuntersuchung": "bis zur Hauptuntersuchung",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bisText(art: Serviceart): string {
|
||||||
|
return BIS_TEXT[art]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Der zeitlich nächste anstehende Service über alle drei Arten hinweg —
|
||||||
|
* Gegenstück zu `naechsterTermin()` im Panel.
|
||||||
|
*
|
||||||
|
* Ölwechsel und Inspektion kommen aus der eigenen Prognose (Servicebuch plus
|
||||||
|
* tatsächliche Fahrleistung), die Hauptuntersuchung aus dem im Profil
|
||||||
|
* gepflegten Termin: sie hat kein Kilometerziel, das Fahrzeug meldet sie nicht,
|
||||||
|
* und hochrechnen lässt sie sich auch nicht.
|
||||||
|
*/
|
||||||
|
export function naechsterService(
|
||||||
|
fahrzeug: Fahrzeug,
|
||||||
|
buch: readonly Buchhaltung[],
|
||||||
|
jetzt = new Date(),
|
||||||
|
): NaechsterService | null {
|
||||||
|
const kandidaten: NaechsterService[] = []
|
||||||
|
|
||||||
|
const oel = oelwechselPrognose(fahrzeug, buch, jetzt)
|
||||||
|
if (oel) kandidaten.push({ art: "Ölwechsel", restKm: oel.restKm, datum: oel.datum })
|
||||||
|
|
||||||
|
const insp = inspektionPrognose(fahrzeug, buch, jetzt)
|
||||||
|
if (insp) kandidaten.push({ art: "Inspektion", restKm: insp.restKm, datum: insp.datum })
|
||||||
|
|
||||||
|
// fahrzeug.hu wird von Hand gepflegt und kann leer oder unlesbar sein.
|
||||||
|
const hu = new Date(fahrzeug.hu)
|
||||||
|
if (fahrzeug.hu && !Number.isNaN(hu.getTime())) {
|
||||||
|
kandidaten.push({
|
||||||
|
art: "Hauptuntersuchung",
|
||||||
|
restKm: null,
|
||||||
|
datum: hu.toISOString().slice(0, 10),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kandidaten.length === 0) return null
|
||||||
|
// ISO-Tagesdaten, deshalb reicht der Zeichenkettenvergleich.
|
||||||
|
return kandidaten.sort((a, b) => a.datum.localeCompare(b.datum))[0] ?? null
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import { useState } from "react"
|
|||||||
import { Fig, ProgressBar, StatusRow, Tile } from "@audi-dash/ui"
|
import { Fig, ProgressBar, StatusRow, Tile } from "@audi-dash/ui"
|
||||||
|
|
||||||
import { useDaten } from "../daten/DatenKontext"
|
import { useDaten } from "../daten/DatenKontext"
|
||||||
|
import { bisText, naechsterService } from "../daten/service"
|
||||||
import { datum, datumZeit, dauer, de, deOderStrich, eur } from "../format"
|
import { datum, datumZeit, dauer, de, deOderStrich, eur } from "../format"
|
||||||
import type { SeitenName } from "../navigation"
|
import type { SeitenName } from "../navigation"
|
||||||
import { Leerzustand, NaviKachel, Wertzeile, Werteliste } from "./bausteine"
|
import { Leerzustand, NaviKachel } from "./bausteine"
|
||||||
import { Bild } from "./Bild"
|
import { Bild } from "./Bild"
|
||||||
|
import type { ServicebuchEintrag } from "./Service"
|
||||||
import { Typenschild, schildErkennen } from "./Typenschild"
|
import { Typenschild, schildErkennen } from "./Typenschild"
|
||||||
|
|
||||||
export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
|
export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string) => void }) {
|
||||||
@@ -40,6 +42,9 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
|
|||||||
.join(" ")
|
.join(" ")
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
|
const buch = (fahrzeug.service["buch"] ?? []) as ServicebuchEintrag[]
|
||||||
|
const service = naechsterService(fahrzeug, buch)
|
||||||
|
|
||||||
const letzteFahrt = fahrten
|
const letzteFahrt = fahrten
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => new Date(b.ts_start).getTime() - new Date(a.ts_start).getTime())[0]
|
.sort((a, b) => new Date(b.ts_start).getTime() - new Date(a.ts_start).getTime())[0]
|
||||||
@@ -103,62 +108,60 @@ export function Uebersicht({ geheZu }: { geheZu: (name: SeitenName, id?: string)
|
|||||||
onClick={() => geheZu("sicherheit")}
|
onClick={() => geheZu("sicherheit")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="dm-raster">
|
<Tile>
|
||||||
<Tile>
|
<span className="ads-eyebrow">Reichweite</span>
|
||||||
<span className="ads-eyebrow">Reichweite</span>
|
<Fig value={deOderStrich(fahrzeug.reichweite)} unit="km" size={40} />
|
||||||
<Fig value={deOderStrich(fahrzeug.reichweite)} unit="km" size={40} />
|
<ProgressBar percent={Math.max(0, Math.min(100, fahrzeug.tankPct))} />
|
||||||
<ProgressBar percent={Math.max(0, Math.min(100, fahrzeug.tankPct))} />
|
<div className="dm-tanktext">
|
||||||
<div className="dm-tanktext">
|
<span>{de(fahrzeug.tankPct)} % im Tank</span>
|
||||||
<span>{de(fahrzeug.tankPct)} % im Tank</span>
|
<span>
|
||||||
<span>
|
rund {de((einstellungen.tankvolumen * fahrzeug.tankPct) / 100, 1)} von{" "}
|
||||||
rund {de((einstellungen.tankvolumen * fahrzeug.tankPct) / 100, 1)} von{" "}
|
{de(einstellungen.tankvolumen)} l
|
||||||
{de(einstellungen.tankvolumen)} l
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</Tile>
|
||||||
</Tile>
|
|
||||||
|
|
||||||
<Tile>
|
{/* Kilometerstand und Service standen bis 2026-08-17 als zwei gleich
|
||||||
<span className="ads-eyebrow">Kilometerstand</span>
|
große Kacheln nebeneinander — gleiche Zahlengröße, gleiche Farbe,
|
||||||
<Fig
|
obwohl die beiden Zahlen Grundverschiedenes meinen: ein gemessener
|
||||||
value={fahrzeug.odoBekannt ? de(fahrzeug.odo) : "—"}
|
Ist-Wert gegen einen täglich schrumpfenden Countdown.
|
||||||
unit="km"
|
|
||||||
size={40}
|
|
||||||
/>
|
|
||||||
</Tile>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Service stand bis 2026-08-17 als Werteliste INNERHALB der
|
Jetzt eine führende Kennzahl, darunter der Kilometerstand als ruhige
|
||||||
Kilometerstand-Kachel - zwei Termine unter einer Überschrift, die
|
Wertezeile — Audi-CI (Hierarchie über Größe und Farbe) plus Apple HIGs
|
||||||
nichts mit ihnen zu tun hat. Apple HIG "Layout" verlangt, verwandte
|
"differentiate controls from content": nur der obere Teil ist der
|
||||||
Dinge zu gruppieren und Unverwandtes zu trennen; außerdem führte von
|
Knopf zur Service-Seite, die Kilometerstand-Zeile bleibt reine
|
||||||
hier kein Weg zur Service-Seite, obwohl es sie gibt. Jetzt eigene,
|
Anzeige. Deckungsgleich mit vHome() im Panel. */}
|
||||||
antippbare Kachel - dieselbe Auflösung wie im Panel (vHome). */}
|
<Tile>
|
||||||
<Tile variant="button" chevron onClick={() => geheZu("service")}>
|
<button
|
||||||
<span className="ads-eyebrow">Service</span>
|
type="button"
|
||||||
<Werteliste
|
className="dm-serviceblock"
|
||||||
kinder={
|
onClick={() => geheZu("service")}
|
||||||
|
>
|
||||||
|
<span className="ads-eyebrow">Nächster Service:</span>
|
||||||
|
<span className="dm-serviceblock__chevron" aria-hidden="true">
|
||||||
|
›
|
||||||
|
</span>
|
||||||
|
{service ? (
|
||||||
<>
|
<>
|
||||||
<Wertzeile
|
<Fig
|
||||||
label="Nächster Ölwechsel"
|
value={service.restKm !== null ? de(service.restKm) : datum(service.datum)}
|
||||||
wert={fahrzeug.oelwechselFaelligTs ? datum(fahrzeug.oelwechselFaelligTs) : "—"}
|
{...(service.restKm !== null ? { unit: "km" } : {})}
|
||||||
zusatz={
|
size={service.restKm !== null ? 40 : 30}
|
||||||
fahrzeug.oelwechselFaelligKm != null
|
|
||||||
? `in ${de(Math.abs(fahrzeug.oelwechselFaelligKm))} km`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Wertzeile
|
|
||||||
label="Nächste Inspektion"
|
|
||||||
wert={fahrzeug.inspektionFaelligTs ? datum(fahrzeug.inspektionFaelligTs) : "—"}
|
|
||||||
zusatz={
|
|
||||||
fahrzeug.inspektionFaelligKm != null
|
|
||||||
? `in ${de(Math.abs(fahrzeug.inspektionFaelligKm))} km`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<span className="dm-fussnote">
|
||||||
|
{service.restKm !== null
|
||||||
|
? `${bisText(service.art)} · vsl. ${datum(service.datum)}`
|
||||||
|
: bisText(service.art)}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
}
|
) : (
|
||||||
/>
|
<span className="dm-fussnote">kein Eintrag im Servicebuch</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<div className="dm-servicezeile">
|
||||||
|
<span className="dm-servicezeile__k">Kilometerstand</span>
|
||||||
|
<Fig value={fahrzeug.odoBekannt ? de(fahrzeug.odo) : "—"} unit="km" size={20} />
|
||||||
|
</div>
|
||||||
</Tile>
|
</Tile>
|
||||||
|
|
||||||
<div className="dm-raster">
|
<div className="dm-raster">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||||
import { beforeEach, describe, expect, it } from "vitest"
|
import { beforeEach, describe, expect, it, vi } from "vitest"
|
||||||
|
|
||||||
import { DatenAnbieter } from "../daten/DatenKontext"
|
import { DatenAnbieter } from "../daten/DatenKontext"
|
||||||
import { Shell } from "../Shell"
|
import { Shell } from "../Shell"
|
||||||
@@ -17,19 +17,19 @@ import { bildschirmbreiteSetzen } from "../tests/aufbau"
|
|||||||
|
|
||||||
const ALLE_SEITEN = Object.keys(TITEL) as SeitenName[]
|
const ALLE_SEITEN = Object.keys(TITEL) as SeitenName[]
|
||||||
|
|
||||||
function zeige(name: SeitenName, id?: string) {
|
function zeige(name: SeitenName, id?: string, geheZu: (name: SeitenName, id?: string) => void = () => {}) {
|
||||||
const api = beispielApi()
|
const api = beispielApi()
|
||||||
return render(
|
return render(
|
||||||
<div className="ads-root" data-theme="nacht">
|
<div className="ads-root" data-theme="nacht">
|
||||||
<DatenAnbieter api={api}>
|
<DatenAnbieter api={api}>
|
||||||
<Shell
|
<Shell
|
||||||
route={id === undefined ? { name } : { name, id }}
|
route={id === undefined ? { name } : { name, id }}
|
||||||
geheZu={() => {}}
|
geheZu={geheZu}
|
||||||
tabBeschriftung
|
tabBeschriftung
|
||||||
>
|
>
|
||||||
<SeiteFuer
|
<SeiteFuer
|
||||||
route={id === undefined ? { name } : { name, id }}
|
route={id === undefined ? { name } : { name, id }}
|
||||||
geheZu={() => {}}
|
geheZu={geheZu}
|
||||||
tabBeschriftung
|
tabBeschriftung
|
||||||
setzeTabBeschriftung={() => {}}
|
setzeTabBeschriftung={() => {}}
|
||||||
beiAbmeldung={() => {}}
|
beiAbmeldung={() => {}}
|
||||||
@@ -82,6 +82,23 @@ describe("Inhalte kommen wirklich aus den Daten", () => {
|
|||||||
expect(screen.getByText("385")).toBeTruthy()
|
expect(screen.getByText("385")).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("Übersicht: der Serviceblock navigiert, die Kilometerstand-Zeile nicht", async () => {
|
||||||
|
// Reproduziert die eigentliche HIG-Vorgabe hinter dem Layout
|
||||||
|
// ("differentiate controls from content"), nicht nur das Aussehen:
|
||||||
|
// nur der obere Teil der Kachel ist ein Knopf.
|
||||||
|
const geheZu = vi.fn()
|
||||||
|
const { container } = zeige("home", undefined, geheZu)
|
||||||
|
await waitFor(() => expect(screen.getByText("Nächster Service:")).toBeTruthy())
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("Nächster Service:"))
|
||||||
|
expect(geheZu).toHaveBeenCalledWith("service")
|
||||||
|
|
||||||
|
geheZu.mockClear()
|
||||||
|
const zeile = container.querySelector(".dm-servicezeile")
|
||||||
|
if (zeile) fireEvent.click(zeile)
|
||||||
|
expect(geheZu).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it("zeigt einen unbekannten Prüfpunkt als unbekannt, nicht als sicher", async () => {
|
it("zeigt einen unbekannten Prüfpunkt als unbekannt, nicht als sicher", async () => {
|
||||||
const { container } = zeige("sicherheit")
|
const { container } = zeige("sicherheit")
|
||||||
await waitFor(() => expect(container.querySelector(".dm-pruefliste")).not.toBeNull())
|
await waitFor(() => expect(container.querySelector(".dm-pruefliste")).not.toBeNull())
|
||||||
|
|||||||
@@ -185,6 +185,50 @@
|
|||||||
padding: var(--sp-4) var(--sp-4) var(--sp-5);
|
padding: var(--sp-4) var(--sp-4) var(--sp-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Übersicht: "Nächster Service" als tippbare Kennzahl innerhalb der Kachel,
|
||||||
|
darunter der Kilometerstand als ruhige Wertezeile. Nur der obere Teil ist
|
||||||
|
ein Knopf, damit "differentiate controls from content" (Apple HIG) gilt -
|
||||||
|
deckungsgleich mit .serviceblock/.servicezeile im Panel
|
||||||
|
(audi-dashboard.css). */
|
||||||
|
.dm-serviceblock {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.dm-serviceblock:active {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
.dm-serviceblock__chevron {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--fg3);
|
||||||
|
}
|
||||||
|
.dm-servicezeile {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--sp-4);
|
||||||
|
margin-top: var(--sp-3);
|
||||||
|
padding-top: 13px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.dm-servicezeile__k {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--fg2);
|
||||||
|
}
|
||||||
|
|
||||||
.dm-tanktext {
|
.dm-tanktext {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
Reference in New Issue
Block a user