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
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user