Files
FamilyMealPlanner/MealMoodTests/WeekScheduleTests.swift
alexandrev-tibco 66128e7a5c premium: configurar días y comidas de una semana concreta
Hasta ahora includeWeekends y activeMealTypes vivían solo en AppSettings y
aplicaban a todo el calendario. WeekPlan puede ahora sobreescribirlos para su
semana (includeWeekendsOverride / mealTypesOverrideRaw) y WeekSchedule resuelve
qué manda en cada semana: su override si lo tiene, los ajustes globales si no.

Todo lo que pintaba días y comidas —planificador, exports, compartir texto,
widget, watch— pasa por settings.schedule(for:), así que una semana con fin de
semana añadido o con desayuno activo se ve igual en todas partes.

Crear o editar el override es premium; los ya guardados se respetan aunque la
suscripción caduque, para no borrar comidas ya planificadas. La hoja avisa y
pide confirmación cuando quitar un día o una comida se lleva por delante algo
ya planificado.

Refs #32

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013su1ttRiMeMYxkZJ1Y3246
2026-09-12 12:05:34 +02:00

236 lines
9.0 KiB
Swift

import XCTest
import SwiftData
@testable import MealMood
final class WeekScheduleTests: XCTestCase {
private func makeSettings(
mealTypes: [MealType] = [.lunch, .dinner],
includeWeekends: Bool = false
) -> AppSettings {
let settings = AppSettings()
settings.activeMealTypes = mealTypes
settings.includeWeekends = includeWeekends
return settings
}
// MARK: - Falling back to the global settings
func testScheduleWithoutOverride_followsSettings() {
let settings = makeSettings(mealTypes: [.dinner], includeWeekends: false)
let plan = WeekPlan(weekStartDate: Date())
let schedule = settings.schedule(for: plan)
XCTAssertEqual(schedule.mealTypes, [.dinner])
XCTAssertFalse(schedule.includeWeekends)
XCTAssertFalse(schedule.isCustomized)
XCTAssertEqual(schedule.dayRange, 0...4)
XCTAssertEqual(schedule.lastDay, 4)
}
func testScheduleForNilPlan_followsSettings() {
let settings = makeSettings(mealTypes: [.lunch, .dinner], includeWeekends: true)
let schedule = settings.schedule(for: nil)
XCTAssertEqual(schedule.mealTypes, [.lunch, .dinner])
XCTAssertTrue(schedule.includeWeekends)
XCTAssertEqual(schedule.dayRange, 0...6)
}
// MARK: - Per-week overrides
func testWeekendOverride_onlyAffectsThatWeek() {
let settings = makeSettings(includeWeekends: false)
let plan = WeekPlan(weekStartDate: Date())
let otherPlan = WeekPlan(weekStartDate: Date().addingDays(7))
plan.includeWeekendsOverride = true
XCTAssertTrue(settings.schedule(for: plan).includeWeekends)
XCTAssertEqual(settings.schedule(for: plan).dayRange, 0...6)
XCTAssertTrue(settings.schedule(for: plan).isCustomized)
XCTAssertFalse(settings.schedule(for: otherPlan).includeWeekends)
XCTAssertFalse(settings.includeWeekends, "the global setting must stay untouched")
}
func testMealTypesOverride_replacesTheGlobalMeals() {
let settings = makeSettings(mealTypes: [.dinner])
let plan = WeekPlan(weekStartDate: Date())
plan.mealTypesOverride = [.dinner, .breakfast, .snack]
XCTAssertEqual(settings.schedule(for: plan).mealTypes, [.breakfast, .snack, .dinner],
"meals are stored in chronological order, not selection order")
XCTAssertEqual(settings.activeMealTypes, [.dinner])
}
func testPartialOverride_keepsSettingsForTheOtherAxis() {
let settings = makeSettings(mealTypes: [.lunch, .dinner], includeWeekends: false)
let plan = WeekPlan(weekStartDate: Date())
plan.includeWeekendsOverride = true
XCTAssertEqual(settings.schedule(for: plan).mealTypes, [.lunch, .dinner])
XCTAssertTrue(settings.schedule(for: plan).includeWeekends)
}
func testEmptyMealTypesOverride_isTreatedAsNoOverride() {
let settings = makeSettings(mealTypes: [.lunch])
let plan = WeekPlan(weekStartDate: Date())
plan.mealTypesOverride = []
XCTAssertNil(plan.mealTypesOverrideRaw)
XCTAssertFalse(plan.hasScheduleOverride)
XCTAssertEqual(settings.schedule(for: plan).mealTypes, [.lunch])
}
func testClearScheduleOverride_returnsToSettings() {
let settings = makeSettings(mealTypes: [.dinner], includeWeekends: false)
let plan = WeekPlan(weekStartDate: Date())
plan.includeWeekendsOverride = true
plan.mealTypesOverride = [.lunch, .dinner]
XCTAssertTrue(plan.hasScheduleOverride)
plan.clearScheduleOverride()
XCTAssertFalse(plan.hasScheduleOverride)
XCTAssertEqual(settings.schedule(for: plan).mealTypes, [.dinner])
XCTAssertFalse(settings.schedule(for: plan).includeWeekends)
}
// MARK: - Premium gating
func testOverrideIsHonouredEvenWhenNotPremium() {
// A lapsed subscription must not silently drop the slots the user
// already planned in a customised week.
let settings = makeSettings(includeWeekends: false)
settings.isPremium = false
let plan = WeekPlan(weekStartDate: Date())
plan.includeWeekendsOverride = true
XCTAssertTrue(settings.schedule(for: plan).includeWeekends)
XCTAssertFalse(PremiumAccess.canCustomizeWeekSchedule(isPremium: false))
XCTAssertTrue(PremiumAccess.canCustomizeWeekSchedule(isPremium: true))
}
}
@MainActor
final class WeekScheduleSlotSyncTests: XCTestCase {
/// Held for the duration of the test: releasing the container tears the
/// store off the coordinator and the next SwiftData call traps.
private var container: ModelContainer!
override func tearDown() {
container = nil
super.tearDown()
}
private func makeContext() throws -> ModelContext {
container = try ModelContainer(
for: AppSettings.self, Dish.self, Tag.self, WeekPlan.self, MealSlot.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
return container.mainContext
}
func testApplyingAnOverrideAddsTheWeekendAndBreakfastSlots() throws {
let context = try makeContext()
let settings = AppSettings()
settings.isPremium = true
settings.activeMealTypes = [.lunch, .dinner]
settings.includeWeekends = false
context.insert(settings)
let monday = Date().startOfWeek()
let plan = DefaultDataService.createWeekPlan(for: monday, settings: settings, context: context)
XCTAssertEqual(plan.slotList.count, 10, "5 weekdays x 2 meals")
let viewModel = HomeViewModel()
viewModel.applyWeekSchedule(
includeWeekends: true,
mealTypes: [.breakfast, .lunch, .dinner],
plan: plan,
settings: settings,
context: context
)
XCTAssertEqual(plan.slotList.count, 21, "7 days x 3 meals")
XCTAssertTrue(plan.hasScheduleOverride)
XCTAssertTrue(plan.slotList.contains { $0.dayOfWeek == 6 && $0.mealTypeEnum == .breakfast })
// The global settings must be untouched this is a one-week change.
XCTAssertFalse(settings.includeWeekends)
XCTAssertEqual(settings.activeMealTypes, [.lunch, .dinner])
}
func testResettingTheOverrideRestoresTheGlobalSlots() throws {
let context = try makeContext()
let settings = AppSettings()
settings.isPremium = true
settings.activeMealTypes = [.dinner]
settings.includeWeekends = false
context.insert(settings)
let plan = DefaultDataService.createWeekPlan(for: Date().startOfWeek(), settings: settings, context: context)
let viewModel = HomeViewModel()
viewModel.applyWeekSchedule(
includeWeekends: true, mealTypes: [.lunch, .dinner],
plan: plan, settings: settings, context: context
)
XCTAssertEqual(plan.slotList.count, 14)
viewModel.resetWeekSchedule(plan: plan, settings: settings, context: context)
XCTAssertFalse(plan.hasScheduleOverride)
XCTAssertEqual(plan.slotList.count, 5, "back to 5 weekdays x dinner")
XCTAssertTrue(plan.slotList.allSatisfy { $0.mealTypeEnum == .dinner && $0.dayOfWeek <= 4 })
}
func testApplyingMatchingValuesClearsTheOverride() throws {
let context = try makeContext()
let settings = AppSettings()
settings.isPremium = true
settings.activeMealTypes = [.lunch, .dinner]
settings.includeWeekends = true
context.insert(settings)
let plan = DefaultDataService.createWeekPlan(for: Date().startOfWeek(), settings: settings, context: context)
let viewModel = HomeViewModel()
viewModel.applyWeekSchedule(
includeWeekends: true, mealTypes: [.lunch, .dinner],
plan: plan, settings: settings, context: context
)
XCTAssertFalse(plan.hasScheduleOverride,
"matching the global settings means the week keeps following them")
}
func testRemovingAMealDropsItsPlannedSlots() throws {
let context = try makeContext()
let settings = AppSettings()
settings.isPremium = true
settings.activeMealTypes = [.lunch, .dinner]
settings.includeWeekends = true
context.insert(settings)
let dish = Dish(name: "Tortilla")
context.insert(dish)
let plan = DefaultDataService.createWeekPlan(for: Date().startOfWeek(), settings: settings, context: context)
let saturdayLunch = try XCTUnwrap(plan.slotList.first { $0.dayOfWeek == 5 && $0.mealTypeEnum == .lunch })
saturdayLunch.dishId = dish.id
let viewModel = HomeViewModel()
viewModel.applyWeekSchedule(
includeWeekends: false, mealTypes: [.dinner],
plan: plan, settings: settings, context: context
)
XCTAssertEqual(plan.slotList.count, 5)
XCTAssertFalse(plan.slotList.contains { $0.mealTypeEnum == .lunch })
XCTAssertFalse(plan.slotList.contains { $0.dayOfWeek > 4 })
}
}