66128e7a5c
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
236 lines
9.0 KiB
Swift
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 })
|
|
}
|
|
}
|