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) } } } }) })