eletrotupi / tcc/ frontend/tests/history/mappers.unit.test.tsc121404b00d29de6d91827ce3a3fc0ede1a0c5ab
11.8 KB | 463 lines Raw
1
import {
2
  mapCareActionToHistoryCard,
3
  mapMoodToHistoryCard,
4
  mapSleepToHistoryCard,
5
  mapTriggerToHistoryCard,
6
} from "@/lib/history/mappers";
7
import type {
8
  ActivityType,
9
  AppointmentType,
10
  CareAction,
11
  MoodEntry,
12
  SleepRecord,
13
  Trigger,
14
} from "@/lib/api";
15
import type { MoodComponent } from "@/lib/api/types";
16
import { parseDateOnly } from "@/lib/utils/time";
17

18
const FIXED = new Date(2025, 0, 15, 10, 0, 0);
19

20
function mood(overrides: Partial<MoodEntry> = {}): MoodEntry {
21
  return {
22
    id: 1,
23
    annotation: "",
24
    moment: FIXED,
25
    selectedMood: "good",
26
    anxietyLevel: 2,
27
    energyLevel: 4,
28
    stressLevel: 1,
29
    moodComponents: [],
30
    createdAt: FIXED,
31
    updatedAt: FIXED,
32
    ...overrides,
33
  };
34
}
35

36
function moodComponent(component: string): MoodComponent {
37
  return {
38
    id: 1,
39
    component,
40
    intensity: "MILD",
41
    createdAt: FIXED,
42
    updatedAt: FIXED,
43
  };
44
}
45

46
function sleep(overrides: Partial<SleepRecord> = {}): SleepRecord {
47
  return {
48
    id: 1,
49
    annotations: "",
50
    date: FIXED,
51
    average: 8,
52
    createdAt: FIXED,
53
    updatedAt: FIXED,
54
    ...overrides,
55
  };
56
}
57

58
function trigger(overrides: Partial<Trigger> = {}): Trigger {
59
  return {
60
    id: 1,
61
    comment: "",
62
    category: "Trabalho",
63
    moment: FIXED,
64
    createdAt: FIXED,
65
    updatedAt: FIXED,
66
    ...overrides,
67
  };
68
}
69

70
function careAction(overrides: Partial<CareAction> = {}): CareAction {
71
  return {
72
    id: 1,
73
    type: "MEDICINE",
74
    moment: FIXED,
75
    userId: 1,
76
    createdAt: FIXED,
77
    updatedAt: FIXED,
78
    ...overrides,
79
  };
80
}
81

82
describe("mapMoodToHistoryCard", () => {
83
  it("maps the record into a mood card", () => {
84
    const record = mood({
85
      id: 7,
86
      annotation: "Me senti bem hoje",
87
      moment: new Date(2025, 5, 10, 14, 30),
88
      selectedMood: "great",
89
      moodComponents: [moodComponent("ANXIOUS"), moodComponent("CALM")],
90
    });
91

92
    expect(mapMoodToHistoryCard(record)).toStrictEqual({
93
      id: "mood-7",
94
      category: "mood",
95
      timestamp: record.moment.toString(),
96
      title: "Ótimo",
97
      summary: "Anxious, Calm · Me senti bem hoje",
98
      badge: undefined,
99
      raw: record,
100
    });
101
  });
102

103
  it("falls back to a generic title for an unknown mood", () => {
104
    expect(mapMoodToHistoryCard(mood({ selectedMood: "unmapped" })).title).toBe(
105
      "Humor",
106
    );
107
  });
108

109
  describe("summary", () => {
110
    it("joins only the components when there is no annotation", () => {
111
      const card = mapMoodToHistoryCard(
112
        mood({
113
          annotation: "",
114
          moodComponents: [moodComponent("ANXIOUS"), moodComponent("CALM")],
115
        }),
116
      );
117

118
      expect(card.summary).toBe("Anxious, Calm");
119
    });
120

121
    it("uses only the annotation when there are no components", () => {
122
      const card = mapMoodToHistoryCard(
123
        mood({ annotation: "Tarde tranquila", moodComponents: [] }),
124
      );
125

126
      expect(card.summary).toBe("Tarde tranquila");
127
    });
128

129
    it("joins components and annotation when both are present", () => {
130
      const card = mapMoodToHistoryCard(
131
        mood({
132
          annotation: "Tarde tranquila",
133
          moodComponents: [moodComponent("CALM")],
134
        }),
135
      );
136

137
      expect(card.summary).toBe("Calm · Tarde tranquila");
138
    });
139

140
    it("falls back when neither is present", () => {
141
      const card = mapMoodToHistoryCard(
142
        mood({ annotation: "", moodComponents: [] }),
143
      );
144

145
      expect(card.summary).toBe("Sem detalhes");
146
    });
147
  });
148
});
149

150
describe("mapSleepToHistoryCard", () => {
151
  it("maps the record into a sleep card", () => {
152
    const record = sleep({
153
      id: 4,
154
      average: 7.5,
155
      annotations: "Dormi bem",
156
      date: new Date(Date.UTC(2025, 0, 15)),
157
      createdAt: new Date(2025, 0, 20, 8, 0, 0),
158
    });
159

160
    expect(mapSleepToHistoryCard(record)).toStrictEqual({
161
      id: "sleep-4",
162
      category: "sleep",
163
      timestamp: parseDateOnly(record.date).toISOString(),
164
      title: "Sono",
165
      summary: "7h 30min · Dormi bem",
166
      badge: { label: "Excelente", variant: "green" },
167
      raw: record,
168
    });
169
  });
170

171
  it("anchors on the picked day, not the creation instant", () => {
172
    const record = sleep({
173
      date: new Date(Date.UTC(2025, 0, 10)),
174
      createdAt: new Date(2025, 0, 15, 8, 0, 0),
175
    });
176

177
    const card = mapSleepToHistoryCard(record);
178

179
    expect(new Date(card.timestamp).toLocaleDateString("pt-BR")).toBe(
180
      "10/01/2025",
181
    );
182
  });
183

184
  it("reads the date-only string the API sends", () => {
185
    const record = sleep({
186
      date: "2025-01-10T00:00:00.000Z" as unknown as Date,
187
    });
188

189
    const card = mapSleepToHistoryCard(record);
190

191
    expect(new Date(card.timestamp).toLocaleDateString("pt-BR")).toBe(
192
      "10/01/2025",
193
    );
194
  });
195

196
  it("omits the separator when there is no annotation", () => {
197
    expect(mapSleepToHistoryCard(sleep({ average: 8 })).summary).toBe(
198
      "8h 00min",
199
    );
200
  });
201

202
  describe("quality badge thresholds", () => {
203
    it.each([
204
      [9, "Excelente", "green"],
205
      [7, "Excelente", "green"],
206
      [6.99, "Regular", "yellow"],
207
      [5, "Regular", "yellow"],
208
      [4.99, "Ruim", "red"],
209
      [0, "Ruim", "red"],
210
    ])("labels %p hours as %p", (average, label, variant) => {
211
      expect(mapSleepToHistoryCard(sleep({ average })).badge).toEqual({
212
        label,
213
        variant,
214
      });
215
    });
216
  });
217

218
  it("pads the minutes to two digits", () => {
219
    expect(mapSleepToHistoryCard(sleep({ average: 6.5 })).summary).toBe(
220
      "6h 30min",
221
    );
222
    expect(mapSleepToHistoryCard(sleep({ average: 6.0167 })).summary).toBe(
223
      "6h 01min",
224
    );
225
  });
226

227
  it("carries rounded minutes into the hour", () => {
228
    expect(mapSleepToHistoryCard(sleep({ average: 7.999 })).summary).toBe(
229
      "8h 00min",
230
    );
231
    expect(mapSleepToHistoryCard(sleep({ average: 5.999 })).summary).toBe(
232
      "6h 00min",
233
    );
234
  });
235
});
236

237
describe("mapTriggerToHistoryCard", () => {
238
  it("maps the record into a trigger card", () => {
239
    const record = trigger({
240
      id: 3,
241
      category: "Trabalho",
242
      comment: "Muito estresse",
243
      moment: new Date(2025, 2, 1, 9, 0, 0),
244
    });
245

246
    expect(mapTriggerToHistoryCard(record)).toStrictEqual({
247
      id: "trigger-3",
248
      category: "trigger",
249
      timestamp: record.moment.toString(),
250
      title: "Gatilho",
251
      summary: "Trabalho · Muito estresse",
252
      badge: undefined,
253
      raw: record,
254
    });
255
  });
256

257
  it("drops the separator when there is no comment", () => {
258
    const card = mapTriggerToHistoryCard(
259
      trigger({ category: "Sono", comment: "" }),
260
    );
261

262
    expect(card.summary).toBe("Sono");
263
  });
264
});
265

266
describe("mapCareActionToHistoryCard", () => {
267
  describe("medicine", () => {
268
    it("uses the regimen name and dosage", () => {
269
      const record = careAction({
270
        id: 10,
271
        type: "MEDICINE",
272
        medicineLog: {
273
          id: 1,
274
          careActionId: 10,
275
          takenAt: FIXED,
276
          regimen: {
277
            id: 1,
278
            name: "Ritalina",
279
            dosage: "10mg",
280
            periodicity: "DAILY",
281
            scheduledAt: [],
282
            active: true,
283
            userId: 1,
284
            createdAt: FIXED,
285
            updatedAt: FIXED,
286
          },
287
        },
288
      });
289

290
      expect(mapCareActionToHistoryCard(record)).toStrictEqual({
291
        id: "care_action-10",
292
        category: "care_action",
293
        subtype: "medicine",
294
        timestamp: record.moment.toString(),
295
        title: "Ritalina",
296
        summary: "10mg",
297
        raw: record,
298
      });
299
    });
300

301
    it("falls back when there is no regimen", () => {
302
      const card = mapCareActionToHistoryCard(careAction({ type: "MEDICINE" }));
303

304
      expect(card.title).toBe("Medicamento");
305
      expect(card.summary).toBe("Sem dosagem registrada");
306
    });
307
  });
308

309
  describe("appointment", () => {
310
    it("joins the duration and the note", () => {
311
      const record = careAction({
312
        id: 20,
313
        type: "APPOINTMENT",
314
        appointment: {
315
          id: 1,
316
          careActionId: 20,
317
          type: "ANALYST",
318
          duration: 50,
319
          note: "Boa sessão",
320
        },
321
      });
322

323
      expect(mapCareActionToHistoryCard(record)).toStrictEqual({
324
        id: "care_action-20",
325
        category: "care_action",
326
        subtype: "appointment",
327
        timestamp: record.moment.toString(),
328
        title: "Consulta · Analista",
329
        summary: "50 min · Boa sessão",
330
        badge: undefined,
331
        raw: record,
332
      });
333
    });
334

335
    it("falls back when there are no details", () => {
336
      const card = mapCareActionToHistoryCard(
337
        careAction({ type: "APPOINTMENT", appointment: undefined }),
338
      );
339

340
      expect(card.summary).toBe("Sem anotações");
341
      expect(card.title).toBe("Consulta · Consulta");
342
    });
343

344
    it.each([
345
      ["ANALYST", "Analista"],
346
      ["PSYCHIATRIST", "Psiquiatra"],
347
      ["GP", "Clínico geral"],
348
      ["NUTRITIONIST", "Nutricionista"],
349
      ["PHYSIOTHERAPIST", "Fisioterapeuta"],
350
      ["OTHER", "Consulta"],
351
      ["unmapped", "Consulta"],
352
    ] as const)("labels the %p appointment as %p", (type, label) => {
353
      const card = mapCareActionToHistoryCard(
354
        careAction({
355
          type: "APPOINTMENT",
356
          appointment: {
357
            id: 1,
358
            careActionId: 1,
359
            type: type as AppointmentType,
360
            duration: 30,
361
          },
362
        }),
363
      );
364

365
      expect(card.title).toBe(`Consulta · ${label}`);
366
    });
367

368
    describe("vínculo badge", () => {
369
      it("is absent with no links", () => {
370
        expect(
371
          mapCareActionToHistoryCard(careAction({ type: "APPOINTMENT" })).badge,
372
        ).toBeUndefined();
373
      });
374

375
      it("uses the singular for a single link", () => {
376
        expect(
377
          mapCareActionToHistoryCard(
378
            careAction({ type: "APPOINTMENT", triggerId: 5 }),
379
          ).badge,
380
        ).toEqual({ label: "1 vínculo", variant: "blue" });
381
      });
382

383
      it("uses the plural for two links", () => {
384
        expect(
385
          mapCareActionToHistoryCard(
386
            careAction({ type: "APPOINTMENT", triggerId: 5, moodId: 6 }),
387
          ).badge,
388
        ).toEqual({ label: "2 vínculos", variant: "blue" });
389
      });
390
    });
391
  });
392

393
  describe("activity", () => {
394
    it("uses the activity label and duration", () => {
395
      const record = careAction({
396
        id: 30,
397
        type: "ACTIVITY",
398
        activity: { id: 1, careActionId: 30, type: "WALK", duration: 30 },
399
      });
400

401
      expect(mapCareActionToHistoryCard(record)).toStrictEqual({
402
        id: "care_action-30",
403
        category: "care_action",
404
        subtype: "activity",
405
        timestamp: record.moment.toString(),
406
        title: "Caminhada",
407
        summary: "30 min",
408
        raw: record,
409
      });
410
    });
411

412
    it("falls back when there is no duration", () => {
413
      const card = mapCareActionToHistoryCard(
414
        careAction({
415
          type: "ACTIVITY",
416
          activity: { id: 1, careActionId: 1, type: "YOGA" },
417
        }),
418
      );
419

420
      expect(card.summary).toBe("Sem duração registrada");
421
    });
422

423
    it.each([
424
      ["WALK", "Caminhada"],
425
      ["YOGA", "Yoga"],
426
      ["GYM", "Academia"],
427
      ["MEDITATION", "Meditação"],
428
      ["SOCIAL", "Momento social"],
429
      ["CREATIVE", "Atividade criativa"],
430
      ["OTHER", "Atividade"],
431
      ["unmapped", "Atividade"],
432
    ] as const)("labels the %p activity as %p", (type, label) => {
433
      const card = mapCareActionToHistoryCard(
434
        careAction({
435
          type: "ACTIVITY",
436
          activity: {
437
            id: 1,
438
            careActionId: 1,
439
            type: type as ActivityType,
440
          },
441
        }),
442
      );
443

444
      expect(card.title).toBe(label);
445
    });
446
  });
447

448
  it("falls back for an unknown care-action type", () => {
449
    const record = careAction({
450
      id: 40,
451
      type: "SOMETHING_ELSE" as unknown as CareAction["type"],
452
    });
453

454
    expect(mapCareActionToHistoryCard(record)).toStrictEqual({
455
      id: "care_action-40",
456
      category: "care_action",
457
      timestamp: record.moment.toString(),
458
      title: "Ação de cuidado",
459
      summary: "",
460
      raw: record,
461
    });
462
  });
463
});