package mobile import ( "encoding/json" "strings" "testing" ) func decodeDay(t *testing.T, s string) map[string]any { t.Helper() var m map[string]any if err := json.Unmarshal([]byte(s), &m); err != nil { t.Fatalf("Day returned invalid JSON: %v\n%s", err, s) } return m } func TestDayCarriesLocalisedRank(t *testing.T) { pl := decodeDay(t, Day("2026-08-01", "of", "vul", "pl")) if pl["rank"] != "memorial" { t.Errorf("pl rank = %v, want memorial", pl["rank"]) } if pl["rank_label"] != "wspomnienie obowiązkowe" { t.Errorf("pl rank_label = %v", pl["rank_label"]) } en := decodeDay(t, Day("2026-08-01", "of", "vul", "en")) if en["rank_label"] != "memorial" { t.Errorf("en rank_label = %v", en["rank_label"]) } } // The app shows the colour as a swatch and never as a word, so the raw key must // be present and no localised label may be. This test exists so the app cannot // quietly start depending on a colour word. func TestDayHasColourKeyButNoColourLabel(t *testing.T) { m := decodeDay(t, Day("2026-08-01", "of", "vul", "pl")) if m["colour"] != "white" { t.Errorf("colour = %v, want white", m["colour"]) } if _, present := m["colour_label"]; present { t.Error("colour_label must not be returned: the app draws a swatch") } } func decodeDays(t *testing.T, s string) []map[string]any { t.Helper() var a []map[string]any if err := json.Unmarshal([]byte(s), &a); err != nil { t.Fatalf("Days returned invalid JSON: %v\n%s", err, s) } return a } func TestDaysReturnsConsecutiveDates(t *testing.T) { a := decodeDays(t, Days("2026-07-27", 7, "of", "pl")) if len(a) != 7 { t.Fatalf("got %d elements, want 7", len(a)) } want := []string{"2026-07-27", "2026-07-28", "2026-07-29", "2026-07-30", "2026-07-31", "2026-08-01", "2026-08-02"} for i, w := range want { if a[i]["date"] != w { t.Errorf("element %d date = %v, want %v", i, a[i]["date"], w) } } } // Days must agree with Day about the same date -- it is a cheaper projection of // the same computation, not a second implementation. func TestDaysAgreesWithDay(t *testing.T) { one := decodeDay(t, Day("2026-08-01", "of", "vul", "pl")) week := decodeDays(t, Days("2026-08-01", 1, "of", "pl")) if len(week) != 1 { t.Fatalf("got %d elements, want 1", len(week)) } row := week[0] for _, k := range []string{"name", "colour", "rank", "rank_label"} { if row[k] != one[k] { t.Errorf("%s: Days = %v, Day = %v", k, row[k], one[k]) } } parts := row["parts"].([]any) readings := one["readings"].([]any) if len(parts) != len(readings) { t.Fatalf("parts = %d, readings = %d", len(parts), len(readings)) } for i := range parts { p := parts[i].(map[string]any) r := readings[i].(map[string]any) if p["part"] != r["part"] || p["citation"] != r["citation"] { t.Errorf("element %d: Days %v/%v vs Day %v/%v", i, p["part"], p["citation"], r["part"], r["citation"]) } } } // No reading text may cross the boundary: that is the whole point of Days. func TestDaysCarriesNoReadingText(t *testing.T) { s := Days("2026-08-01", 7, "of", "pl") if strings.Contains(s, `"text"`) { t.Error("Days must not return reading text") } if strings.Contains(s, "colour_label") { t.Error("Days must not return a colour label") } } func TestDaysRejectsBadInput(t *testing.T) { for _, c := range []struct { start string count int }{ {"not-a-date", 7}, {"2026-08-01", 0}, {"2026-08-01", -1}, {"2026-08-01", 400}, } { if got := Days(c.start, c.count, "of", "pl"); got != "[]" { t.Errorf("Days(%q, %d) = %s, want []", c.start, c.count, got) } } } func BenchmarkDaysWeek(b *testing.B) { for i := 0; i < b.N; i++ { Days("2026-07-27", 7, "of", "pl") } } // This is the assertion that would have caught the app's dead 1962 checkboxes: // it listed nine part IDs where the engine emits two. func TestPartLabelsMatchesWhatTheEngineEmits(t *testing.T) { var ef []map[string]any if err := json.Unmarshal([]byte(PartLabels("ef", "pl")), &ef); err != nil { t.Fatalf("invalid JSON: %v", err) } if len(ef) != 2 { t.Fatalf("ef: got %d labels, want 2: %v", len(ef), ef) } if ef[0]["part"] != "epistola" || ef[0]["label"] != "Lekcja" { t.Errorf("ef[0] = %v, want epistola/Lekcja", ef[0]) } if ef[1]["part"] != "evangelium" || ef[1]["label"] != "Ewangelia" { t.Errorf("ef[1] = %v, want evangelium/Ewangelia", ef[1]) } var of []map[string]any if err := json.Unmarshal([]byte(PartLabels("of", "pl")), &of); err != nil { t.Fatalf("invalid JSON: %v", err) } if len(of) != 5 { t.Fatalf("of: got %d labels, want 5: %v", len(of), of) } // aklamacja was missing from the app's hardcoded list. var seen []string for _, e := range of { seen = append(seen, e["part"].(string)) } want := []string{"pierwsze_czytanie", "psalm", "drugie_czytanie", "aklamacja", "ewangelia"} for i := range want { if seen[i] != want[i] { t.Errorf("of order = %v, want %v", seen, want) break } } } func TestPartLabelsUnknownForm(t *testing.T) { // An unknown form is treated as the modern one, matching lectByForm. if got := PartLabels("nonsense", "en"); got == "[]" { t.Error("unknown form should fall back to the modern lectionary, not empty") } }