eletrotupi / tcc/ commit / b3ead45

frontend: cover the constants lookups and the query keys

The mood, trigger, care-action and notification lookup tables and every
react-query key builder had no offline check, so a drifted label, a lost
fallback or a reshaped key could only surface as a stale cache on a
device. Each key's base, list, detail and filtered shapes are pinned,
along with the day segment the insight key embeds
Pedro Lucas Porcellis porcellis@eletrotupi.com · 9 days ago b3ead45a73252ca3006b75f6c77a3d72e8055586 Browse files
Parents ff054a2
Changed files
6
Additions
+395
Deletions
0
/dev/null → frontend/tests/constants/care-action-categories.unit.test.ts
1 +
import {
2 +
  getActivityCategory,
3 +
  getCareActionMeta,
4 +
} from "@/constants/care-action-categories";
5 +
import type {
6 +
  AppointmentType,
7 +
  CareAction,
8 +
  CareActionType,
9 +
  MedicineRegimen,
10 +
} from "@/lib/api";
11 +

12 +
const FIXED = new Date(2025, 0, 15, 10, 0, 0);
13 +

14 +
const GENERIC_ACTIVITY = {
15 +
  icon: "star",
16 +
  color: "#6B7280",
17 +
  iconBackground: "#F3F4F6",
18 +
  label: "Atividade",
19 +
};
20 +

21 +
const APPOINTMENTS: { type: AppointmentType; label: string }[] = [
22 +
  { type: "ANALYST", label: "Analista" },
23 +
  { type: "PSYCHIATRIST", label: "Psiquiatra" },
24 +
  { type: "GP", label: "Clínico" },
25 +
  { type: "NUTRITIONIST", label: "Nutricionista" },
26 +
  { type: "PHYSIOTHERAPIST", label: "Fisioterapeuta" },
27 +
  { type: "OTHER", label: "Consulta" },
28 +
];
29 +

30 +
function regimen(overrides: Partial<MedicineRegimen> = {}): MedicineRegimen {
31 +
  return {
32 +
    id: 1,
33 +
    name: "Sertralina",
34 +
    dosage: "50mg",
35 +
    periodicity: "DAILY",
36 +
    scheduledAt: ["08:00"],
37 +
    active: true,
38 +
    userId: 1,
39 +
    createdAt: FIXED,
40 +
    updatedAt: FIXED,
41 +
    ...overrides,
42 +
  };
43 +
}
44 +

45 +
function careAction(overrides: Partial<CareAction> = {}): CareAction {
46 +
  return {
47 +
    id: 1,
48 +
    type: "ACTIVITY",
49 +
    moment: FIXED,
50 +
    userId: 1,
51 +
    createdAt: FIXED,
52 +
    updatedAt: FIXED,
53 +
    ...overrides,
54 +
  };
55 +
}
56 +

57 +
describe("getCareActionMeta", () => {
58 +
  it.each(APPOINTMENTS)("labels a $type appointment", ({ type, label }) => {
59 +
    expect(
60 +
      getCareActionMeta(
61 +
        careAction({
62 +
          type: "APPOINTMENT",
63 +
          appointment: { id: 1, careActionId: 1, type, duration: 50 },
64 +
        }),
65 +
      ),
66 +
    ).toStrictEqual({
67 +
      icon: "psychology",
68 +
      color: "#1D4ED8",
69 +
      iconBackground: "#DBEAFE",
70 +
      label,
71 +
    });
72 +
  });
73 +

74 +
  it("falls back to a generic appointment without a detail", () => {
75 +
    expect(getCareActionMeta(careAction({ type: "APPOINTMENT" }))).toStrictEqual(
76 +
      {
77 +
        icon: "psychology",
78 +
        color: "#1D4ED8",
79 +
        iconBackground: "#DBEAFE",
80 +
        label: "Consulta",
81 +
      },
82 +
    );
83 +
  });
84 +

85 +
  it("labels medicine by the regimen name", () => {
86 +
    expect(
87 +
      getCareActionMeta(
88 +
        careAction({
89 +
          type: "MEDICINE",
90 +
          medicineLog: {
91 +
            id: 1,
92 +
            careActionId: 1,
93 +
            takenAt: FIXED,
94 +
            regimen: regimen(),
95 +
          },
96 +
        }),
97 +
      ),
98 +
    ).toStrictEqual({
99 +
      icon: "medication",
100 +
      color: "#7C3AED",
101 +
      iconBackground: "#EDE9FE",
102 +
      label: "Sertralina",
103 +
    });
104 +
  });
105 +

106 +
  it("falls back to a generic medicine label without a regimen", () => {
107 +
    expect(getCareActionMeta(careAction({ type: "MEDICINE" }))).toStrictEqual({
108 +
      icon: "medication",
109 +
      color: "#7C3AED",
110 +
      iconBackground: "#EDE9FE",
111 +
      label: "Medicação",
112 +
    });
113 +
  });
114 +

115 +
  it("labels an activity by its activity type", () => {
116 +
    expect(
117 +
      getCareActionMeta(
118 +
        careAction({
119 +
          type: "ACTIVITY",
120 +
          activity: { id: 1, careActionId: 1, type: "WALK" },
121 +
        }),
122 +
      ),
123 +
    ).toStrictEqual({
124 +
      icon: "directions-walk",
125 +
      color: "#059669",
126 +
      iconBackground: "#D1FAE5",
127 +
      label: "Caminhada",
128 +
    });
129 +
  });
130 +

131 +
  it("falls back to a generic activity without a detail", () => {
132 +
    expect(getCareActionMeta(careAction({ type: "ACTIVITY" }))).toStrictEqual(
133 +
      GENERIC_ACTIVITY,
134 +
    );
135 +
  });
136 +

137 +
  it("falls back to a generic care action for a type it does not know", () => {
138 +
    const unknown = careAction({ type: "UNKNOWN" as unknown as CareActionType });
139 +

140 +
    expect(getCareActionMeta(unknown)).toStrictEqual({
141 +
      icon: "star",
142 +
      color: "#6B7280",
143 +
      iconBackground: "#F3F4F6",
144 +
      label: "Cuidado",
145 +
    });
146 +
  });
147 +
});
148 +

149 +
describe("getActivityCategory", () => {
150 +
  it("finds an activity by its type", () => {
151 +
    expect(getActivityCategory("YOGA")).toStrictEqual({
152 +
      icon: "self-improvement",
153 +
      color: "#7C3AED",
154 +
      iconBackground: "#EDE9FE",
155 +
      label: "Yoga",
156 +
    });
157 +
  });
158 +

159 +
  it("falls back to the catch-all activity", () => {
160 +
    expect(getActivityCategory()).toStrictEqual(GENERIC_ACTIVITY);
161 +
    expect(getActivityCategory("SKATE")).toStrictEqual(GENERIC_ACTIVITY);
162 +
  });
163 +
});
/dev/null → frontend/tests/constants/mood-components.unit.test.ts
1 +
import {
2 +
  getMoodComponent,
3 +
  intensityLabel,
4 +
  intensityToValue,
5 +
} from "@/constants/mood-components";
6 +

7 +
describe("getMoodComponent", () => {
8 +
  it("finds a component by its id", () => {
9 +
    expect(getMoodComponent("joy")).toStrictEqual({
10 +
      id: "joy",
11 +
      label: "Alegria",
12 +
      color: "#FBBF24",
13 +
    });
14 +
  });
15 +

16 +
  it("returns undefined for an unknown id", () => {
17 +
    expect(getMoodComponent("ennui")).toBeUndefined();
18 +
  });
19 +
});
20 +

21 +
describe("intensityLabel", () => {
22 +
  it.each([
23 +
    { value: 1, label: "Suave" },
24 +
    { value: 2, label: "Moderada" },
25 +
    { value: 3, label: "Intensa" },
26 +
  ])("labels the numeric intensity $value", ({ value, label }) => {
27 +
    expect(intensityLabel(value)).toBe(label);
28 +
  });
29 +

30 +
  it.each([
31 +
    { value: "LIGHT", label: "Suave" },
32 +
    { value: "MODERATE", label: "Moderada" },
33 +
    { value: "HIGH", label: "Intensa" },
34 +
  ])("labels the string intensity $value", ({ value, label }) => {
35 +
    expect(intensityLabel(value)).toBe(label);
36 +
  });
37 +
});
38 +

39 +
describe("intensityToValue", () => {
40 +
  it.each([
41 +
    { value: 1, expected: "LIGHT" },
42 +
    { value: 2, expected: "MODERATE" },
43 +
    { value: 3, expected: "HIGH" },
44 +
  ])("maps the numeric intensity $value", ({ value, expected }) => {
45 +
    expect(intensityToValue(value)).toBe(expected);
46 +
  });
47 +

48 +
  it("round-trips every value back to its label", () => {
49 +
    for (const value of [1, 2, 3]) {
50 +
      expect(intensityLabel(intensityToValue(value))).toBe(
51 +
        intensityLabel(value),
52 +
      );
53 +
    }
54 +
  });
55 +
});
/dev/null → frontend/tests/constants/moods.unit.test.ts
1 +
import { getMood } from "@/constants/moods";
2 +

3 +
describe("getMood", () => {
4 +
  it("finds a mood by its id", () => {
5 +
    expect(getMood("good")).toStrictEqual({
6 +
      id: "good",
7 +
      icon: "😊",
8 +
      label: "Bem",
9 +
    });
10 +
  });
11 +

12 +
  it("returns undefined for an unknown id", () => {
13 +
    expect(getMood("elated")).toBeUndefined();
14 +
  });
15 +
});
/dev/null → frontend/tests/constants/notification-kinds.unit.test.ts
1 +
import { NOTIFICATION_KINDS } from "@/constants/notification-kinds";
2 +

3 +
describe("NOTIFICATION_KINDS", () => {
4 +
  it("carries the icon and bubble for a daily reminder", () => {
5 +
    expect(NOTIFICATION_KINDS.DAILY_REMINDER).toStrictEqual({
6 +
      icon: "mood",
7 +
      color: "#0EA5E9",
8 +
      iconBackground: "#E0F2FE",
9 +
    });
10 +
  });
11 +

12 +
  it("carries the icon and bubble for a medicine reminder", () => {
13 +
    expect(NOTIFICATION_KINDS.MEDICINE_REMINDER).toStrictEqual({
14 +
      icon: "medication",
15 +
      color: "#7C3AED",
16 +
      iconBackground: "#EDE9FE",
17 +
    });
18 +
  });
19 +

20 +
  it("carries the icon and bubble for an insight alert", () => {
21 +
    expect(NOTIFICATION_KINDS.INSIGHT_ALERT).toStrictEqual({
22 +
      icon: "lightbulb-outline",
23 +
      color: "#D97706",
24 +
      iconBackground: "#FEF3C7",
25 +
    });
26 +
  });
27 +
});
/dev/null → frontend/tests/constants/trigger-categories.unit.test.ts
1 +
import { getTriggerCategory } from "@/constants/trigger-categories";
2 +

3 +
describe("getTriggerCategory", () => {
4 +
  it("finds a category by its id", () => {
5 +
    expect(getTriggerCategory("WORK")).toStrictEqual({
6 +
      id: "WORK",
7 +
      label: "Trabalho",
8 +
      icon: "work",
9 +
      color: "#3b82f6",
10 +
      iconBackground: "#dbeafe",
11 +
      variant: "blue",
12 +
    });
13 +
  });
14 +

15 +
  it("falls back to the catch-all category for an unknown id", () => {
16 +
    expect(getTriggerCategory("UNKNOWN")).toStrictEqual({
17 +
      id: "OTHER",
18 +
      label: "Outro",
19 +
      icon: "help-outline",
20 +
      color: "#94a3b8",
21 +
      iconBackground: "#f1f5f9",
22 +
      variant: "gray",
23 +
    });
24 +
  });
25 +
});
/dev/null → frontend/tests/hooks/queryKeys.unit.test.ts
1 +
import { careActionKeys } from "@/hooks/useCareAction.queries";
2 +
import { insightKeys } from "@/hooks/useInsights.queries";
3 +
import { medicineRegimenKeys } from "@/hooks/useMedicineRegimen.queries";
4 +
import { medicineTodayKeys } from "@/hooks/useMedicineToday.queries";
5 +
import { moodKeys } from "@/hooks/useMoodEntries.queries";
6 +
import { notificationKeys } from "@/hooks/useNotifications.queries";
7 +
import { sleepRecordKeys } from "@/hooks/useSleepRecord.queries";
8 +
import { triggerKeys } from "@/hooks/useTrigger.queries";
9 +

10 +
const nested = [
11 +
  { name: "moodKeys", keys: moodKeys, base: "mood-entries" },
12 +
  { name: "triggerKeys", keys: triggerKeys, base: "triggers" },
13 +
  { name: "sleepRecordKeys", keys: sleepRecordKeys, base: "sleep-records" },
14 +
  { name: "careActionKeys", keys: careActionKeys, base: "careActions" },
15 +
];
16 +

17 +
describe.each(nested)("$name", ({ keys, base }) => {
18 +
  it("nests the list and detail builders under the base", () => {
19 +
    expect(keys.all()).toStrictEqual([base]);
20 +
    expect(keys.lists()).toStrictEqual([base, "list"]);
21 +
    expect(keys.detail("9")).toStrictEqual([base, "detail", "9"]);
22 +
  });
23 +

24 +
  it("carries the filters into the list key", () => {
25 +
    expect(keys.list({ limit: 10 })).toStrictEqual([
26 +
      base,
27 +
      "list",
28 +
      { limit: 10 },
29 +
    ]);
30 +
    expect(keys.list()).toStrictEqual([base, "list", undefined]);
31 +
  });
32 +
});
33 +

34 +
describe("medicineRegimenKeys", () => {
35 +
  it("nests the unfiltered list and detail builders under the base", () => {
36 +
    expect(medicineRegimenKeys.all()).toStrictEqual(["medicineRegimens"]);
37 +
    expect(medicineRegimenKeys.lists()).toStrictEqual([
38 +
      "medicineRegimens",
39 +
      "list",
40 +
    ]);
41 +
    expect(medicineRegimenKeys.list()).toStrictEqual([
42 +
      "medicineRegimens",
43 +
      "list",
44 +
    ]);
45 +
    expect(medicineRegimenKeys.detail("2")).toStrictEqual([
46 +
      "medicineRegimens",
47 +
      "detail",
48 +
      "2",
49 +
    ]);
50 +
  });
51 +
});
52 +

53 +
describe("medicineTodayKeys", () => {
54 +
  it("nests a single list builder under the base", () => {
55 +
    expect(medicineTodayKeys.all()).toStrictEqual(["medicineToday"]);
56 +
    expect(medicineTodayKeys.list()).toStrictEqual(["medicineToday", "list"]);
57 +
  });
58 +
});
59 +

60 +
describe("notificationKeys", () => {
61 +
  it("nests the infinite builder under the list segment", () => {
62 +
    expect(notificationKeys.all()).toStrictEqual(["notifications"]);
63 +
    expect(notificationKeys.lists()).toStrictEqual(["notifications", "list"]);
64 +
    expect(notificationKeys.infinite()).toStrictEqual([
65 +
      "notifications",
66 +
      "list",
67 +
      "infinite",
68 +
    ]);
69 +
  });
70 +
});
71 +

72 +
describe("insightKeys", () => {
73 +
  beforeEach(() => {
74 +
    jest.useFakeTimers();
75 +
  });
76 +

77 +
  afterEach(() => {
78 +
    jest.useRealTimers();
79 +
  });
80 +

81 +
  it("nests the list builder under the base", () => {
82 +
    expect(insightKeys.all()).toStrictEqual(["insights"]);
83 +
    expect(insightKeys.lists()).toStrictEqual(["insights", "list"]);
84 +
    expect(insightKeys.list({ period: "DAILY" })).toStrictEqual([
85 +
      "insights",
86 +
      "list",
87 +
      { period: "DAILY" },
88 +
    ]);
89 +
  });
90 +

91 +
  it("scopes a type key to the local day and rolls it over at midnight", () => {
92 +
    jest.setSystemTime(new Date(2025, 0, 15, 23, 59));
93 +

94 +
    expect(insightKeys.byType("DAILY_SLEEP")).toStrictEqual([
95 +
      "insights",
96 +
      "type",
97 +
      "DAILY_SLEEP",
98 +
      "2025-01-15",
99 +
    ]);
100 +

101 +
    jest.setSystemTime(new Date(2025, 0, 16, 0, 1));
102 +

103 +
    expect(insightKeys.byType("DAILY_SLEEP")).toStrictEqual([
104 +
      "insights",
105 +
      "type",
106 +
      "DAILY_SLEEP",
107 +
      "2025-01-16",
108 +
    ]);
109 +
  });
110 +
});