mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
fix(insights): stop the pulse chart from faking a continuous shape, add honest error states
Intraday pulse rendered sparse spot readings as a smooth interpolated
curve across hours-wide gaps, implying continuous monitoring the data
never had. The chart now grids one point per bucket, breaks the area
over any missing bucket (connectNulls={false}), and discloses coverage
("based on N readings across M hours") whenever that produces a visible
break.
Four surfaces also collapsed a failed fetch into the honest empty/no-
data state instead of surfacing the error: the Today hero, the
medication efficacy tab (previously stuck on the loading skeleton
forever on error), the intraday pulse chart, and the recent-workouts
dashboard tile. All four now branch on isError and render
QueryErrorCard with a retry action; the tile's "loading…" text line is
replaced with row-shaped skeletons.
This commit is contained in:
1 parent
fb73c97125
commit
d124a3239d
12 files changed
+176
-36
No files matched your search
@@ -32,11 +32,15 @@ let mockResult: {
|
||||
isLoading: boolean;
|
||||
isEmpty: boolean;
|
||||
error: Error | null;
|
||||
isError: boolean;
|
||||
refetch: () => void;
|
||||
} = {
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isEmpty: false,
|
||||
error: null,
|
||||
isError: false,
|
||||
refetch: () => {},
|
||||
};
|
||||
|
||||
vi.mock("@/hooks/use-workouts", async () => {
|
||||
@@ -66,15 +70,26 @@ beforeEach(() => {
|
||||
isLoading: false,
|
||||
isEmpty: false,
|
||||
error: null,
|
||||
isError: false,
|
||||
refetch: () => {},
|
||||
};
|
||||
});
|
||||
|
||||
describe("<RecentWorkoutsTile>", () => {
|
||||
it("renders the loading placeholder while the query is in flight", () => {
|
||||
it("renders row-shaped skeletons while the query is in flight", () => {
|
||||
mockResult.isLoading = true;
|
||||
const html = render();
|
||||
expect(html).toContain('data-slot="recent-workouts-loading"');
|
||||
expect(html).toContain("Loading workouts");
|
||||
expect(html).toContain('data-slot="skeleton"');
|
||||
});
|
||||
|
||||
it("renders a retry card instead of the empty state on a failed fetch", () => {
|
||||
mockResult.isError = true;
|
||||
const html = render();
|
||||
expect(html).toContain('data-slot="query-error-card"');
|
||||
// A failed fetch must never read as the honest "no workouts yet"
|
||||
// empty state — that would misreport an outage as an empty account.
|
||||
expect(html).not.toContain('data-slot="recent-workouts-empty"');
|
||||
});
|
||||
|
||||
it("renders the empty-state with the Apple-Health onboarding cue", () => {
|
||||
|
||||
@@ -17,6 +17,8 @@ import { useWorkouts, type WorkoutListEntry } from "@/hooks/use-workouts";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { getDateTimeFormat } from "@/lib/intl/formatter-cache";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { QueryErrorCard } from "@/components/ui/query-error-card";
|
||||
|
||||
/**
|
||||
* v1.4.32 — `<RecentWorkoutsTile>`.
|
||||
@@ -99,7 +101,7 @@ function renderRow(workout: WorkoutListEntry, sportName: string) {
|
||||
|
||||
export function RecentWorkoutsTile() {
|
||||
const { t, locale } = useTranslations();
|
||||
const { data, isLoading } = useWorkouts({ limit: 3 });
|
||||
const { data, isLoading, isError, refetch } = useWorkouts({ limit: 3 });
|
||||
const workouts = data?.workouts ?? [];
|
||||
|
||||
return (
|
||||
@@ -131,12 +133,24 @@ export function RecentWorkoutsTile() {
|
||||
// v1.4.43 W11-L6 — reserve roughly the loaded tile height
|
||||
// (3 workouts × ~3 rem rows + spacing) so the surrounding
|
||||
// dashboard layout doesn't reflow once the data lands.
|
||||
<p
|
||||
data-slot="recent-workouts-loading"
|
||||
className="text-muted-foreground min-h-[10rem] text-xs"
|
||||
>
|
||||
{t("dashboard.recentWorkouts.loading")}
|
||||
</p>
|
||||
// v1.29.x — row-shaped skeletons instead of a "loading…" text
|
||||
// line, matching the sport-icon + label + day + duration shape
|
||||
// the loaded rows render.
|
||||
<div data-slot="recent-workouts-loading" className="space-y-2">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div key={i} className="flex items-center gap-3 px-1 py-1">
|
||||
<Skeleton className="size-7 shrink-0 rounded-full" />
|
||||
<Skeleton className="h-4 min-w-0 flex-1" />
|
||||
<Skeleton className="h-4 w-12 shrink-0" />
|
||||
<Skeleton className="h-4 w-8 shrink-0" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : isError ? (
|
||||
<QueryErrorCard
|
||||
onRetry={() => refetch()}
|
||||
className="border-0 bg-transparent shadow-none"
|
||||
/>
|
||||
) : workouts.length === 0 ? (
|
||||
<div data-slot="recent-workouts-empty" className="space-y-1">
|
||||
<p className="text-sm">{t("dashboard.recentWorkouts.empty.title")}</p>
|
||||
|
||||
Reference in new issue
Block a user