diff --git a/gerbil-manager-web/.gitignore b/gerbil-manager-web/.gitignore index cf0e91e..0677d0d 100644 --- a/gerbil-manager-web/.gitignore +++ b/gerbil-manager-web/.gitignore @@ -25,4 +25,6 @@ dist-ssr # QA-1: Playwright-Artefakte test-results/ -playwright-report/ +playwright-report/ +blob-report/ +playwright/.cache/ diff --git a/gerbil-manager-web/e2e/mock-api.ts b/gerbil-manager-web/e2e/mock-api.ts index 2aea058..ea19acb 100644 --- a/gerbil-manager-web/e2e/mock-api.ts +++ b/gerbil-manager-web/e2e/mock-api.ts @@ -20,7 +20,9 @@ const newId = (prefix: string) => `${prefix}-e2e-${++seq}` /** Gridify-Escapes entfernen (\, vor Sonderzeichen). */ const unescapeGridify = (s: string) => s.replace(/\\(.)/g, '$1') -/** Mini-Gridify: genau die Ausdrücke, die die App baut (==, contains, ',', '|'). */ +/** Mini-Gridify: genau die Ausdrücke, die die App baut (=, contains, ',', '|'). + * Gleichheit akzeptiert '=' (kanonisch) UND '==' (alt) — POLISH-1 stellte den + * Client auf Gridifys echtes '=' um; der Mock bleibt für beide robust. */ function matchesFilter(row: Row, filter: string | null): boolean { if (!filter) return true return filter.split(',').every((andPart) => @@ -31,7 +33,7 @@ function matchesFilter(row: Row, filter: string | null): boolean { .toLowerCase() .includes(unescapeGridify(m[2]).toLowerCase()) } - m = cond.match(/^(\w+)==(.*)$/) + m = cond.match(/^(\w+)==?(.*)$/) if (m) return String(row[m[1]] ?? '') === unescapeGridify(m[2]) return true }), diff --git a/gerbil-manager-web/src/api/client.ts b/gerbil-manager-web/src/api/client.ts index 90498e0..dbddb10 100644 --- a/gerbil-manager-web/src/api/client.ts +++ b/gerbil-manager-web/src/api/client.ts @@ -13,15 +13,27 @@ export class ApiError extends Error { readonly status: number | null /** The request path that failed (for diagnostics/logging). */ readonly path?: string + /** Parsed error response body, if any (e.g. { code, ... } for 400s). */ + readonly body?: unknown - constructor(message: string, status: number | null, path?: string) { + constructor(message: string, status: number | null, path?: string, body?: unknown) { super(message) this.name = 'ApiError' this.status = status this.path = path + this.body = body } } +/** Read a `code` field off a parsed ApiError body, if present. */ +export function errorCode(err: unknown): string | null { + if (err instanceof ApiError && err.body && typeof err.body === 'object' && 'code' in err.body) { + const code = (err.body as { code: unknown }).code + return typeof code === 'string' ? code : null + } + return null +} + function errorMessageFor(status: number): string { if (status === 404) return de.api.errors.notFound if (status >= 500) return de.api.errors.server @@ -40,7 +52,14 @@ async function request(path: string, init?: RequestInit): Promise { } if (!response.ok) { - throw new ApiError(errorMessageFor(response.status), response.status, path) + // Best-effort parse of a JSON error body (e.g. { code: "InvalidParentGender" }). + let body: unknown + try { + body = await response.clone().json() + } catch { + body = undefined + } + throw new ApiError(errorMessageFor(response.status), response.status, path, body) } if (response.status === 204) { diff --git a/gerbil-manager-web/src/api/gridify.ts b/gerbil-manager-web/src/api/gridify.ts index 19bf8dd..cce4f66 100644 --- a/gerbil-manager-web/src/api/gridify.ts +++ b/gerbil-manager-web/src/api/gridify.ts @@ -47,8 +47,10 @@ export function condition(c: FilterCondition): string { const suffix = c.caseInsensitive === false ? '' : '/i' return `${c.field}=*${escaped}*${suffix}` } + // Gridify's equals operator is a single '='; callers use '==' semantically. + const op = c.op === '==' ? '=' : c.op const suffix = c.caseInsensitive ? '/i' : '' - return `${c.field}${c.op}${escaped}${suffix}` + return `${c.field}${op}${escaped}${suffix}` } /** Join conditions with AND (comma). Falsy/empty conditions are dropped. */ diff --git a/gerbil-manager-web/src/components/GroupComposer.tsx b/gerbil-manager-web/src/components/GroupComposer.tsx index 46eb221..1e143c3 100644 --- a/gerbil-manager-web/src/components/GroupComposer.tsx +++ b/gerbil-manager-web/src/components/GroupComposer.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from 'react' +import { useNavigate } from 'react-router-dom' import JSZip from 'jszip' import { de } from '../strings/de' import type { Gerbil } from '../api/types' @@ -29,6 +30,7 @@ function groupGender(animals: Gerbil[]): string { export default function GroupComposer({ groupNumber, animals, farbschlagOf }: GroupComposerProps) { const t = de.pages.abgabe + const navigate = useNavigate() const [status, setStatus] = useState('free') const [reservedName, setReservedName] = useState('') const [tagline, setTagline] = useState('') @@ -244,7 +246,13 @@ export default function GroupComposer({ groupNumber, animals, farbschlagOf }: Gr - diff --git a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx index 4a52b27..3cec5fd 100644 --- a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx +++ b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx @@ -181,7 +181,7 @@ export default function GerbilDetailPage() {

{t.detail.genotypeNotSet}

)} -

Weitere Daten

+

{t.detail.moreData}

{(['photos', 'health', 'weight'] as const).map((key) => (