diff --git a/src/components/insights/__tests__/insight-advisor-card.test.tsx b/src/components/insights/__tests__/insight-advisor-card.test.tsx index b7ecdd98e..8b3ebaa8a 100644 --- a/src/components/insights/__tests__/insight-advisor-card.test.tsx +++ b/src/components/insights/__tests__/insight-advisor-card.test.tsx @@ -221,3 +221,63 @@ describe(" — summary typography polish (B1b)", () => { ); }); }); + +describe(" — polished loading / empty / error states (B1b)", () => { + it("loading state shows a skeleton that mirrors the final layout", () => { + const html = renderToStaticMarkup( + + + , + ); + // A discoverable skeleton slot replaces the spinner-only loading state. + expect(html).toMatch(/data-slot="insight-skeleton"/); + // 3 placeholder rec rows match the final card grid (so the page + // doesn't visually jump when content lands). + expect(html).toMatch(/data-slot="insight-skeleton-card"/); + }); + + it("empty state shows a friendly illustration + CTA", () => { + const html = renderToStaticMarkup( + + {}} + /> + , + ); + expect(html).toMatch(/data-slot="insight-empty-state"/); + // The Start-Analysis button stays — that's the CTA. + expect(html).toContain("Start analysis"); + }); + + it("error state surfaces a retry button when onRegenerate is provided", () => { + const html = renderToStaticMarkup( + + {}} + /> + , + ); + expect(html).toMatch(/data-slot="insight-error-state"/); + expect(html).toContain("Something went wrong"); + expect(html).toMatch(/data-slot="insight-retry-button"/); + }); + + it("error state without onRegenerate hides the retry button", () => { + const html = renderToStaticMarkup( + + + , + ); + expect(html).toContain("No retry available"); + expect(html).not.toContain('data-slot="insight-retry-button"'); + }); +}); diff --git a/src/components/insights/insight-advisor-card.tsx b/src/components/insights/insight-advisor-card.tsx index 36753b40e..ca19b655c 100644 --- a/src/components/insights/insight-advisor-card.tsx +++ b/src/components/insights/insight-advisor-card.tsx @@ -317,14 +317,36 @@ export function InsightAdvisorCard({ : CLASSIFICATION_STYLES.gut; // ── Loading State ───────────────────────────────────── + // v1.4.16 phase B1b — skeleton mirrors the final card layout (header + // pulse + 3 placeholder rec rows) so the page doesn't visually jump + // when content lands. Animation keys off Tailwind `animate-pulse`, + // which `prefers-reduced-motion: reduce` already silences via the + // `motion-reduce:animate-none` utility on the same elements. if (loading && !insight) { return ( - - - - - {t("insights.generating")} - + + +
+ {icon ?? } + + {t("insights.aiAnalysisTitle")} + +
+

{title}

+
+ +
+
+
+ {[0, 1, 2].map((i) => ( +
+ ))} +
); @@ -347,14 +369,45 @@ export function InsightAdvisorCard({ {error && ( -
- - {error} +
+
+ + {error} +
+ {onRegenerate && ( + + )}
)} {!error && ( -
-

+

+
+
+

{t("insights.noAnalysisYet")}

{onRegenerate && (