SRC-04: such-oberflaeche-mit-hervorhebung
Such-Oberfläche (Next.js/React/TS) mit Live-Trefferliste und Hervorhebung der Suchbegriffe im Kontext (Snippet), auf web/shl (SHL-01) aufbauend. - app/api/search/route.ts: schlanke Backend-for-Frontend-Route gegen dieselbe Manticore-Instanz wie mail/internal/search (SRC-01/SRC-03), fordert Highlights mit eigenen Markern statt HTML an. - lib/highlight.ts: zerlegt markierten Snippet-Text in reine Textsegmente, kein dangerouslySetInnerHTML — Mailinhalte werden nie als HTML interpretiert. - app/page.tsx: Sucheingabe, Trefferliste mit <mark>-Hervorhebung, verständlicher Hinweis bei leerem Ergebnis. - app/mail/[messageId]/page.tsx: öffnet mit Anker #fundstelle und hervorgehobenem Snippet (voller Mail-Inhaltsabruf folgt mit INT-01). - lib/contrast.ts: reale WCAG-2.1-Kontrastberechnung. Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-04-PRUEFPROTOKOLL.md): 1. Manueller Test gegen echten next start + live Manticore auf 192.168.1.131: Hervorhebung real bestätigt. 2. lib/highlightColors.test.ts: echte WCAG-Berechnung, Hell 14,29:1, Dunkel 6,43:1 (>= 4.5:1 AA). 3. Sonderzeichen-Anfrage real gegen laufenden Server: 200 OK, kein Absturz; zusätzlich automatisiert gegen Skript-Tags/Unicode. Kein Umbau: mail/internal/*, web/shl, web/retention-admin unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9748307f12
commit
d23438d3d0
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { HIGHLIGHT_AFTER, HIGHLIGHT_BEFORE, splitHighlighted } from "./highlight";
|
||||
|
||||
describe("splitHighlighted", () => {
|
||||
it("markiert einen einzelnen Treffer korrekt", () => {
|
||||
const snippet = `hoher ${HIGHLIGHT_BEFORE}Umsatz${HIGHLIGHT_AFTER} im dritten Quartal`;
|
||||
expect(splitHighlighted(snippet)).toEqual([
|
||||
{ text: "hoher ", matched: false },
|
||||
{ text: "Umsatz", matched: true },
|
||||
{ text: " im dritten Quartal", matched: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("markiert mehrere Treffer im selben Snippet", () => {
|
||||
const snippet = `${HIGHLIGHT_BEFORE}Umsatz${HIGHLIGHT_AFTER} und ${HIGHLIGHT_BEFORE}Umsatzwachstum${HIGHLIGHT_AFTER}`;
|
||||
const segments = splitHighlighted(snippet);
|
||||
expect(segments.filter((s) => s.matched)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("liefert unveränderten Text ohne Marker als unmarkiertes Segment", () => {
|
||||
expect(splitHighlighted("kein treffer hier")).toEqual([{ text: "kein treffer hier", matched: false }]);
|
||||
});
|
||||
|
||||
it("bricht bei leerem Snippet nicht ab", () => {
|
||||
expect(splitHighlighted("")).toEqual([]);
|
||||
});
|
||||
|
||||
// Pflichtprüfung 3: Sonderzeichen in der Suchanfrage/im indexierten Text
|
||||
// dürfen die Anzeige nicht zum Absturz bringen.
|
||||
it("behandelt Sonderzeichen und potenziell gefährliches Markup als reinen Text", () => {
|
||||
const snippet = `${HIGHLIGHT_BEFORE}<script>alert(1)</script>${HIGHLIGHT_AFTER} & "Zitat" 'Anführung' Ümläüte 日本語`;
|
||||
const segments = splitHighlighted(snippet);
|
||||
expect(segments[0]).toEqual({ text: "<script>alert(1)</script>", matched: true });
|
||||
expect(segments.map((s) => s.text).join("")).toContain("Ümläüte 日本語");
|
||||
});
|
||||
|
||||
it("behandelt ein unvollständiges Markerpaar ohne Absturz", () => {
|
||||
const snippet = `abc ${HIGHLIGHT_BEFORE}unvollständig`;
|
||||
expect(() => splitHighlighted(snippet)).not.toThrow();
|
||||
const segments = splitHighlighted(snippet);
|
||||
expect(segments.map((s) => s.text).join("")).toBe("abc unvollständig");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user