Files
FamilyMealPlanner/MealMoodTests/WatchWeekPayloadTests.swift
T
alexandrev-tibco bc684a099c watch: semanas navegables y los dias que no se planifican
Dos cosas que faltaban, las dos visibles en domingo con el fin de semana
desactivado:

El envio al reloj solo llevaba la semana en curso, asi que no habia forma de
ver las semanas ya planificadas por delante. Ahora viajan cuatro (la anterior,
la actual y las dos siguientes) y la vista de semana se desliza entre ellas,
arrancando en la actual.

Y un dia fuera del plan dejaba al reloj sin nada que enseñar: el domingo no
existe en el payload si no planificas fines de semana, asi que la complicacion
salia vacia y la app decia "abre MealMood en el iPhone", como si fuera un
problema de sincronizacion. Ahora se distingue: si hoy no se planifica se dice,
y se enseña el proximo dia que si tiene comidas — que es lo util en el reloj.

De paso, las semanas se comparan con el calendario alineado a lunes. Con el del
sistema, en las regiones donde la semana empieza en domingo, un domingo caia en
otra semana que su propio lunes y un envio recien hecho parecia caducado.

Refs #36, #37

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

269 lines
11 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import XCTest
import SwiftData
@testable import MealMood
/// Regression suite for "the watch showed Wednesday's meal on Saturday".
///
/// The snapshot the iPhone sends carries an `isToday` flag, which is true only
/// at the moment it was built. The watch outlives that moment it keeps the
/// last snapshot in its app group so "today" has to be resolved when the view
/// renders, never read from the payload.
final class WatchWeekPayloadTests: XCTestCase {
private let calendar = Calendar(identifier: .gregorian)
private func date(_ year: Int, _ month: Int, _ day: Int) -> Date {
calendar.date(from: DateComponents(year: year, month: month, day: day, hour: 12))!
}
private func makePayload(weekStart: Date, todayFlagOn flaggedDay: Int?) -> WatchWeekPayload {
let days = (0...6).map { offset in
WatchWeekPayload.Day(
dayOfWeek: offset,
title: "Day \(offset)",
isToday: offset == flaggedDay,
date: calendar.date(byAdding: .day, value: offset, to: weekStart)!,
meals: [
WatchWeekPayload.Meal(type: "dinner", label: "Dinner", name: "Dish \(offset)")
]
)
}
return WatchWeekPayload(
weekTitle: "Week",
days: days,
updatedAt: weekStart,
emptyMealText: "Not planned",
emptyDayText: "Nothing planned for today",
staleText: "Open MealMood on your iPhone"
)
}
// MARK: - The reported bug
func testSaturdayShowsSaturdayEvenIfTheSnapshotWasBuiltOnWednesday() {
let monday = date(2026, 9, 7)
// Snapshot built on Wednesday: that's the day it flagged as "today".
let payload = makePayload(weekStart: monday, todayFlagOn: 2)
let saturday = date(2026, 9, 12)
let today = payload.currentDay(now: saturday, calendar: calendar)
XCTAssertEqual(today?.dayOfWeek, 5, "Saturday must win over the frozen Wednesday flag")
XCTAssertEqual(today?.meals.first?.name, "Dish 5")
XCTAssertFalse(payload.isStale(now: saturday, calendar: calendar))
}
func testSnapshotFromAnotherWeekHasNoToday() {
let lastMonday = date(2026, 9, 7)
let payload = makePayload(weekStart: lastMonday, todayFlagOn: 2)
let nextSaturday = date(2026, 9, 19)
XCTAssertNil(payload.currentDay(now: nextSaturday, calendar: calendar))
XCTAssertTrue(payload.isStale(now: nextSaturday, calendar: calendar),
"showing last week's meals as today's is exactly what must not happen")
}
func testEachDayKnowsWhetherItIsToday() {
let monday = date(2026, 9, 7)
let payload = makePayload(weekStart: monday, todayFlagOn: 2)
let thursday = date(2026, 9, 10)
XCTAssertTrue(payload.days[3].isCurrentDay(now: thursday, calendar: calendar))
XCTAssertFalse(payload.days[2].isCurrentDay(now: thursday, calendar: calendar),
"the day flagged when the snapshot was built is not today any more")
}
// MARK: - Empty states
func testDayWithNoDishesIsEmpty() {
let empty = WatchWeekPayload.Day(
dayOfWeek: 1, title: "Tue", isToday: true, date: date(2026, 9, 8),
meals: [
WatchWeekPayload.Meal(type: "lunch", label: "Lunch", name: nil),
WatchWeekPayload.Meal(type: "dinner", label: "Dinner", name: nil)
]
)
XCTAssertTrue(empty.isEmpty)
let planned = WatchWeekPayload.Day(
dayOfWeek: 1, title: "Tue", isToday: true, date: date(2026, 9, 8),
meals: [
WatchWeekPayload.Meal(type: "lunch", label: "Lunch", name: nil),
WatchWeekPayload.Meal(type: "dinner", label: "Dinner", name: "Tortilla")
]
)
XCTAssertFalse(planned.isEmpty, "one planned meal is enough for the day not to be empty")
}
func testPayloadCarriesTheLocalizedEmptyTexts() {
// The watch bundle has no strings of its own, so these have to travel.
let payload = makePayload(weekStart: date(2026, 9, 7), todayFlagOn: 0)
XCTAssertEqual(payload.emptyMealText, "Not planned")
XCTAssertEqual(payload.emptyDayText, "Nothing planned for today")
XCTAssertEqual(payload.staleText, "Open MealMood on your iPhone")
}
// MARK: - Compatibility with snapshots already on people's watches
func testSnapshotWrittenBeforeTheFixStillDecodes() throws {
// Shape written by 2.1.0: no `date`, no texts. Stamped inside the
// current week, which is when its weekday offsets still mean something.
let wednesday = date(2026, 9, 9)
let json = """
{
"weekTitle": "7 13 sep",
"updatedAt": \(wednesday.timeIntervalSinceReferenceDate),
"days": [
{"dayOfWeek": 0, "title": "Mon 7", "isToday": false,
"meals": [{"type": "dinner", "label": "Dinner", "name": "Pizza"}]},
{"dayOfWeek": 2, "title": "Wed 9", "isToday": true,
"meals": [{"type": "dinner", "label": "Dinner", "name": "Salmon"}]}
]
}
"""
let payload = try XCTUnwrap(WatchWeekPayload.decode(Data(json.utf8)))
XCTAssertEqual(payload.days.count, 2)
XCTAssertNil(payload.days[0].date)
XCTAssertNil(payload.emptyMealText, "old snapshots have no texts; the UI falls back")
// Without dates it can only fall back to the weekday offset.
XCTAssertEqual(payload.currentDay(now: wednesday, calendar: calendar)?.dayOfWeek, 2)
}
func testDatelessSnapshotFromAnotherWeekIsStale() throws {
// The watch that reported the bug had one of these: no dates, built in a
// previous week. Trusting its weekday offsets is what showed the wrong
// day, so it now counts as stale instead.
let lastWednesday = date(2026, 9, 2)
let json = """
{
"weekTitle": "31 ago 6 sep",
"updatedAt": \(lastWednesday.timeIntervalSinceReferenceDate),
"days": [
{"dayOfWeek": 5, "title": "Sat 5", "isToday": false,
"meals": [{"type": "dinner", "label": "Dinner", "name": "Old dinner"}]}
]
}
"""
let payload = try XCTUnwrap(WatchWeekPayload.decode(Data(json.utf8)))
let saturday = date(2026, 9, 12)
XCTAssertNil(payload.currentDay(now: saturday, calendar: calendar))
XCTAssertTrue(payload.isStale(now: saturday, calendar: calendar))
}
// MARK: - A week with no plan yet
@MainActor
func testWeekWithoutAPlanStillProducesASnapshot() throws {
// Staying quiet here is what left the watch showing the previous
// week: no plan meant no push at all, so the old snapshot survived.
let container = try ModelContainer(
for: AppSettings.self, Dish.self, Tag.self, WeekPlan.self, MealSlot.self, ShoppingItem.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = ModelContext(container)
let settings = AppSettings()
settings.activeMealTypes = [.lunch, .dinner]
settings.includeWeekends = true
context.insert(settings)
let payload = try XCTUnwrap(
WatchSyncService.makePayload(plan: nil, dishes: [], settings: settings),
"a missing plan must still produce the current week"
)
XCTAssertEqual(payload.days.count, 7)
XCTAssertNotNil(payload.currentDay(), "today must be in there")
XCTAssertTrue(payload.currentDay()?.isEmpty ?? false, "and it must read as nothing planned")
XCTAssertFalse(payload.isStale(), "an empty current week is not stale")
_ = container
}
// MARK: - Days that are deliberately not planned
@MainActor
func testSundayWithWeekendsOffSaysSoAndPointsAtMonday() throws {
// The reported case: weekends off, so Sunday isn't in the payload at
// all. That is not "out of date" it's a day nobody plans.
let container = try ModelContainer(
for: AppSettings.self, Dish.self, Tag.self, WeekPlan.self, MealSlot.self, ShoppingItem.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = ModelContext(container)
let settings = AppSettings()
settings.activeMealTypes = [.dinner]
settings.includeWeekends = false
context.insert(settings)
let sunday = date(2026, 9, 13)
let thisMonday = sunday.startOfWeek()
let nextMonday = thisMonday.addingDays(7)
let dish = Dish(name: "Lentejas")
context.insert(dish)
let nextWeekPlan = WeekPlan(weekStartDate: nextMonday)
let slot = MealSlot(dayOfWeek: 0, mealType: MealType.dinner.rawValue)
slot.dishId = dish.id
slot.weekPlan = nextWeekPlan
nextWeekPlan.slotList.append(slot)
context.insert(nextWeekPlan)
let payload = try XCTUnwrap(
WatchSyncService.makePayload(plans: [nextWeekPlan], dishes: [dish], settings: settings, now: sunday)
)
XCTAssertTrue(payload.coversCurrentWeek(now: sunday, calendar: calendar))
XCTAssertFalse(payload.isStale(now: sunday, calendar: calendar),
"a Sunday you don't plan is not a stale snapshot")
switch payload.todayState(now: sunday, calendar: calendar) {
case .notPlannedToday(let next):
XCTAssertEqual(next?.meals.first?.name, "Lentejas",
"it should point at the next planned day, not show nothing")
default:
XCTFail("Sunday with weekends off must report as not planned")
}
_ = container
}
// MARK: - Moving between weeks
@MainActor
func testThePhoneSendsTheNeighbouringWeeks() throws {
let container = try ModelContainer(
for: AppSettings.self, Dish.self, Tag.self, WeekPlan.self, MealSlot.self, ShoppingItem.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = ModelContext(container)
let settings = AppSettings()
settings.activeMealTypes = [.lunch, .dinner]
settings.includeWeekends = true
context.insert(settings)
let now = date(2026, 9, 13)
let payload = try XCTUnwrap(
WatchSyncService.makePayload(plans: [], dishes: [], settings: settings, now: now)
)
let weeks = try XCTUnwrap(payload.weeks)
XCTAssertEqual(weeks.count, WatchSyncService.weekOffsets.count,
"without several weeks the watch has nothing to page through")
XCTAssertEqual(weeks.filter { $0.isCurrentWeek(now: now, calendar: calendar) }.count, 1)
XCTAssertEqual(payload.weekStartDate, now.startOfWeek())
// Weeks arrive in order, so paging forward moves forward in time.
XCTAssertEqual(weeks.map(\.weekStartDate), weeks.map(\.weekStartDate).sorted())
_ = container
}
func testWeekdayOffsetMapsMondayToZeroAndSundayToSix() {
XCTAssertEqual(WatchWeekPayload.weekdayOffset(for: date(2026, 9, 7), calendar: calendar), 0)
XCTAssertEqual(WatchWeekPayload.weekdayOffset(for: date(2026, 9, 12), calendar: calendar), 5)
XCTAssertEqual(WatchWeekPayload.weekdayOffset(for: date(2026, 9, 13), calendar: calendar), 6)
}
}