Files
mdm-ordinis/ordinis-admin-ui/src/lib/dates.test.ts
T
Zimin A.N. f7efb23e09 test+ci: 51 frontend unit-тестов (Vitest) + CI rollback на single resolve-tag
Tests (Vitest):
- src/lib/dates.ts (вынесено из SchemaDrivenForm + dictionaries.$name): parseFormDate,
  formatIsoDate, formatIsoDateTime, extractTime, combineDateTime, localTzOffset, nowIsoLocal
- src/lib/dates.test.ts (20 tests): TZ-safety, pure-date local midnight, far-future filter,
  round-trip parse/format, time extraction в local TZ, clear-button (null Date → '')
- src/components/schema/types.test.ts (31 tests): buildSchemaJson всех 9 kinds + x-unique +
  x-id-source, parseSchemaJson roundtrip, diffSchemas (8 breaking detection cases),
  suggestVersionBump (major/minor/patch + breaking-wins-mix)

CI:
- Откатил dual resolve-tag (BACKEND/FRONTEND) на single resolve-tag.
  GitLab variable interpolation в trigger jobs не подхватывает dotenv от needs-job
  → admin-ui-only push не доезжал до infra. Now backwards compat path работает
  как раньше с UPSTREAM_IMAGE_TAG.
- Backend docker jobs (maven-package + 4 docker-*) теперь без rules:changes —
  catch ImagePullBackOff если frontend push шёл без backend rebuild. BuildKit cache
  делает повторный билд идентичных layers ~30 sec.
- Path-filter оставлен на maven-test (тесты не запускаются на frontend-only PR
  — они бэкендовые) и на docker-admin-ui (frontend-only).

Run: pnpm test → 51 passed.
2026-05-04 04:37:24 +03:00

140 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from 'vitest'
import {
combineDateTime,
extractTime,
formatIsoDate,
formatIsoDateTime,
localTzOffset,
nowIsoLocal,
parseFormDate,
} from './dates'
describe('parseFormDate', () => {
test('возвращает null для не-строки и пустой строки', () => {
expect(parseFormDate(null)).toBeNull()
expect(parseFormDate(undefined)).toBeNull()
expect(parseFormDate(123)).toBeNull()
expect(parseFormDate('')).toBeNull()
})
test('pure-date YYYY-MM-DD парсится как локальная полуночь (без TZ-сдвига)', () => {
const d = parseFormDate('2026-05-08')!
expect(d.getFullYear()).toBe(2026)
expect(d.getMonth()).toBe(4) // май = 4 (zero-indexed)
expect(d.getDate()).toBe(8)
expect(d.getHours()).toBe(0)
expect(d.getMinutes()).toBe(0)
})
test('ISO datetime с offset парсится корректно', () => {
const d = parseFormDate('2026-05-08T14:30:00+03:00')!
expect(d).toBeInstanceOf(Date)
expect(Number.isNaN(d.getTime())).toBe(false)
})
test('far-future (year >= 9999) возвращает null', () => {
expect(parseFormDate('9999-12-31T23:59:59Z')).toBeNull()
expect(parseFormDate('10000-01-01')).toBeNull()
})
test('невалидная строка → null', () => {
expect(parseFormDate('not-a-date')).toBeNull()
})
})
describe('formatIsoDate', () => {
test('форматирует Date как YYYY-MM-DD в local TZ', () => {
const d = new Date(2026, 4, 8) // 8 мая 2026 local
expect(formatIsoDate(d)).toBe('2026-05-08')
})
test('паддит month/day двумя цифрами', () => {
const d = new Date(2026, 0, 5)
expect(formatIsoDate(d)).toBe('2026-01-05')
})
})
describe('localTzOffset', () => {
test('возвращает offset в формате ±HH:mm', () => {
const off = localTzOffset(new Date())
expect(off).toMatch(/^[+-]\d{2}:\d{2}$/)
})
})
describe('formatIsoDateTime', () => {
test('собирает ISO с временем по умолчанию 00:00', () => {
const d = new Date(2026, 4, 8) // 8 мая 2026 local
const iso = formatIsoDateTime(d)
expect(iso).toMatch(/^2026-05-08T00:00:00[+-]\d{2}:\d{2}$/)
})
test('применяет переданное время', () => {
const d = new Date(2026, 4, 8)
const iso = formatIsoDateTime(d, '14:30')
expect(iso).toMatch(/^2026-05-08T14:30:00[+-]\d{2}:\d{2}$/)
})
test('round-trip: parse → format не меняет дату/время', () => {
const orig = formatIsoDateTime(new Date(2026, 4, 8), '14:30')
const parsed = parseFormDate(orig)!
expect(parsed.getFullYear()).toBe(2026)
expect(parsed.getMonth()).toBe(4)
expect(parsed.getDate()).toBe(8)
expect(parsed.getHours()).toBe(14)
expect(parsed.getMinutes()).toBe(30)
})
})
describe('extractTime', () => {
test('возвращает HH:mm из ISO datetime', () => {
// 14:30 в local TZ — формируем явно через formatIsoDateTime
const iso = formatIsoDateTime(new Date(2026, 4, 8), '14:30')
expect(extractTime(iso, '00:00')).toBe('14:30')
})
test('возвращает fallback для пустой строки', () => {
expect(extractTime(undefined, '23:59')).toBe('23:59')
expect(extractTime('', '00:00')).toBe('00:00')
})
test('возвращает fallback для невалидной строки', () => {
expect(extractTime('garbage', '12:34')).toBe('12:34')
})
test('паддит часы и минуты двумя цифрами', () => {
const iso = formatIsoDateTime(new Date(2026, 4, 8), '05:07')
expect(extractTime(iso, '00:00')).toBe('05:07')
})
})
describe('combineDateTime', () => {
test('null Date → пустая строка (для clear button)', () => {
expect(combineDateTime(null, '14:30', 'previous')).toBe('')
})
test('собирает ISO datetime из Date + HH:mm', () => {
const d = new Date(2026, 4, 8)
const iso = combineDateTime(d, '14:30', '')
expect(iso).toMatch(/^2026-05-08T14:30:00[+-]\d{2}:\d{2}$/)
})
test('защитный fallback на 00:00 при невалидном time', () => {
const d = new Date(2026, 4, 8)
const iso = combineDateTime(d, 'invalid', '')
expect(iso).toMatch(/^2026-05-08T00:00:00[+-]\d{2}:\d{2}$/)
})
})
describe('nowIsoLocal', () => {
test('возвращает ISO datetime в local TZ', () => {
const iso = nowIsoLocal()
expect(iso).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00[+-]\d{2}:\d{2}$/)
})
test('parsable обратно в Date', () => {
const iso = nowIsoLocal()
const d = parseFormDate(iso)
expect(d).not.toBeNull()
})
})