Files
FamilyMealPlanner/MealMoodTests/WatchWeekPayloadTests.swift
T
alexandrev-tibco a049e4f74b watch: el dia de hoy se resuelve al pintar, no al generar el envio
El iPhone marcaba isToday al construir el snapshot y el reloj se creia esa
marca para siempre. Como el snapshot sobrevive en el app group, un sabado
seguia enseñando la cena del miercoles: el dia que era cuando se genero.

Ahora cada dia viaja con su fecha real y el reloj busca hoy cuando dibuja. Si
el snapshot es de otra semana no hay "hoy" que enseñar, asi que en vez de
colar la comida de otro dia lo dice y pide abrir el iPhone. Para snapshots
guardados por versiones anteriores, que no traen fecha, solo se confia en el
indice del dia mientras el propio snapshot sea de esta semana.

Ademas el reloj pide datos al abrirse y el iPhone, en vez de responder con lo
ultimo que cacheo, reconstruye la semana desde el store — que es la otra mitad
del "no se actualiza": el cache podia ser de hace dias.

Y cuando no hay nada planificado se dice, que antes era un guion indistinguible
de una comida marcada como no planificada: "Hoy no hay nada planificado" para
el dia entero y "Sin planificar" por comida, en los 6 idiomas (el bundle del
reloj no tiene cadenas propias, viajan en el envio).

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 13:21:21 +02:00

162 lines
6.8 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
@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))
}
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)
}
}