diff --git a/messages/de.json b/messages/de.json index 2629c846f..8557410f9 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8231,7 +8231,9 @@ "flowSpotting": "Schmierblutung", "flowLight": "leicht", "flowMedium": "mittel", - "flowHeavy": "stark" + "flowHeavy": "stark", + "legendIntercourse": "Geschlechtsverkehr", + "legendOtherEntries": "Weitere Einträge" }, "predictions": { "title": "Was als Nächstes kommt", diff --git a/messages/en.json b/messages/en.json index b2d727fc7..0a02c12f6 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8231,7 +8231,9 @@ "flowSpotting": "spotting", "flowLight": "light", "flowMedium": "medium", - "flowHeavy": "heavy" + "flowHeavy": "heavy", + "legendIntercourse": "Intercourse", + "legendOtherEntries": "Other entries" }, "predictions": { "title": "What comes next", diff --git a/messages/es.json b/messages/es.json index 638d44389..853bb3f69 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8231,7 +8231,9 @@ "flowSpotting": "manchado", "flowLight": "ligero", "flowMedium": "medio", - "flowHeavy": "abundante" + "flowHeavy": "abundante", + "legendIntercourse": "Relaciones", + "legendOtherEntries": "Otras entradas" }, "predictions": { "title": "Qué viene después", diff --git a/messages/fr.json b/messages/fr.json index a91aaba73..7942c43e9 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8231,7 +8231,9 @@ "flowSpotting": "spotting", "flowLight": "léger", "flowMedium": "moyen", - "flowHeavy": "abondant" + "flowHeavy": "abondant", + "legendIntercourse": "Rapports", + "legendOtherEntries": "Autres saisies" }, "predictions": { "title": "Ce qui arrive ensuite", diff --git a/messages/it.json b/messages/it.json index ba55d1bd9..bb9c4302e 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8231,7 +8231,9 @@ "flowSpotting": "spotting", "flowLight": "leggero", "flowMedium": "medio", - "flowHeavy": "abbondante" + "flowHeavy": "abbondante", + "legendIntercourse": "Rapporti", + "legendOtherEntries": "Altre voci" }, "predictions": { "title": "Cosa arriva dopo", diff --git a/messages/ko.json b/messages/ko.json index 78925959c..e629f3528 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -8231,7 +8231,9 @@ "flowSpotting": "점상 출혈", "flowLight": "적음", "flowMedium": "보통", - "flowHeavy": "많음" + "flowHeavy": "많음", + "legendIntercourse": "성관계", + "legendOtherEntries": "기타 기록" }, "predictions": { "title": "다음에 올 일", diff --git a/messages/pl.json b/messages/pl.json index 89b15b449..536400683 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8231,7 +8231,9 @@ "flowSpotting": "plamienie", "flowLight": "lekkie", "flowMedium": "średnie", - "flowHeavy": "obfite" + "flowHeavy": "obfite", + "legendIntercourse": "Współżycie", + "legendOtherEntries": "Inne wpisy" }, "predictions": { "title": "Co dalej", diff --git a/src/components/cycle/__tests__/bbt-chart.test.tsx b/src/components/cycle/__tests__/bbt-chart.test.tsx index a07a707f2..e2adcb629 100644 --- a/src/components/cycle/__tests__/bbt-chart.test.tsx +++ b/src/components/cycle/__tests__/bbt-chart.test.tsx @@ -28,6 +28,15 @@ function day(date: string, overrides: Partial = {}): CalendarDay { basalBodyTempC: null, ovulationTest: null, cervicalMucus: null, + cervixPosition: null, + cervixFirmness: null, + cervixOpening: null, + intermenstrualBleeding: false, + sexualActivity: false, + pregnancyTest: null, + progesteroneTest: null, + contraceptive: null, + hasNote: false, ...overrides, }; } diff --git a/src/components/cycle/__tests__/cycle-calendar.test.tsx b/src/components/cycle/__tests__/cycle-calendar.test.tsx index 800802a61..1d8032356 100644 --- a/src/components/cycle/__tests__/cycle-calendar.test.tsx +++ b/src/components/cycle/__tests__/cycle-calendar.test.tsx @@ -28,9 +28,27 @@ function dayBase(date: string): CalendarDay { basalBodyTempC: null, ovulationTest: null, cervicalMucus: null, + cervixPosition: null, + cervixFirmness: null, + cervixOpening: null, + intermenstrualBleeding: false, + sexualActivity: false, + pregnancyTest: null, + progesteroneTest: null, + contraceptive: null, + hasNote: false, }; } +/** The opening tag of one day's cell, by its date in the aria-label. */ +function cellTag(html: string, date: string): string { + const match = html.match( + new RegExp(`<(?:button|div)[^>]*aria-label="${date}[^"]*"[^>]*>`), + ); + if (!match) throw new Error(`no cell for ${date}`); + return match[0]; +} + describe("", () => { const today = "2026-06-15"; @@ -130,4 +148,85 @@ describe("", () => { ); expect(html).toContain('data-fertile="true"'); }); + + describe("per-day entries beyond period and symptoms (#1032)", () => { + it("marks an intercourse day with its own marker and names it", () => { + const days = [{ ...dayBase("2026-06-05"), sexualActivity: true }]; + const html = render( + {}} />, + ); + const cell = cellTag(html, "2026-06-05"); + expect(cell).toContain('data-intercourse="true"'); + expect(cell).toContain("Intercourse"); + // Only that day. + expect(cellTag(html, "2026-06-06")).not.toContain("data-intercourse"); + expect(html).toContain('data-slot="cycle-calendar-intercourse"'); + }); + + it("the legend names intercourse and the other-entries marker", () => { + const html = render( + {}} />, + ); + expect(html).toContain('data-legend="cycle.calendar.legendIntercourse"'); + expect(html).toContain('data-legend="cycle.calendar.legendOtherEntries"'); + expect(html).toContain("Intercourse"); + expect(html).toContain("Other entries"); + }); + + it.each([ + [ + "a pregnancy test", + { pregnancyTest: "POSITIVE" }, + "Pregnancy test (Positive)", + ], + [ + "a progesterone test", + { progesteroneTest: "NEGATIVE" }, + "Progesterone test (Negative)", + ], + ["contraception", { contraceptive: "ORAL" }, "Contraceptive (Pill)"], + [ + "spotting", + { intermenstrualBleeding: true }, + "Spotting between periods", + ], + ["a note", { hasNote: true }, "Note"], + ["a temperature", { basalBodyTempC: 36.6 }, "Basal body temperature"], + [ + "an ovulation test", + { ovulationTest: "POSITIVE_LH_SURGE" }, + "Ovulation test (LH surge)", + ], + [ + "a mucus reading", + { cervicalMucus: "CREAMY" }, + "Cervical mucus (Creamy)", + ], + ["a cervix sign", { cervixPosition: "HIGH" }, "Cervix"], + ] as const)( + "a day with only %s logged no longer looks empty", + (_label, over, aria) => { + const days = [{ ...dayBase("2026-06-08"), ...over } as CalendarDay]; + const html = render( + {}} />, + ); + const cell = cellTag(html, "2026-06-08"); + expect(cell).toContain('data-other-entries="true"'); + expect(cell).toContain(aria); + }, + ); + + it("an empty day carries neither marker", () => { + const html = render( + {}} + />, + ); + const cell = cellTag(html, "2026-06-08"); + expect(cell).not.toContain("data-other-entries"); + expect(cell).not.toContain("data-intercourse"); + }); + }); }); diff --git a/src/components/cycle/__tests__/cycle-view-sheet-context.test.tsx b/src/components/cycle/__tests__/cycle-view-sheet-context.test.tsx index e32bb1c3a..f727d776e 100644 --- a/src/components/cycle/__tests__/cycle-view-sheet-context.test.tsx +++ b/src/components/cycle/__tests__/cycle-view-sheet-context.test.tsx @@ -45,6 +45,15 @@ function gridDay(date: string, over: Partial = {}): CalendarDay { basalBodyTempC: null, ovulationTest: null, cervicalMucus: null, + cervixPosition: null, + cervixFirmness: null, + cervixOpening: null, + intermenstrualBleeding: false, + sexualActivity: false, + pregnancyTest: null, + progesteroneTest: null, + contraceptive: null, + hasNote: false, ...over, }; } diff --git a/src/components/cycle/__tests__/log-day-sheet.test.ts b/src/components/cycle/__tests__/log-day-sheet.test.ts index 9f0a1e56a..78e280933 100644 --- a/src/components/cycle/__tests__/log-day-sheet.test.ts +++ b/src/components/cycle/__tests__/log-day-sheet.test.ts @@ -281,6 +281,15 @@ function gridDay(date: string, over: Partial = {}): CalendarDay { basalBodyTempC: null, ovulationTest: null, cervicalMucus: null, + cervixPosition: null, + cervixFirmness: null, + cervixOpening: null, + intermenstrualBleeding: false, + sexualActivity: false, + pregnancyTest: null, + progesteroneTest: null, + contraceptive: null, + hasNote: false, ...over, }; } diff --git a/src/components/cycle/cycle-calendar.tsx b/src/components/cycle/cycle-calendar.tsx index c38cd30d8..126b41d1a 100644 --- a/src/components/cycle/cycle-calendar.tsx +++ b/src/components/cycle/cycle-calendar.tsx @@ -12,6 +12,7 @@ import type { CalendarDay } from "./types"; import { FERTILE_HUE, FLOW_HUE, + INTERCOURSE_HUE, OVULATION_HUE, flowOpacity, } from "./phase-tokens"; @@ -25,7 +26,11 @@ import { * single dated dot); the fertile window (already goal-gated server-side — the * API nulls it unless TRYING_TO_CONCEIVE) renders as a green ring; the * predicted-ovulation day gets an amber marker; per-day symptom presence - * shows a small dot. Selecting a day calls `onSelectDay` (the log-day sheet). + * shows a small dot; intercourse shows a diamond; anything else logged that + * day (a test, contraception, spotting, a temperature, a mucus or cervix + * reading, a note) shows a hollow ring, so a day with only one of those + * logged no longer looks like a day with nothing logged. Selecting a day + * calls `onSelectDay` (the log-day sheet). * * a11y: each day is a real `