- Leitor bíblico consumindo a API NBV (bible.falehandix.com.br): lista de livros por testamento e leitura de capítulos - Plano de 180 dias com três trilhas diárias (História, Sabedoria e Profetas, Novo Testamento) geradas deterministicamente sobre o cânon embutido (1.189 capítulos, cobertura testada) - Lembrete diário via notificação local recorrente (UNCalendarNotificationTrigger, identificador fixo, sem duplicatas), com pedido de autorização em contexto, configurações de horário/som, e deep link da notificação para a Leitura de Hoje - Estado do plano em SwiftData: progresso, pausa/retomada, reinício e conclusão preservando preferências do lembrete - 15 testes unitários (Testing) e 2 testes de UI (XCUIAutomation) - Ícone do app a partir do logo biblia.png Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
192 lines
6.6 KiB
Swift
192 lines
6.6 KiB
Swift
//
|
||
// Bible_WeekTests.swift
|
||
// Bible-WeekTests
|
||
//
|
||
// Testes do Plano de 180 Dias e do lembrete diário (lógica pura,
|
||
// sem depender do UNUserNotificationCenter real).
|
||
//
|
||
|
||
import Foundation
|
||
import Testing
|
||
import UserNotifications
|
||
@testable import Bible_Week
|
||
|
||
// MARK: - Cronograma do plano
|
||
|
||
struct PlanScheduleTests {
|
||
|
||
@Test func planHas180Days() {
|
||
#expect(PlanSchedule.days.count == 180)
|
||
#expect(PlanSchedule.days.first?.day == 1)
|
||
#expect(PlanSchedule.days.last?.day == 180)
|
||
}
|
||
|
||
@Test func everyDayHasAllThreeTracksWithChapters() {
|
||
for day in PlanSchedule.days {
|
||
#expect(day.readings.count == PlanTrack.allCases.count)
|
||
for reading in day.readings {
|
||
#expect(!reading.chapters.isEmpty, "Dia \(day.day), trilha \(reading.track) sem capítulos")
|
||
#expect(!reading.label.isEmpty)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Cada trilha deve cobrir todos os seus capítulos, em ordem canônica,
|
||
/// sem lacunas e sem repetições — a Bíblia inteira em 180 dias.
|
||
@Test func scheduleCoversEntireCanonExactlyOnce() {
|
||
for track in PlanTrack.allCases {
|
||
let scheduled = PlanSchedule.days.flatMap { day in
|
||
day.reading(for: track)?.chapters ?? []
|
||
}
|
||
let expected = track.books.flatMap { book in
|
||
(1...book.chapterCount).map {
|
||
ChapterRef(bookCode: book.code, bookName: book.name, chapter: $0)
|
||
}
|
||
}
|
||
#expect(scheduled == expected, "Trilha \(track) não cobre o cânon exatamente")
|
||
}
|
||
let total = PlanTrack.allCases.reduce(0) { sum, track in
|
||
sum + track.books.reduce(0) { $0 + $1.chapterCount }
|
||
}
|
||
#expect(total == 1189)
|
||
}
|
||
|
||
@Test func labelsCollapseConsecutiveChaptersIntoRanges() {
|
||
let reading = TrackReading(track: .history, chapters: [
|
||
ChapterRef(bookCode: "GEN", bookName: "Gênesis", chapter: 34),
|
||
ChapterRef(bookCode: "GEN", bookName: "Gênesis", chapter: 35),
|
||
ChapterRef(bookCode: "GEN", bookName: "Gênesis", chapter: 36),
|
||
])
|
||
#expect(reading.label == "Gênesis 34–36")
|
||
|
||
let crossBook = TrackReading(track: .history, chapters: [
|
||
ChapterRef(bookCode: "GEN", bookName: "Gênesis", chapter: 50),
|
||
ChapterRef(bookCode: "EXO", bookName: "Êxodo", chapter: 1),
|
||
])
|
||
#expect(crossBook.label == "Gênesis 50 e Êxodo 1")
|
||
}
|
||
}
|
||
|
||
// MARK: - Estado do plano
|
||
|
||
struct ReadingPlanStateTests {
|
||
|
||
@Test func currentDayAdvancesWhenAllTracksCompleted() {
|
||
let plan = ReadingPlanState()
|
||
#expect(plan.currentDay == 1)
|
||
|
||
// Concluir só uma trilha não avança o dia.
|
||
plan.setCompleted(true, day: 1, track: .history)
|
||
#expect(plan.currentDay == 1)
|
||
|
||
plan.setCompleted(true, day: 1, track: .wisdom)
|
||
plan.setCompleted(true, day: 1, track: .newTestament)
|
||
#expect(plan.currentDay == 2)
|
||
#expect(plan.completedDaysCount == 1)
|
||
}
|
||
|
||
@Test func settingCompletedTwiceDoesNotDuplicate() {
|
||
let plan = ReadingPlanState()
|
||
plan.setCompleted(true, day: 1, track: .history)
|
||
plan.setCompleted(true, day: 1, track: .history)
|
||
#expect(plan.completedKeys.count == 1)
|
||
|
||
plan.setCompleted(false, day: 1, track: .history)
|
||
#expect(plan.completedKeys.isEmpty)
|
||
}
|
||
|
||
@Test func changingReminderTimeReplacesPreviousValue() {
|
||
let plan = ReadingPlanState()
|
||
#expect(plan.reminderHour == 20)
|
||
#expect(plan.reminderMinute == 0)
|
||
|
||
plan.reminderHour = 7
|
||
plan.reminderMinute = 30
|
||
#expect(plan.reminderTimeText == "07:30")
|
||
}
|
||
|
||
@Test func pausePreservesReminderPreference() {
|
||
let plan = ReadingPlanState()
|
||
plan.reminderEnabled = true
|
||
plan.reminderHour = 7
|
||
plan.reminderMinute = 30
|
||
|
||
plan.isPaused = true
|
||
#expect(plan.reminderEnabled)
|
||
#expect(plan.reminderHour == 7)
|
||
#expect(plan.reminderMinute == 30)
|
||
|
||
plan.isPaused = false
|
||
#expect(plan.reminderEnabled)
|
||
#expect(plan.reminderTimeText == "07:30")
|
||
}
|
||
|
||
@Test func planCompletionDetection() {
|
||
let plan = ReadingPlanState()
|
||
for day in 1...PlanSchedule.totalDays {
|
||
for track in PlanTrack.allCases {
|
||
plan.setCompleted(true, day: day, track: track)
|
||
}
|
||
}
|
||
#expect(plan.isCompleted)
|
||
#expect(plan.completedDaysCount == 180)
|
||
#expect(plan.progress == 1.0)
|
||
}
|
||
|
||
@Test func restartClearsProgressButKeepsReminderPreference() {
|
||
let plan = ReadingPlanState()
|
||
plan.reminderEnabled = true
|
||
plan.reminderHour = 20
|
||
plan.reminderMinute = 0
|
||
plan.setCompleted(true, day: 1, track: .history)
|
||
plan.isPaused = true
|
||
|
||
plan.restart()
|
||
|
||
#expect(plan.completedKeys.isEmpty)
|
||
#expect(plan.currentDay == 1)
|
||
#expect(!plan.isPaused)
|
||
// Preferências de lembrete sobrevivem ao reinício.
|
||
#expect(plan.reminderEnabled)
|
||
#expect(plan.reminderTimeText == "20:00")
|
||
}
|
||
}
|
||
|
||
// MARK: - Conteúdo do lembrete
|
||
|
||
struct ReminderContentTests {
|
||
|
||
@Test func contentIncludesJourneyDayWhenAvailable() {
|
||
let content = ReadingReminderService.makeContent(currentDay: 34, soundEnabled: true)
|
||
#expect(content.title.contains("34"))
|
||
#expect(!content.body.isEmpty)
|
||
#expect(content.sound != nil)
|
||
}
|
||
|
||
@Test func contentFallsBackToDefaultTitleWithoutDay() {
|
||
let content = ReadingReminderService.makeContent(currentDay: nil, soundEnabled: true)
|
||
#expect(content.title == "📖 Sua leitura de hoje")
|
||
}
|
||
|
||
@Test func soundOffProducesSilentNotification() {
|
||
let content = ReadingReminderService.makeContent(currentDay: 1, soundEnabled: false)
|
||
#expect(content.sound == nil)
|
||
}
|
||
|
||
@Test func userInfoRoutesToReadingPlanToday() {
|
||
let content = ReadingReminderService.makeContent(currentDay: 1, soundEnabled: true)
|
||
#expect(content.userInfo[NotificationUserInfo.destinationKey] as? String == NotificationUserInfo.destinationReadingPlan)
|
||
#expect(content.userInfo[NotificationUserInfo.planIdKey] as? String == "bible-180")
|
||
#expect(content.userInfo[NotificationUserInfo.screenKey] as? String == "today")
|
||
}
|
||
|
||
@Test func messagesAvoidPressuringLanguage() {
|
||
for message in ReadingReminderService.motivationalMessages {
|
||
#expect(!message.lowercased().contains("streak"))
|
||
#expect(!message.lowercased().contains("sequência"))
|
||
#expect(!message.lowercased().contains("esqueceu"))
|
||
#expect(!message.lowercased().contains("perdendo"))
|
||
}
|
||
}
|
||
}
|