Files
FamilyMealPlanner/MealMoodTests/WatchWeekPayloadTests.swift
T
alexandrev-tibco 4579b9bb69 watch: enviar la semana aunque todavia no este planificada
Faltaba la otra mitad del "no se actualiza": makePayload devolvia nil cuando la
semana en curso no tenia plan creado, asi que el iPhone no mandaba nada — ni
respondia cuando el reloj pedia datos — y el reloj se quedaba con el ultimo
snapshot, el del miercoles. Ahora la semana viaja igualmente, vacia, y el reloj
dice que no hay nada planificado en vez de enseñar lo de hace dias.

Ademas el reloj muestra cuando se actualizo por ultima vez. Sin eso, un
snapshot viejo y uno recien llegado se ven igual, y no hay forma de saber si
el problema es el envio o lo que se pinta.

Refs #34, #35

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

192 lines
8.1 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
}
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)
}
}