aboutsummaryrefslogtreecommitdiff
path: root/mobile/mobile_test.go
blob: 1a115f3e96ec740e7a3c95b3053c8f0e789a7cfd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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")
	}
}