Files
bible/Bible-WeekTests/Bible_WeekTests.swift
Matheus A Silveira 80185726af Adiciona Plano Bíblia em 180 Dias com lembrete diário e ícone do app
- 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>
2026-09-04 18:54:48 -03:00

192 lines
6.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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 3436")
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"))
}
}
}