mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
test(export): pin Arztbericht hero render + CTA contract
Adds an SSR-only contract suite for `<ArztberichtHeroCard>` covering the hero slot, value statement, CTA touch-target floor, the `aria-describedby` link from the CTA to the value statement, the i18n key resolution path (DE + EN), the hero-gradient + glow-purple visual treatment, and the closed-state pass that keeps the dialog markup out of the SSR output until the CTA opens it. Updates the existing `<ExportSection>` smoke suite for the v1.4.37 W7a shape: four secondary cards in the grid (not five), the hero mounted above the grid, and the "Other export options" sub-heading.
This commit is contained in:
1 parent
0af77230c8
commit
002426170d
2 files changed
+155
-12
No files matched your search
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* v1.4.37 W7a — Arztbericht hero card contract.
|
||||
*
|
||||
* The flagship doctor-report PDF surface was promoted from one of five
|
||||
* grid cards into a hero card at the top of `/settings/export`. This
|
||||
* suite pins the SSR contract:
|
||||
*
|
||||
* 1. The hero renders with the expected `data-testid` slot.
|
||||
* 2. The value statement is present (Marc-Voice copy, no AI mention).
|
||||
* 3. The CTA button is present, clears the 44 px touch floor, and
|
||||
* carries an `aria-describedby` link to the value statement.
|
||||
* 4. The CTA is wired to a click handler (the prop `data-testid`
|
||||
* surface is the e2e contract).
|
||||
* 5. Both DE and EN locales resolve their copy without leaking the
|
||||
* raw i18n key.
|
||||
*
|
||||
* Project convention is SSR-only tests (no `@testing-library/react`).
|
||||
* The actual click-flow + PDF generation is exercised by the e2e
|
||||
* Playwright path and the dialog's own integration tests.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||
usePathname: () => "/settings/export",
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { ArztberichtHeroCard } from "../arztbericht-hero-card";
|
||||
|
||||
function renderSSR(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<ArztberichtHeroCard> — SSR contract", () => {
|
||||
it("renders the hero slot with eyebrow + title", () => {
|
||||
const html = renderSSR(<ArztberichtHeroCard />);
|
||||
expect(html).toContain('data-testid="export-hero-doctor-report"');
|
||||
// Title falls back to the existing doctorReport.title key — keeps
|
||||
// a single source-of-truth for the brand name.
|
||||
expect(html).toContain("Doctor Report");
|
||||
// Eyebrow surfaces the use-case framing.
|
||||
expect(html).toContain("Doctor visit");
|
||||
});
|
||||
|
||||
it("renders the value statement as a Marc-Voice one-liner", () => {
|
||||
const html = renderSSR(<ArztberichtHeroCard />);
|
||||
expect(html).toContain('data-testid="export-hero-doctor-report-value"');
|
||||
expect(html).toContain("printable PDF report");
|
||||
// No AI/Coach name-drop in the user-facing artefact (Marc-Voice).
|
||||
expect(html.toLowerCase()).not.toContain("coach");
|
||||
expect(html.toLowerCase()).not.toContain(" ai ");
|
||||
});
|
||||
|
||||
it("renders the CTA with min-h-11 touch target + aria-describedby", () => {
|
||||
const html = renderSSR(<ArztberichtHeroCard />);
|
||||
expect(html).toContain('data-testid="export-hero-doctor-report-action"');
|
||||
// 44 px touch floor (WCAG 2.5.5) — the hero CTA pins `h-11` on
|
||||
// mobile and falls back to the compact `sm:h-10` on pointer
|
||||
// devices.
|
||||
expect(html).toContain("h-11");
|
||||
// Aria-describedby links the CTA to the value statement so screen
|
||||
// readers announce the framing alongside the action label.
|
||||
expect(html).toMatch(/aria-describedby="[^"]+"/);
|
||||
});
|
||||
|
||||
it("does NOT leak raw i18n keys", () => {
|
||||
const html = renderSSR(<ArztberichtHeroCard />);
|
||||
expect(html).not.toContain("settings.sections.export.hero");
|
||||
expect(html).not.toContain("settings.sections.export.cards.doctorReport");
|
||||
});
|
||||
|
||||
it("renders German copy when the DE locale is active", () => {
|
||||
const html = renderSSR(<ArztberichtHeroCard />, "de");
|
||||
expect(html).toContain("Arztbericht");
|
||||
expect(html).toContain("Arzttermin");
|
||||
// Umlauts encoded as their literal UTF-8 character (not their HTML
|
||||
// entity), matching the rest of the codebase.
|
||||
expect(html).toContain("druckbarer PDF-Bericht");
|
||||
expect(html).not.toContain("settings.sections.export.hero");
|
||||
});
|
||||
|
||||
it("uses the hero-gradient + glow-purple visual treatment", () => {
|
||||
const html = renderSSR(<ArztberichtHeroCard />);
|
||||
// Same surface utilities as the Insights hero strip — keeps the
|
||||
// app's hero language consistent.
|
||||
expect(html).toContain("hero-gradient");
|
||||
expect(html).toContain("glow-purple");
|
||||
});
|
||||
|
||||
it("does not paint dialog markup in the closed-state SSR pass", () => {
|
||||
// Radix Dialog renders its portal content lazily, so the closed
|
||||
// SSR pass never carries `role="dialog"`. The CTA testid IS
|
||||
// present so the e2e suite can drive the open from there. Pinning
|
||||
// this contract guards against an accidental `defaultOpen={true}`
|
||||
// regression that would briefly paint the dialog above the page on
|
||||
// every cold mount.
|
||||
const html = renderSSR(<ArztberichtHeroCard />);
|
||||
expect(html).toContain('data-testid="export-hero-doctor-report-action"');
|
||||
expect(html).not.toContain('role="dialog"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("<ArztberichtHeroCard> — heading hierarchy", () => {
|
||||
it("uses an <h2> so it nests cleanly below the page-level <h1>", () => {
|
||||
const html = renderSSR(<ArztberichtHeroCard />);
|
||||
// Page-level h1 (settings-section-export-title) wraps the section;
|
||||
// hero owns the h2 slot. The export-section test pins the outer
|
||||
// h1; this assertion pins the hero's contribution.
|
||||
expect(html).toMatch(/<h2[^>]*id="export-hero-doctor-report-title"/);
|
||||
});
|
||||
});
|
||||
@@ -1,15 +1,24 @@
|
||||
/**
|
||||
* v1.4.16 Phase B7 — Settings → Export section.
|
||||
*
|
||||
* The acceptance criterion for the consolidated Export menu is "five cards":
|
||||
* 1. Doctor Report (PDF)
|
||||
* The acceptance criterion for the consolidated Export menu was "five
|
||||
* cards":
|
||||
* 1. Doctor Report (PDF) — promoted to the hero in v1.4.37 W7a;
|
||||
* now lives at the top of the page, NOT in the grid
|
||||
* 2. Measurements CSV
|
||||
* 3. Medications CSV (with optional intake-history toggle)
|
||||
* 4. Mood CSV
|
||||
* 5. Full JSON Backup
|
||||
*
|
||||
* Each card must surface a title, a 1-line description, and a download/generate
|
||||
* button. SSR-only smoke test — interaction is exercised by the e2e suite.
|
||||
* Each card must surface a title, a 1-line description, and a
|
||||
* download/generate button. SSR-only smoke test — interaction is
|
||||
* exercised by the e2e suite.
|
||||
*
|
||||
* v1.4.37 W7a — the doctor-report card was extracted into
|
||||
* `<ArztberichtHeroCard>` and rendered above the grid. The remaining
|
||||
* four cards stay under a "Weitere Export-Optionen" / "Other export
|
||||
* options" sub-heading. The hero owns its own contract test; this
|
||||
* suite pins the page-level shape.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
@@ -51,12 +60,27 @@ describe("<ExportSection> — SSR smoke", () => {
|
||||
expect(html).not.toContain("settings.sections.export.");
|
||||
});
|
||||
|
||||
it("renders exactly five export cards", () => {
|
||||
it("renders the Arztbericht hero card at the top of the page", () => {
|
||||
const html = render(<ExportSection />);
|
||||
// The hero owns its own contract suite; here we pin that the page
|
||||
// mounts it, and that the legacy in-grid doctor-report card is
|
||||
// gone (v1.4.37 W7a moved it into the hero).
|
||||
expect(html).toContain('data-testid="export-hero-doctor-report"');
|
||||
expect(html).not.toContain('data-testid="export-card-doctor-report"');
|
||||
});
|
||||
|
||||
it("renders the 'Other export options' sub-heading above the grid", () => {
|
||||
const html = render(<ExportSection />);
|
||||
expect(html).toContain("Other export options");
|
||||
expect(html).toContain('id="settings-section-export-other-title"');
|
||||
});
|
||||
|
||||
it("renders exactly four secondary export cards in the grid", () => {
|
||||
const html = render(<ExportSection />);
|
||||
// Each card carries a stable data-testid so the e2e suite can target
|
||||
// them without relying on the localised label.
|
||||
// them without relying on the localised label. The doctor-report
|
||||
// card is no longer in this grid — it's the hero above.
|
||||
const ids = [
|
||||
"export-card-doctor-report",
|
||||
"export-card-measurements-csv",
|
||||
"export-card-medications-csv",
|
||||
"export-card-mood-csv",
|
||||
@@ -75,17 +99,20 @@ describe("<ExportSection> — SSR smoke", () => {
|
||||
expect(html).toContain('data-testid="export-medications-include-intake"');
|
||||
});
|
||||
|
||||
it("renders a download/generate button per card", () => {
|
||||
it("renders a download/generate button per secondary card + the hero CTA", () => {
|
||||
const html = render(<ExportSection />);
|
||||
// Buttons are typed `button` and decorated with a stable testid so a
|
||||
// single regex finds all five — the labels themselves are i18n'd.
|
||||
// Four secondary cards each carry an `export-action-*` testid; the
|
||||
// hero owns the `export-hero-doctor-report-action` slot.
|
||||
const buttonMatches = html.match(/data-testid="export-action-/g);
|
||||
expect(buttonMatches?.length ?? 0).toBe(5);
|
||||
expect(buttonMatches?.length ?? 0).toBe(4);
|
||||
expect(html).toContain('data-testid="export-hero-doctor-report-action"');
|
||||
});
|
||||
|
||||
it("German locale renders the DE heading", () => {
|
||||
it("German locale renders the DE heading + hero copy", () => {
|
||||
const html = render(<ExportSection />, "de");
|
||||
expect(html).toContain("Export");
|
||||
expect(html).toContain("Arztbericht");
|
||||
expect(html).toContain("Weitere Export-Optionen");
|
||||
expect(html).not.toContain("settings.sections.export.");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user