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>
This commit is contained in:
2026-09-04 18:54:48 -03:00
parent 4d36df488f
commit 80185726af
19 changed files with 1583 additions and 110 deletions

View File

@@ -2,17 +2,190 @@
// Bible_WeekTests.swift
// Bible-WeekTests
//
// Created by Matheus A Silveira on 04/09/26.
// 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
struct Bible_WeekTests {
// MARK: - Cronograma do plano
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
// Swift Testing Documentation
// https://developer.apple.com/documentation/testing
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"))
}
}
}