fix+test: автономные правки после ревью v0.5.7
Bug — Heatmap/streak/achievements не обновлялись после markDone/
markChallengeDone. Регресс из Sprint C (история выделена из
state-broadcast). Корень: store мутирует Exercise.lastDoneAt
in-place → state.exercises ref не меняется → useEffect([exercises])
не fires → Dashboard не перетягивает history.
Фикс: новый event IPC.evtHistoryChanged + broadcastHistoryChanged().
Триггерится после markDone/snooze/skip/markChallengeDone/
clearHistory/import. Dashboard.useEffect подписывается через
onHistoryChanged.
Settings → AboutCard теперь показывает текущую версию приложения
(раньше была только кнопка «Что нового»). Версия через
IPC.getAppVersion.
Tests:
+6 для repsDoneTodayForExercise — match-challenges, snapshot,
deleted-exercise fallback, ignore skip/snooze.
+2 для dailyReps с новыми snapshot-полями (match-challenges
и deleted exercises).
+6 для unseenVersions + RELEASE_NOTES контракт.
+7 для adjustNextFireAt (адаптивный шедулер): малая история,
плохой/хороший час, MAX_SHIFT_HOURS, фильтр по упражнению,
30-day window.
Итого 135 → 159 (+24).
Грепнул src/ на стейл-references к removed setPaused/isPaused/
`let paused` — чисто. Sprint C-D refactor завершён без residue.
This commit is contained in:
75
src/shared/release-notes.test.ts
Normal file
75
src/shared/release-notes.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { unseenVersions, RELEASE_NOTES } from './release-notes'
|
||||
|
||||
describe('unseenVersions', () => {
|
||||
// Завязываемся на реальный RELEASE_NOTES — это OK, тест защищает контракт.
|
||||
// Если из RELEASE_NOTES удалят ключ, упадёт expect.
|
||||
|
||||
it('returns only current version when lastSeen is undefined (new user proxy)', () => {
|
||||
// Логика: при отсутствии lastSeen возвращаем только current — мы НЕ
|
||||
// знаем, новичок это или нет; UI решает (через exercises.lastDoneAt).
|
||||
// Эта функция только показывает «что было бы показано».
|
||||
const result = unseenVersions('0.5.6', undefined)
|
||||
expect(result).toEqual(['0.5.6'])
|
||||
})
|
||||
|
||||
it('returns nothing when lastSeen equals current', () => {
|
||||
expect(unseenVersions('0.5.6', '0.5.6')).toEqual([])
|
||||
})
|
||||
|
||||
it('returns versions strictly between lastSeen and current (desc)', () => {
|
||||
// Юзер видел 0.5.4, обновился на 0.5.7 → видит 0.5.5, 0.5.6, 0.5.7.
|
||||
const result = unseenVersions('0.5.7', '0.5.4')
|
||||
// Порядок desc (новейшее сверху).
|
||||
expect(result[0]).toBe('0.5.7')
|
||||
expect(result).toContain('0.5.5')
|
||||
expect(result).toContain('0.5.6')
|
||||
expect(result).not.toContain('0.5.4')
|
||||
expect(result).not.toContain('0.5.3')
|
||||
})
|
||||
|
||||
it('skips versions beyond current (no notes for not-yet-installed releases)', () => {
|
||||
// Юзер видел 0.5.4, current=0.5.5 → видит только 0.5.5, не 0.5.6+.
|
||||
const result = unseenVersions('0.5.5', '0.5.4')
|
||||
expect(result).toEqual(['0.5.5'])
|
||||
})
|
||||
|
||||
it('handles versions with patch increments correctly', () => {
|
||||
const result = unseenVersions('0.5.7', '0.5.6')
|
||||
expect(result).toEqual(['0.5.7'])
|
||||
})
|
||||
|
||||
it('lastSeen ahead of current returns empty (downgrade case)', () => {
|
||||
const result = unseenVersions('0.5.3', '0.5.5')
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('RELEASE_NOTES contract', () => {
|
||||
it('has both ru and en for every version', () => {
|
||||
for (const [v, notes] of Object.entries(RELEASE_NOTES)) {
|
||||
expect(notes.ru, `v${v} missing ru notes`).toBeTruthy()
|
||||
expect(notes.en, `v${v} missing en notes`).toBeTruthy()
|
||||
expect(notes.ru.length, `v${v} empty ru`).toBeGreaterThan(0)
|
||||
expect(notes.en.length, `v${v} empty en`).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('all version keys match semver-light /^\\d+\\.\\d+\\.\\d+$/', () => {
|
||||
for (const v of Object.keys(RELEASE_NOTES)) {
|
||||
expect(v).toMatch(/^\d+\.\d+\.\d+$/)
|
||||
}
|
||||
})
|
||||
|
||||
it('every note item has title; tag is in allowed set if present', () => {
|
||||
const allowedTags = new Set(['new', 'fix', 'security', 'perf'])
|
||||
for (const notes of Object.values(RELEASE_NOTES)) {
|
||||
for (const lang of ['ru', 'en'] as const) {
|
||||
for (const it of notes[lang]) {
|
||||
expect(it.title.length).toBeGreaterThan(0)
|
||||
if (it.tag) expect(allowedTags.has(it.tag)).toBe(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user