718eef16bf
- 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
162 lines
6.3 KiB
Swift
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"))
|
|
}
|
|
}
|