package tui import ( "errors" "strings" "testing" tea "github.com/charmbracelet/bubbletea" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/liturgy" ) func TestVersionCycle(t *testing.T) { m := New(config.Config{Versions: []string{"bt", "wuj", "vul"}, DefaultVersion: "bt"}, "", "") if m.version() != "bt" { t.Fatalf("start = %q", m.version()) } m = m.cycleVersion(+1) if m.version() != "wuj" { t.Errorf("after tab = %q", m.version()) } m = m.cycleVersion(-1) if m.version() != "bt" { t.Errorf("after shift-tab = %q", m.version()) } } func TestOfflineDropsBT(t *testing.T) { m := New(config.Config{Versions: []string{"bt", "wuj", "vul"}, DefaultVersion: "bt", Offline: true}, "", "") for _, v := range m.versions { if v == "bt" { t.Error("offline model kept bt") } } } // TestBodyLinesLocalisesMessages checks that the loading/no-readings/error // messages follow cfg.UILanguage. func TestBodyLinesLocalisesMessages(t *testing.T) { en := Model{cfg: config.Config{UILanguage: "en"}, loading: true} if lines := en.bodyLines(80); len(lines) == 0 || !strings.Contains(lines[0], "loading") { t.Errorf("en loading bodyLines = %v, want it to contain %q", lines, "loading") } pl := Model{cfg: config.Config{UILanguage: "pl"}, loading: true} if lines := pl.bodyLines(80); len(lines) == 0 || !strings.Contains(lines[0], "ładowanie") { t.Errorf("pl loading bodyLines = %v, want it to contain %q", lines, "ładowanie") } enEmpty := Model{cfg: config.Config{UILanguage: "en"}, date: "2026-07-22"} if lines := enEmpty.bodyLines(80); len(lines) == 0 || !strings.Contains(lines[0], "no readings for") { t.Errorf("en no-readings bodyLines = %v, want it to contain %q", lines, "no readings for") } } // TestBodyLinesSingleSpaceConcatenation is a regression test for the // i18n-migration whitespace bug: i18n.UI.NoReadingsFor/ErrorPrefix must // carry exactly one trailing space, since bodyLines concatenates the date/ // error string directly onto them with no separator of its own -- the // pre-i18n TUI code produced "błąd: " + err / "brak czytań na " + date (one // space), and the i18n fields regressed to two. func TestBodyLinesSingleSpaceConcatenation(t *testing.T) { enEmpty := Model{cfg: config.Config{UILanguage: "en"}, date: "2026-07-22"} lines := enEmpty.bodyLines(80) if len(lines) == 0 || !strings.Contains(lines[0], "no readings for 2026-07-22") { t.Errorf("en no-readings bodyLines[0] = %q, want it to contain %q (single space)", lines[0], "no readings for 2026-07-22") } if strings.Contains(lines[0], "for 2026") { t.Errorf("en no-readings bodyLines[0] = %q, has 2+ spaces before the date", lines[0]) } plEmpty := Model{cfg: config.Config{UILanguage: "pl"}, date: "2026-07-22"} lines = plEmpty.bodyLines(80) if len(lines) == 0 || !strings.Contains(lines[0], "brak czytań na 2026-07-22") { t.Errorf("pl no-readings bodyLines[0] = %q, want it to contain %q (single space)", lines[0], "brak czytań na 2026-07-22") } errText := "boom" enErr := Model{cfg: config.Config{UILanguage: "en"}, err: errors.New(errText)} lines = enErr.bodyLines(80) if len(lines) == 0 || !strings.Contains(lines[0], "error: "+errText) { t.Errorf("en error bodyLines[0] = %q, want it to contain %q (single space)", lines[0], "error: "+errText) } if strings.Contains(lines[0], "error: ") { t.Errorf("en error bodyLines[0] = %q, has 2+ spaces after \"error:\"", lines[0]) } plErr := Model{cfg: config.Config{UILanguage: "pl"}, err: errors.New(errText)} lines = plErr.bodyLines(80) if len(lines) == 0 || !strings.Contains(lines[0], "błąd: "+errText) { t.Errorf("pl error bodyLines[0] = %q, want it to contain %q (single space)", lines[0], "błąd: "+errText) } if strings.Contains(lines[0], "błąd: ") { t.Errorf("pl error bodyLines[0] = %q, has 2+ spaces after \"błąd:\"", lines[0]) } } // TestBodyLinesLocalisesHeading checks that a modern-lectionary section // heading's label word follows cfg.UILanguage while the citation stays // exactly as scraped (render.LocalizeHeading, brief §3b). func TestBodyLinesLocalisesHeading(t *testing.T) { sec := liturgy.Section{Heading: "Ewangelia (J 20, 1. 11-18)", PartID: "ewangelia"} en := Model{cfg: config.Config{UILanguage: "en", Lectionary: "new"}, sections: []liturgy.Section{sec}} body := strings.Join(en.bodyLines(80), "\n") if !strings.Contains(body, "Gospel (J 20, 1. 11-18)") { t.Errorf("en bodyLines missing localised heading: %q", body) } pl := Model{cfg: config.Config{UILanguage: "pl", Lectionary: "new"}, sections: []liturgy.Section{sec}} body = strings.Join(pl.bodyLines(80), "\n") if !strings.Contains(body, "Ewangelia (J 20, 1. 11-18)") { t.Errorf("pl bodyLines missing unchanged heading: %q", body) } } // TestDayInfoLineShown checks the day-info line (celebration Name, plus // its temporal Season if any) is rendered under the top header bar when // the loaded source carries one, and that pageSize grows its chrome // allowance to account for the extra line. func TestDayInfoLineShown(t *testing.T) { m := Model{ cfg: config.Config{UILanguage: "en"}, date: "2026-07-22", dayInfo: liturgy.DayInfo{Name: "St. Mary Magdalene", Season: "Feria IV after VIII Sunday after Pentecost"}, } out := m.View() if !strings.Contains(out, "St. Mary Magdalene") { t.Errorf("View() missing day-info name: %q", out) } if !strings.Contains(out, "Feria IV after VIII Sunday after Pentecost") { t.Errorf("View() missing day-info season: %q", out) } if got := m.headerLines(); got != 2 { t.Errorf("headerLines() = %d, want 2 when DayInfo.Name is set", got) } } // TestDayInfoLineOmittedWhenEmpty checks the header block stays a single // line (headerLines() == 1) when the source yielded no DayInfo -- e.g. a // harvested-sigla offline load, which carries citations only. func TestDayInfoLineOmittedWhenEmpty(t *testing.T) { m := Model{cfg: config.Config{UILanguage: "en"}, date: "2026-07-22"} if line := m.dayInfoLine(); line != "" { t.Errorf("dayInfoLine() = %q, want empty when DayInfo is zero", line) } if got := m.headerLines(); got != 1 { t.Errorf("headerLines() = %d, want 1 when DayInfo is zero", got) } } // TestFooterKeysLocalised checks the footer keybar text follows // cfg.UILanguage. func TestFooterKeysLocalised(t *testing.T) { en := Model{cfg: config.Config{UILanguage: "en"}, date: "2026-07-22"} if out := en.View(); !strings.Contains(out, "q quit") { t.Errorf("en View() footer missing %q: %q", "q quit", out) } pl := Model{cfg: config.Config{UILanguage: "pl"}, date: "2026-07-22"} if out := pl.View(); !strings.Contains(out, "q wyjście") { t.Errorf("pl View() footer missing %q: %q", "q wyjście", out) } } func TestJumpToDate(t *testing.T) { send := func(m Model, s string) Model { for _, r := range s { nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{r}}) m = nm.(Model) } return m } m := New(config.Default(), "2026-07-22", "wuj") m = send(m, "d") if !m.jumping { t.Fatal("'d' did not enter jump mode") } m = send(m, "2026-01-02") if m.jumpBuf != "2026-01-02" { t.Fatalf("jumpBuf = %q", m.jumpBuf) } nm, _ := m.Update(tea.KeyMsg{Type: tea.KeyEnter}) m = nm.(Model) if m.jumping { t.Error("still in jump mode after Enter") } if m.date != "2026-01-02" { t.Errorf("date = %q, want 2026-01-02", m.date) } // Esc cancels without changing the date. m2 := New(config.Default(), "2026-07-22", "wuj") m2 = send(m2, "d") m2 = send(m2, "2099") nm2, _ := m2.Update(tea.KeyMsg{Type: tea.KeyEsc}) m2 = nm2.(Model) if m2.jumping || m2.jumpBuf != "" || m2.date != "2026-07-22" { t.Errorf("esc did not cancel cleanly: jumping=%v buf=%q date=%q", m2.jumping, m2.jumpBuf, m2.date) } }