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:
@@ -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 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user