- ReviewsSection (Server Component) mit Durchschnitt, zugaenglichen Sternen, Initialen statt Profilbildern, relativer Zeit, Links zur Rezension und zum Maps-Eintrag sowie Hinweis "Rezensionen von Google" - lange Texte optisch gekuerzt, "Weiterlesen" mit aria-expanded - ohne Daten ausgeblendet; in Entwicklung/Staging ein Einrichtungshinweis - Laden parallel zu loadPublicPage, Fehler blenden nur den Abschnitt aus - Links aus der API-Antwort nur zu HTTPS-Google-Hosts (safeGoogleUrl) - Linkstile und Browser-Oberflaechen (Scrollbar, Caret, Unterstreichung) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
54 lines
2.4 KiB
TypeScript
54 lines
2.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { safeGoogleUrl } from "@/lib/google-reviews";
|
|
import { formatRating, initialsOf, needsClamp, REVIEW_CLAMP_CHARS, reviewCountLabel, starsLabel } from "@/lib/review-format";
|
|
|
|
// Bewusst neutrale Platzhalternamen: keine echt wirkenden Personen.
|
|
describe("initialsOf", () => {
|
|
it("nimmt den ersten und letzten Namensteil", () => {
|
|
expect(initialsOf("Testperson Mittel Beispiel")).toBe("TB");
|
|
});
|
|
it("kommt mit einem einzelnen Namen und Sonderzeichen aus", () => {
|
|
expect(initialsOf(" ärger ")).toBe("Ä");
|
|
expect(initialsOf("(Test) Person.")).toBe("TP");
|
|
});
|
|
it("liefert ein Fragezeichen ohne verwertbare Zeichen", () => {
|
|
expect(initialsOf(" ... ")).toBe("?");
|
|
});
|
|
});
|
|
|
|
describe("Bewertungstexte", () => {
|
|
it("formatiert den Durchschnitt deutsch mit einer Nachkommastelle", () => {
|
|
expect(formatRating(4.84)).toBe("4,8");
|
|
expect(formatRating(5)).toBe("5,0");
|
|
});
|
|
it("beschreibt Sterne zugaenglich", () => {
|
|
expect(starsLabel(4)).toBe("4 von 5 Sternen");
|
|
expect(starsLabel(4.6)).toBe("4,6 von 5 Sternen");
|
|
});
|
|
it("unterscheidet Einzahl und Mehrzahl", () => {
|
|
expect(reviewCountLabel(1)).toBe("1 Rezension");
|
|
expect(reviewCountLabel(1200)).toBe("1.200 Rezensionen");
|
|
});
|
|
it("kuerzt erst oberhalb der Schwelle", () => {
|
|
expect(needsClamp("x".repeat(REVIEW_CLAMP_CHARS))).toBe(false);
|
|
expect(needsClamp("x".repeat(REVIEW_CLAMP_CHARS + 1))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("safeGoogleUrl", () => {
|
|
it("laesst nur HTTPS-Links zu Google-Hosts durch", () => {
|
|
expect(safeGoogleUrl("https://www.google.com/maps/contrib/123")).toBe("https://www.google.com/maps/contrib/123");
|
|
expect(safeGoogleUrl("https://maps.google.com/?cid=1")).toBe("https://maps.google.com/?cid=1");
|
|
expect(safeGoogleUrl("https://maps.app.goo.gl/abc")).toBe("https://maps.app.goo.gl/abc");
|
|
});
|
|
it("verwirft fremde Hosts, HTTP, Zugangsdaten und Unsinn", () => {
|
|
expect(safeGoogleUrl("https://google.com.example.org/")).toBeNull();
|
|
expect(safeGoogleUrl("https://evilgoogle.com/")).toBeNull();
|
|
expect(safeGoogleUrl("http://www.google.com/")).toBeNull();
|
|
expect(safeGoogleUrl("https://user:[email protected]/")).toBeNull();
|
|
expect(safeGoogleUrl("javascript:alert(1)")).toBeNull();
|
|
expect(safeGoogleUrl("kein link")).toBeNull();
|
|
expect(safeGoogleUrl(undefined)).toBeNull();
|
|
});
|
|
});
|