mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
fix(symptoms): show the load error in the quick entry instead of the empty hint
This commit is contained in:
1 parent
d644db1975
commit
1eab6145a9
2 files changed
+119
-77
No files matched your search
@@ -7,13 +7,16 @@
|
||||
* - drop the `resolvedAt === null` filter on the episodes → "offers no
|
||||
* episode selector when every episode is resolved" goes red;
|
||||
* - render the add chip unconditionally → "a delegate who may log but not
|
||||
* manage gets no add chip" goes red.
|
||||
* manage gets no add chip" goes red;
|
||||
* - drop the `definitions.isError` branch → "a failed read shows the error
|
||||
* row, not the empty hint" goes red (the empty hint renders instead).
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
canManage: true,
|
||||
definitionsFailed: false,
|
||||
episodes: [] as Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -34,30 +37,42 @@ vi.mock("@/components/illness/use-illness", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../use-symptoms", () => ({
|
||||
useSymptomDefinitions: () => ({
|
||||
isLoading: false,
|
||||
data: {
|
||||
limit: 8,
|
||||
definitions: [
|
||||
{
|
||||
id: "d1",
|
||||
label: "Aura",
|
||||
icon: "Zap",
|
||||
sortOrder: 0,
|
||||
isActive: true,
|
||||
recent: { count30d: 0, maxIntensity30d: null, lastOccurredAt: null },
|
||||
useSymptomDefinitions: () =>
|
||||
state.definitionsFailed
|
||||
? { isLoading: false, isError: true, data: undefined, refetch: vi.fn() }
|
||||
: {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: {
|
||||
limit: 8,
|
||||
definitions: [
|
||||
{
|
||||
id: "d1",
|
||||
label: "Aura",
|
||||
icon: "Zap",
|
||||
sortOrder: 0,
|
||||
isActive: true,
|
||||
recent: {
|
||||
count30d: 0,
|
||||
maxIntensity30d: null,
|
||||
lastOccurredAt: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d2",
|
||||
label: "Headache",
|
||||
icon: "Brain",
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
recent: {
|
||||
count30d: 0,
|
||||
maxIntensity30d: null,
|
||||
lastOccurredAt: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "d2",
|
||||
label: "Headache",
|
||||
icon: "Brain",
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
recent: { count30d: 0, maxIntensity30d: null, lastOccurredAt: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
useLogSymptomEvent: () => ({ mutateAsync: vi.fn(), isPending: false }),
|
||||
useCreateSymptomDefinition: () => ({
|
||||
mutateAsync: vi.fn(),
|
||||
@@ -111,4 +126,19 @@ describe("<SymptomEntryForm>", () => {
|
||||
];
|
||||
expect(render()).not.toContain("-episode");
|
||||
});
|
||||
|
||||
it("a failed read shows the error row, not the empty hint", () => {
|
||||
state.canManage = true;
|
||||
state.episodes = [];
|
||||
state.definitionsFailed = true;
|
||||
try {
|
||||
const html = render();
|
||||
expect(html).toContain('data-slot="query-error-row"');
|
||||
expect(html).toContain("load your symptoms");
|
||||
expect(html).not.toContain("first symptom");
|
||||
expect(html).not.toContain('data-testid="symptom-add-chip"');
|
||||
} finally {
|
||||
state.definitionsFailed = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { FieldGroup } from "@/components/ui/field-group";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { QueryErrorRow } from "@/components/ui/query-error-row";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -210,59 +211,70 @@ export function SymptomEntryForm({
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Label id={symptomGroupId}>{t("symptoms.entry.symptom")}</Label>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-labelledby={symptomGroupId}
|
||||
aria-describedby={
|
||||
symptomError ? `${symptomGroupId}-error` : undefined
|
||||
}
|
||||
className="flex flex-wrap gap-2"
|
||||
>
|
||||
{list.map((definition) => {
|
||||
const selected = definitionId === definition.id;
|
||||
return (
|
||||
<button
|
||||
key={definition.id}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
data-testid="symptom-chip"
|
||||
onClick={() => {
|
||||
setDefinitionId(selected ? null : definition.id);
|
||||
setSymptomError(null);
|
||||
}}
|
||||
className={cn(
|
||||
"focus-visible:ring-ring/50 inline-flex min-h-11 items-center gap-2 rounded-full border px-3 py-1.5 text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none sm:min-h-8",
|
||||
selected
|
||||
? "border-primary bg-primary/10 text-foreground"
|
||||
: "border-border text-foreground hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
<SymptomIcon name={definition.icon} className="size-4" />
|
||||
{definition.label ?? t("symptoms.unreadableLabel")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{canDefine && !atLimit ? (
|
||||
<AddSymptomChip
|
||||
onCreated={(id) => {
|
||||
setDefinitionId(id);
|
||||
setSymptomError(null);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{list.length === 0 && !definitions.isLoading ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{canDefine
|
||||
? t("symptoms.entry.emptyHint")
|
||||
: t("symptoms.entry.emptyHintNoManage")}
|
||||
</p>
|
||||
) : atLimit && canDefine ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("symptoms.entry.limitHint", { limit })}
|
||||
</p>
|
||||
) : null}
|
||||
{definitions.isError ? (
|
||||
// A failed read is not an empty list: the empty hint would invite
|
||||
// the person to define a symptom they may already have.
|
||||
<QueryErrorRow
|
||||
message={t("symptoms.section.loadError")}
|
||||
onRetry={() => void definitions.refetch()}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-labelledby={symptomGroupId}
|
||||
aria-describedby={
|
||||
symptomError ? `${symptomGroupId}-error` : undefined
|
||||
}
|
||||
className="flex flex-wrap gap-2"
|
||||
>
|
||||
{list.map((definition) => {
|
||||
const selected = definitionId === definition.id;
|
||||
return (
|
||||
<button
|
||||
key={definition.id}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
data-testid="symptom-chip"
|
||||
onClick={() => {
|
||||
setDefinitionId(selected ? null : definition.id);
|
||||
setSymptomError(null);
|
||||
}}
|
||||
className={cn(
|
||||
"focus-visible:ring-ring/50 inline-flex min-h-11 items-center gap-2 rounded-full border px-3 py-1.5 text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none sm:min-h-8",
|
||||
selected
|
||||
? "border-primary bg-primary/10 text-foreground"
|
||||
: "border-border text-foreground hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
<SymptomIcon name={definition.icon} className="size-4" />
|
||||
{definition.label ?? t("symptoms.unreadableLabel")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{canDefine && !atLimit ? (
|
||||
<AddSymptomChip
|
||||
onCreated={(id) => {
|
||||
setDefinitionId(id);
|
||||
setSymptomError(null);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{list.length === 0 && !definitions.isLoading ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{canDefine
|
||||
? t("symptoms.entry.emptyHint")
|
||||
: t("symptoms.entry.emptyHintNoManage")}
|
||||
</p>
|
||||
) : atLimit && canDefine ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("symptoms.entry.limitHint", { limit })}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{symptomError ? (
|
||||
<p
|
||||
id={`${symptomGroupId}-error`}
|
||||
|
||||
Reference in new issue
Block a user