feat(meals): вкладка «Питание» — напоминания о еде по времени суток
Новая модель Meal — напоминание по настенным часам (time HH:MM + дни недели), в отличие от interval-based Exercise. Отдельная вкладка «Питание» с пресетами быстрого добавления (Завтрак/Обед/Ужин/Перекус). - shared: тип Meal, meals в AppState, nextMealOccurrence (DST-safe), SAMPLE_MEALS, MEAL_PRESETS; IPC-каналы meal:* + evtFireMeal - main: валидация (строгая HH:MM-проверка диапазона), store-мутаторы с пересчётом nextFireAt, scheduler.checkDueMeals (гейт только globalEnabled, grace-окно 120с, игнор тихих часов/ВКС), notifications.fireMealReminder, IPC-хендлеры - renderer: вкладка Meals + MealEditor (время/дни/иконка), MealReminder в окне напоминания (Поел/Отложить, TTS), пункт в Sidebar, маршрут, i18n RU/EN, иконки UtensilsCrossed/Soup - persistence: meals additive (без bump схемы — старые state'ы получают []) - +24 теста (203 -> 227): nextMealOccurrence, валидаторы приёмов пищи, scheduler meal-gating (вкл/выкл, grace, игнор тихих часов) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,12 +18,90 @@ import {
|
||||
validateExercisePatch,
|
||||
validateChallengeInput,
|
||||
validateChallengePatch,
|
||||
validateMealInput,
|
||||
validateMealPatch,
|
||||
validateSettingsPatch,
|
||||
validateId,
|
||||
validateActualReps,
|
||||
validateSnoozeMinutes
|
||||
} from './validate'
|
||||
|
||||
describe('validateMealInput', () => {
|
||||
it('принимает валидный приём пищи', () => {
|
||||
const r = validateMealInput({
|
||||
name: 'Обед',
|
||||
time: '13:00',
|
||||
icon: 'Soup',
|
||||
enabled: true,
|
||||
days: [1, 2, 3, 4, 5]
|
||||
})
|
||||
expect(r).toEqual({
|
||||
name: 'Обед',
|
||||
time: '13:00',
|
||||
icon: 'Soup',
|
||||
enabled: true,
|
||||
days: [1, 2, 3, 4, 5]
|
||||
})
|
||||
})
|
||||
|
||||
it('дефолтит icon и enabled', () => {
|
||||
const r = validateMealInput({ name: 'Ужин', time: '19:00', days: [] })
|
||||
expect(r?.icon).toBe('UtensilsCrossed')
|
||||
expect(r?.enabled).toBe(true)
|
||||
})
|
||||
|
||||
it('реджектит без имени / времени', () => {
|
||||
expect(validateMealInput({ time: '13:00', days: [] })).toBeNull()
|
||||
expect(validateMealInput({ name: 'X', days: [] })).toBeNull()
|
||||
})
|
||||
|
||||
it('реджектит кривое время', () => {
|
||||
expect(
|
||||
validateMealInput({ name: 'X', time: '99:99', days: [] })
|
||||
).toBeNull()
|
||||
expect(
|
||||
validateMealInput({ name: 'X', time: 'noon', days: [] })
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('реджектит дни вне диапазона и дедупит', () => {
|
||||
expect(
|
||||
validateMealInput({ name: 'X', time: '13:00', days: [7] })
|
||||
).toBeNull()
|
||||
const r = validateMealInput({
|
||||
name: 'X',
|
||||
time: '13:00',
|
||||
days: [1, 1, 2]
|
||||
})
|
||||
expect(r?.days).toEqual([1, 2])
|
||||
})
|
||||
|
||||
it('реджектит не-объект', () => {
|
||||
expect(validateMealInput(null)).toBeNull()
|
||||
expect(validateMealInput('meal')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateMealPatch', () => {
|
||||
it('частичный патч только заданных полей', () => {
|
||||
expect(validateMealPatch({ time: '07:30' })).toEqual({ time: '07:30' })
|
||||
expect(validateMealPatch({ enabled: false })).toEqual({ enabled: false })
|
||||
})
|
||||
|
||||
it('реджектит кривое время в патче', () => {
|
||||
expect(validateMealPatch({ time: '25:00' })).toBeNull()
|
||||
})
|
||||
|
||||
it('пропускает scheduler-поля с range-check', () => {
|
||||
expect(validateMealPatch({ nextFireAt: 123 })).toEqual({ nextFireAt: 123 })
|
||||
expect(validateMealPatch({ nextFireAt: -1 })).toBeNull()
|
||||
})
|
||||
|
||||
it('реджектит кривые дни', () => {
|
||||
expect(validateMealPatch({ days: [0, 8] })).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
const validExercise = {
|
||||
name: 'Push-ups',
|
||||
reps: 10,
|
||||
|
||||
Reference in New Issue
Block a user