Files
FamilyMealPlanner/MealMoodTests/DateHelpersTests.swift
T
alexandrev-tibco 718eef16bf reglas: el panel explica que se incumple y como arreglarlo; stats por etiqueta
- RuleViolation.reasons: el motor devuelve ahora QUE regla rompe cada
  asignacion (repetido en la semana, maximo por semana superado, no
  consecutivo, no mismo dia, solo comida/cena, solo entre semana o fin
  de semana) con su etiqueta y limite
- Panel de avisos: bloque con cada regla rota en lenguaje claro + una
  sugerencia de que hacer, y boton nuevo "Cambiar plato" que abre el
  selector de ese hueco (antes solo se podia quitar o ignorar)
- Estadisticas: matriz "Etiquetas por tipo de comida" (p.ej. cuantas
  cenas son de pescado) y "Platos por etiqueta" del catalogo
- Tests: 4 casos de las razones de incumplimiento; harness que renderiza
  las secciones nuevas de stats a /tmp/stats_render.png
- Strings en 6 idiomas

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
2026-09-10 21:06:42 +02:00

162 lines
6.3 KiB
Swift

import XCTest
import SwiftUI
import SwiftData
@testable import MealMood
/// Not a real assertion suite: renders every export style with sample data to
/// /tmp/mealmood-export-previews/ so design changes can be reviewed as images.
final class ExportStyleRenderTests: XCTestCase {
@MainActor
func testRenderAllExportStyles() throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(
for: AppSettings.self, Dish.self, Tag.self, WeekPlan.self, MealSlot.self,
configurations: config
)
let context = container.mainContext
let settings = AppSettings()
settings.activeMealTypes = [.lunch, .dinner]
settings.includeWeekends = true
settings.language = "es"
context.insert(settings)
let names = [
"Tortilla de patatas", "Salmón al horno", "Ensalada César",
"Lentejas con chorizo", "Pizza casera", "Pollo asado con patatas",
"Crema de calabaza", "Espaguetis boloñesa", "Merluza en salsa verde",
"Arroz tres delicias", "Fajitas de pollo", "Gazpacho", "Croquetas caseras", "Sopa de fideos"
]
let dishes = names.map { Dish(name: $0) }
dishes.forEach { context.insert($0) }
let monday = Calendar(identifier: .gregorian).date(from: DateComponents(year: 2026, month: 8, day: 31))!
let plan = WeekPlan(weekStartDate: monday)
context.insert(plan)
var dishIndex = 0
for day in 0...6 {
for meal in [MealType.lunch, .dinner] {
let slot = MealSlot(dayOfWeek: day, mealType: meal.rawValue)
if day == 5 && meal == .dinner {
slot.isEatingOut = true
} else {
slot.dishId = dishes[dishIndex % dishes.count].id
if day == 2 && meal == .dinner {
slot.secondaryDishId = dishes[(dishIndex + 5) % dishes.count].id
}
dishIndex += 1
}
slot.weekPlan = plan
plan.slotList.append(slot)
context.insert(slot)
}
}
try context.save()
let outputDir = URL(fileURLWithPath: "/tmp/mealmood-export-previews")
try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true)
for style in WeekExportStyle.allCases {
let renderer = ImageRenderer(content: WeekPlanShareView(
plan: plan, settings: settings, dishes: dishes, tags: [],
exportStyleOverride: style
))
let size = WeekPlanShareView.canvasSize(for: style)
renderer.proposedSize = ProposedViewSize(width: size.width, height: size.height)
renderer.scale = 1
let image = try XCTUnwrap(renderer.uiImage, "render failed for \(style.rawValue)")
let data = try XCTUnwrap(image.pngData())
try data.write(to: outputDir.appendingPathComponent("export-\(style.rawValue).png"))
}
}
}
final class DateHelpersTests: XCTestCase {
func testStartOfWeekReturnsMonday() {
var components = DateComponents()
components.year = 2026
components.month = 2
components.day = 18 // Wednesday
let calendar = Calendar(identifier: .gregorian)
let input = calendar.date(from: components)!
let start = input.startOfWeek()
let weekday = calendar.component(.weekday, from: start)
XCTAssertEqual(weekday, 2) // Monday in Gregorian calendar
}
func testCombineDateAndTime() {
let calendar = Calendar(identifier: .gregorian)
let date = calendar.date(from: DateComponents(year: 2026, month: 2, day: 20))!
let time = calendar.date(from: DateComponents(hour: 21, minute: 30))!
let combined = combineDateAndTime(date: date, time: time)
let parts = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: combined)
XCTAssertEqual(parts.year, 2026)
XCTAssertEqual(parts.month, 2)
XCTAssertEqual(parts.day, 20)
XCTAssertEqual(parts.hour, 21)
XCTAssertEqual(parts.minute, 30)
}
}
/// Renders the stats screen with sample data to /tmp so the new sections can
/// be reviewed as an image.
final class StatsRenderTests: XCTestCase {
@MainActor
func testRenderStats() throws {
let tags = [
Tag(name: "Pasta", nameEN: "Pasta", color: "#E8A87C"),
Tag(name: "Pescado", nameEN: "Fish", color: "#7CB7E8"),
Tag(name: "Verdura", nameEN: "Veggie", color: "#8CD790"),
Tag(name: "Carne", nameEN: "Meat", color: "#E87C7C")
]
let dishes: [Dish] = [
Dish(name: "Espaguetis", tagIds: [tags[0].id]),
Dish(name: "Lasaña", tagIds: [tags[0].id, tags[3].id]),
Dish(name: "Merluza", tagIds: [tags[1].id]),
Dish(name: "Salmón", tagIds: [tags[1].id]),
Dish(name: "Ensalada", tagIds: [tags[2].id]),
Dish(name: "Pollo asado", tagIds: [tags[3].id]),
Dish(name: "Crema de calabaza", tagIds: [tags[2].id])
]
var plans: [WeekPlan] = []
for week in 0..<3 {
let plan = WeekPlan(weekStartDate: Date().startOfWeek().addingDays(-7 * week))
var i = week
for day in 0...4 {
for meal in ["lunch", "dinner"] {
let slot = MealSlot(dayOfWeek: day, mealType: meal, dishId: dishes[i % dishes.count].id)
slot.weekPlan = plan
plan.slotList.append(slot)
i += 1
}
}
plans.append(plan)
}
let view = StatsView(weekPlans: plans, allDishes: dishes, allTags: tags, language: .spanish)
// Render just the new sections: ImageRenderer can't draw a NavigationStack.
let content = VStack(alignment: .leading, spacing: 24) {
view.tagByMealSection
view.dishesPerTagSection
}
.padding(16)
.frame(width: 390)
.background(Color.mealMoodBackground)
let renderer = ImageRenderer(content: content)
renderer.scale = 2
let image = try XCTUnwrap(renderer.uiImage)
let data = try XCTUnwrap(image.pngData())
try data.write(to: URL(fileURLWithPath: "/tmp/stats_render.png"))
}
}