a049e4f74b
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
162 lines
6.8 KiB
Swift
162 lines
6.8 KiB
Swift
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)
|
||
}
|
||
}
|