refactor(frontend): unify dateFormat + numberFormat into formatters (closes #46)
- Create src/web/src/lib/formatters.ts with all exports from both modules - Migrate all 14 import sites to @/lib/formatters (Opción A — immediate migration) - Replace dateFormat.test.ts with formatters.test.ts including 10 smoke tests + full suite - Delete src/web/src/lib/dateFormat.ts and numberFormat.ts - 464 tests green, tsc clean (TS5101 warning is pre-existing)
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
// Modal de edición / creación de IngresosBrutos
|
// Modal de edición / creación de IngresosBrutos
|
||||||
// CRÍTICO: NO incluye campo Alícuota en modo edit (inmutable, cambiar via NuevaVersion)
|
// CRÍTICO: NO incluye campo Alícuota en modo edit (inmutable, cambiar via NuevaVersion)
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { todayArgentina } from '@/lib/dateFormat'
|
import { todayArgentina } from '@/lib/formatters'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
import { useNuevaVersionIngresosBrutos } from '../hooks/useIngresosBrutos'
|
import { useNuevaVersionIngresosBrutos } from '../hooks/useIngresosBrutos'
|
||||||
import type { IngresosBrutos } from '../types/ingresosBrutos.types'
|
import type { IngresosBrutos } from '../types/ingresosBrutos.types'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { prevCivilDate, formatCivilDate } from '@/lib/dateFormat'
|
import { prevCivilDate, formatCivilDate } from '@/lib/formatters'
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
alicuota: z.coerce
|
alicuota: z.coerce
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
import { useNuevaVersionTipoDeIva } from '../hooks/useTiposDeIva'
|
import { useNuevaVersionTipoDeIva } from '../hooks/useTiposDeIva'
|
||||||
import type { TipoDeIva } from '../types/tipoDeIva.types'
|
import type { TipoDeIva } from '../types/tipoDeIva.types'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { prevCivilDate, formatCivilDate } from '@/lib/dateFormat'
|
import { prevCivilDate, formatCivilDate } from '@/lib/formatters'
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
porcentaje: z.coerce
|
porcentaje: z.coerce
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Modal de edición / creación de TipoDeIva
|
// Modal de edición / creación de TipoDeIva
|
||||||
// CRÍTICO: NO incluye campo Porcentaje (inmutable, cambiar via NuevaVersion)
|
// CRÍTICO: NO incluye campo Porcentaje (inmutable, cambiar via NuevaVersion)
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { todayArgentina } from '@/lib/dateFormat'
|
import { todayArgentina } from '@/lib/formatters'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { CanPerform } from '@/components/auth/CanPerform'
|
|||||||
import { useMedio } from '../hooks/useMedio'
|
import { useMedio } from '../hooks/useMedio'
|
||||||
import { DeactivateMedioModal } from '../components/DeactivateMedioModal'
|
import { DeactivateMedioModal } from '../components/DeactivateMedioModal'
|
||||||
import { tipoMedioLabel } from '../tipoMedio'
|
import { tipoMedioLabel } from '../tipoMedio'
|
||||||
import { formatInstantOrDash } from '@/lib/dateFormat'
|
import { formatInstantOrDash } from '@/lib/formatters'
|
||||||
|
|
||||||
export function MedioDetailPage() {
|
export function MedioDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { todayArgentina } from '@/lib/dateFormat'
|
import { todayArgentina } from '@/lib/formatters'
|
||||||
import { useAddProductPrice } from '../hooks/useAddProductPrice'
|
import { useAddProductPrice } from '../hooks/useAddProductPrice'
|
||||||
|
|
||||||
// ─── Schema (Zod, espejo del backend) ────────────────────────────────────────
|
// ─── Schema (Zod, espejo del backend) ────────────────────────────────────────
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { CanPerform } from '@/components/auth/CanPerform'
|
import { CanPerform } from '@/components/auth/CanPerform'
|
||||||
import { formatCivilDate } from '@/lib/dateFormat'
|
import { formatCivilDate, formatCurrency } from '@/lib/formatters'
|
||||||
import { formatCurrency } from '@/lib/numberFormat'
|
|
||||||
import { useProductPrices } from '../hooks/useProductPrices'
|
import { useProductPrices } from '../hooks/useProductPrices'
|
||||||
import { AddProductPriceDialog } from './AddProductPriceDialog'
|
import { AddProductPriceDialog } from './AddProductPriceDialog'
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useMedio } from '../../medios/hooks/useMedio'
|
|||||||
import { DeactivatePuntoDeVentaModal } from '../components/DeactivatePuntoDeVentaModal'
|
import { DeactivatePuntoDeVentaModal } from '../components/DeactivatePuntoDeVentaModal'
|
||||||
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
|
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
|
||||||
import { PdvInactivoBanner } from '../components/PdvInactivoBanner'
|
import { PdvInactivoBanner } from '../components/PdvInactivoBanner'
|
||||||
import { formatInstantOrDash } from '@/lib/dateFormat'
|
import { formatInstantOrDash } from '@/lib/formatters'
|
||||||
|
|
||||||
export function PuntoDeVentaDetailPage() {
|
export function PuntoDeVentaDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { DeactivateSeccionModal } from '../components/DeactivateSeccionModal'
|
|||||||
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
|
import { MedioInactivoBanner } from '../components/MedioInactivoBanner'
|
||||||
import { tipoSeccionLabel } from '../tipoSeccion'
|
import { tipoSeccionLabel } from '../tipoSeccion'
|
||||||
import { useMedio } from '../../medios/hooks/useMedio'
|
import { useMedio } from '../../medios/hooks/useMedio'
|
||||||
import { formatInstantOrDash } from '@/lib/dateFormat'
|
import { formatInstantOrDash } from '@/lib/formatters'
|
||||||
|
|
||||||
export function SeccionDetailPage() {
|
export function SeccionDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { ColumnDef } from '@tanstack/react-table'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { DataTable } from '@/components/ui/data-table'
|
import { DataTable } from '@/components/ui/data-table'
|
||||||
import type { UserListItem } from '../types'
|
import type { UserListItem } from '../types'
|
||||||
import { formatInstantOrDash } from '@/lib/dateFormat'
|
import { formatInstantOrDash } from '@/lib/formatters'
|
||||||
|
|
||||||
interface UsersTableProps {
|
interface UsersTableProps {
|
||||||
rows: UserListItem[]
|
rows: UserListItem[]
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
|
* Punto unificado de formateo — fecha y número.
|
||||||
|
* Anteriormente separado en dateFormat.ts y numberFormat.ts (unificado en issue #46).
|
||||||
|
*
|
||||||
* Localización temporal Argentina — utility centralizada.
|
* Localización temporal Argentina — utility centralizada.
|
||||||
* Ver: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.17 ⏰ Localización Temporal Argentina.md
|
* Ver: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.17 ⏰ Localización Temporal Argentina.md
|
||||||
* Engram topic_key: sig-cm2/conventions/fechas-timezones
|
* Engram topic_key: sig-cm2/conventions/fechas-timezones
|
||||||
@@ -121,3 +124,20 @@ export function parseArgentinaDateTimeToUtc(localDateTime: string): string {
|
|||||||
// Parse "yyyy-MM-ddTHH:mm" como ART (offset -03:00) y convertir a ISO UTC
|
// Parse "yyyy-MM-ddTHH:mm" como ART (offset -03:00) y convertir a ISO UTC
|
||||||
return new Date(`${localDateTime}:00-03:00`).toISOString();
|
return new Date(`${localDateTime}:00-03:00`).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Números
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatea un número como moneda ARS (pesos argentinos).
|
||||||
|
* Output: "$ 1.500,50" o similar según locale.
|
||||||
|
*/
|
||||||
|
export function formatCurrency(amount: number): string {
|
||||||
|
return new Intl.NumberFormat('es-AR', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'ARS',
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(amount);
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* Formateo de números — utility centralizada.
|
|
||||||
* Usar SIEMPRE estas funciones en lugar de Intl.NumberFormat inline.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formatea un número como moneda ARS (pesos argentinos).
|
|
||||||
* Output: "$ 1.500,50" o similar según locale.
|
|
||||||
*/
|
|
||||||
export function formatCurrency(amount: number): string {
|
|
||||||
return new Intl.NumberFormat('es-AR', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'ARS',
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
}).format(amount)
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import { useState, type FormEvent } from 'react'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { parseArgentinaDateTimeToUtc } from '@/lib/dateFormat'
|
import { parseArgentinaDateTimeToUtc } from '@/lib/formatters'
|
||||||
|
|
||||||
/** Filtros crudos del form (todos strings para binding directo del input). */
|
/** Filtros crudos del form (todos strings para binding directo del input). */
|
||||||
export interface AuditFiltersValue {
|
export interface AuditFiltersValue {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
toApiFilter,
|
toApiFilter,
|
||||||
type AuditFiltersValue,
|
type AuditFiltersValue,
|
||||||
} from './AuditFilters'
|
} from './AuditFilters'
|
||||||
import { formatInstant } from '@/lib/dateFormat'
|
import { formatInstant } from '@/lib/formatters'
|
||||||
|
|
||||||
/** Copia texto al clipboard con fallback + toast. */
|
/** Copia texto al clipboard con fallback + toast. */
|
||||||
async function copyToClipboard(text: string, label: string): Promise<void> {
|
async function copyToClipboard(text: string, label: string): Promise<void> {
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
||||||
import {
|
|
||||||
AR_TZ,
|
|
||||||
formatInstant,
|
|
||||||
formatInstantOrDash,
|
|
||||||
formatCivilDate,
|
|
||||||
formatCivilDateRange,
|
|
||||||
todayArgentina,
|
|
||||||
parseCivilDate,
|
|
||||||
prevCivilDate,
|
|
||||||
} from '@/lib/dateFormat';
|
|
||||||
|
|
||||||
describe('dateFormat.ts', () => {
|
|
||||||
describe('AR_TZ constant', () => {
|
|
||||||
it('is America/Argentina/Buenos_Aires', () => {
|
|
||||||
expect(AR_TZ).toBe('America/Argentina/Buenos_Aires');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatInstant (Cat1 — UTC → AR display)', () => {
|
|
||||||
it('converts 01:30 UTC to 22:30 ART previous day', () => {
|
|
||||||
const iso = '2026-05-01T01:30:00.000Z';
|
|
||||||
const result = formatInstant(iso);
|
|
||||||
// format es-AR short + medium: "30/4/2026, 22:30:00" o similar
|
|
||||||
expect(result).toMatch(/30\/0?4\/2026/);
|
|
||||||
expect(result).toMatch(/22:30/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses AR timezone regardless of browser TZ', () => {
|
|
||||||
const iso = '2026-05-01T12:00:00.000Z'; // 09:00 ART
|
|
||||||
const result = formatInstant(iso);
|
|
||||||
expect(result).toMatch(/01\/0?5\/2026/);
|
|
||||||
expect(result).toMatch(/09:00/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatInstantOrDash (Cat1 nullable)', () => {
|
|
||||||
it('returns "—" for null', () => {
|
|
||||||
expect(formatInstantOrDash(null)).toBe('—');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns "—" for undefined', () => {
|
|
||||||
expect(formatInstantOrDash(undefined)).toBe('—');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('delegates to formatInstant for valid ISO', () => {
|
|
||||||
const iso = '2026-05-01T01:30:00.000Z';
|
|
||||||
expect(formatInstantOrDash(iso)).toBe(formatInstant(iso));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatCivilDate (Cat2 — yyyy-MM-dd → dd/MM/yyyy)', () => {
|
|
||||||
it('splits manually without new Date()', () => {
|
|
||||||
expect(formatCivilDate('2026-05-01')).toBe('01/05/2026');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves day for early-year dates', () => {
|
|
||||||
expect(formatCivilDate('2026-01-01')).toBe('01/01/2026');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves day for end-of-year dates', () => {
|
|
||||||
expect(formatCivilDate('2026-12-31')).toBe('31/12/2026');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatCivilDateRange', () => {
|
|
||||||
it('renders full range with arrow', () => {
|
|
||||||
expect(formatCivilDateRange('2026-05-01', '2026-12-31'))
|
|
||||||
.toBe('01/05/2026 → 31/12/2026');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders open range when hasta is null', () => {
|
|
||||||
expect(formatCivilDateRange('2026-05-01', null))
|
|
||||||
.toBe('desde 01/05/2026');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('todayArgentina (fix BUG-FE-03)', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 2026-04-30 at 22:30 ART of 30/04 (BUG-FE-03 regression)', () => {
|
|
||||||
// 22:30 ART = 01:30 UTC del día siguiente
|
|
||||||
vi.useFakeTimers();
|
|
||||||
vi.setSystemTime(new Date('2026-05-01T01:30:00.000Z'));
|
|
||||||
|
|
||||||
expect(todayArgentina()).toBe('2026-04-30');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 2026-05-01 at 00:30 ART of 01/05', () => {
|
|
||||||
// 00:30 ART = 03:30 UTC
|
|
||||||
vi.useFakeTimers();
|
|
||||||
vi.setSystemTime(new Date('2026-05-01T03:30:00.000Z'));
|
|
||||||
|
|
||||||
expect(todayArgentina()).toBe('2026-05-01');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 2024-02-28 at 22:30 ART of 28/02/2024 (bisiesto)', () => {
|
|
||||||
// 22:30 ART del 28/02/2024 = 01:30 UTC del 29/02/2024
|
|
||||||
vi.useFakeTimers();
|
|
||||||
vi.setSystemTime(new Date('2024-02-29T01:30:00.000Z'));
|
|
||||||
|
|
||||||
expect(todayArgentina()).toBe('2024-02-28');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns 2026-12-31 at 22:30 ART of 31/12 (end of year)', () => {
|
|
||||||
// 22:30 ART del 31/12/2026 = 01:30 UTC del 01/01/2027
|
|
||||||
vi.useFakeTimers();
|
|
||||||
vi.setSystemTime(new Date('2027-01-01T01:30:00.000Z'));
|
|
||||||
|
|
||||||
expect(todayArgentina()).toBe('2026-12-31');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('parseCivilDate', () => {
|
|
||||||
it('splits "2026-05-01" into {year, month, day}', () => {
|
|
||||||
expect(parseCivilDate('2026-05-01')).toEqual({ year: 2026, month: 5, day: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not use new Date() (no UTC creep)', () => {
|
|
||||||
const result = parseCivilDate('2026-01-01');
|
|
||||||
expect(result.year).toBe(2026);
|
|
||||||
expect(result.month).toBe(1);
|
|
||||||
expect(result.day).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('prevCivilDate (BUG-FE-04 — fecha cierre NuevaVigencia)', () => {
|
|
||||||
it('returns day before (2026-05-01 → 2026-04-30)', () => {
|
|
||||||
expect(prevCivilDate('2026-05-01')).toBe('2026-04-30');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('crosses month boundary (2026-05-31 → 2026-05-30)', () => {
|
|
||||||
expect(prevCivilDate('2026-06-01')).toBe('2026-05-31');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('crosses year boundary (2026-01-01 → 2025-12-31)', () => {
|
|
||||||
expect(prevCivilDate('2026-01-01')).toBe('2025-12-31');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles leap year (2024-03-01 → 2024-02-29)', () => {
|
|
||||||
expect(prevCivilDate('2024-03-01')).toBe('2024-02-29');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
190
src/web/src/tests/lib/formatters.test.ts
Normal file
190
src/web/src/tests/lib/formatters.test.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { describe, it, expect, afterEach, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
AR_TZ,
|
||||||
|
formatInstant,
|
||||||
|
formatInstantOrDash,
|
||||||
|
formatCivilDate,
|
||||||
|
formatCivilDateRange,
|
||||||
|
todayArgentina,
|
||||||
|
parseCivilDate,
|
||||||
|
prevCivilDate,
|
||||||
|
parseArgentinaDateTimeToUtc,
|
||||||
|
formatCurrency,
|
||||||
|
} from '@/lib/formatters';
|
||||||
|
|
||||||
|
// ─── Smoke test — API unificada accesible desde @/lib/formatters ───────────────
|
||||||
|
|
||||||
|
describe('formatters.ts — smoke (unified module exports)', () => {
|
||||||
|
it('exports AR_TZ', () => {
|
||||||
|
expect(AR_TZ).toBe('America/Argentina/Buenos_Aires');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports formatInstant (function)', () => {
|
||||||
|
expect(typeof formatInstant).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports formatInstantOrDash (function)', () => {
|
||||||
|
expect(typeof formatInstantOrDash).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports formatCivilDate (function)', () => {
|
||||||
|
expect(typeof formatCivilDate).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports formatCivilDateRange (function)', () => {
|
||||||
|
expect(typeof formatCivilDateRange).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports todayArgentina (function)', () => {
|
||||||
|
expect(typeof todayArgentina).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports parseCivilDate (function)', () => {
|
||||||
|
expect(typeof parseCivilDate).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports prevCivilDate (function)', () => {
|
||||||
|
expect(typeof prevCivilDate).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports parseArgentinaDateTimeToUtc (function)', () => {
|
||||||
|
expect(typeof parseArgentinaDateTimeToUtc).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exports formatCurrency (function)', () => {
|
||||||
|
expect(typeof formatCurrency).toBe('function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Suite completa — mismos tests que dateFormat.test.ts ─────────────────────
|
||||||
|
|
||||||
|
describe('formatInstant (Cat1 — UTC → AR display)', () => {
|
||||||
|
it('converts 01:30 UTC to 22:30 ART previous day', () => {
|
||||||
|
const iso = '2026-05-01T01:30:00.000Z';
|
||||||
|
const result = formatInstant(iso);
|
||||||
|
expect(result).toMatch(/30\/0?4\/2026/);
|
||||||
|
expect(result).toMatch(/22:30/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses AR timezone regardless of browser TZ', () => {
|
||||||
|
const iso = '2026-05-01T12:00:00.000Z'; // 09:00 ART
|
||||||
|
const result = formatInstant(iso);
|
||||||
|
expect(result).toMatch(/01\/0?5\/2026/);
|
||||||
|
expect(result).toMatch(/09:00/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatInstantOrDash (Cat1 nullable)', () => {
|
||||||
|
it('returns "—" for null', () => {
|
||||||
|
expect(formatInstantOrDash(null)).toBe('—');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "—" for undefined', () => {
|
||||||
|
expect(formatInstantOrDash(undefined)).toBe('—');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates to formatInstant for valid ISO', () => {
|
||||||
|
const iso = '2026-05-01T01:30:00.000Z';
|
||||||
|
expect(formatInstantOrDash(iso)).toBe(formatInstant(iso));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatCivilDate (Cat2 — yyyy-MM-dd → dd/MM/yyyy)', () => {
|
||||||
|
it('splits manually without new Date()', () => {
|
||||||
|
expect(formatCivilDate('2026-05-01')).toBe('01/05/2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves day for early-year dates', () => {
|
||||||
|
expect(formatCivilDate('2026-01-01')).toBe('01/01/2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves day for end-of-year dates', () => {
|
||||||
|
expect(formatCivilDate('2026-12-31')).toBe('31/12/2026');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatCivilDateRange', () => {
|
||||||
|
it('renders full range with arrow', () => {
|
||||||
|
expect(formatCivilDateRange('2026-05-01', '2026-12-31'))
|
||||||
|
.toBe('01/05/2026 → 31/12/2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders open range when hasta is null', () => {
|
||||||
|
expect(formatCivilDateRange('2026-05-01', null))
|
||||||
|
.toBe('desde 01/05/2026');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('todayArgentina (fix BUG-FE-03)', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 2026-04-30 at 22:30 ART of 30/04 (BUG-FE-03 regression)', () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2026-05-01T01:30:00.000Z'));
|
||||||
|
expect(todayArgentina()).toBe('2026-04-30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 2026-05-01 at 00:30 ART of 01/05', () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2026-05-01T03:30:00.000Z'));
|
||||||
|
expect(todayArgentina()).toBe('2026-05-01');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 2024-02-28 at 22:30 ART of 28/02/2024 (bisiesto)', () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2024-02-29T01:30:00.000Z'));
|
||||||
|
expect(todayArgentina()).toBe('2024-02-28');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 2026-12-31 at 22:30 ART of 31/12 (end of year)', () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2027-01-01T01:30:00.000Z'));
|
||||||
|
expect(todayArgentina()).toBe('2026-12-31');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseCivilDate', () => {
|
||||||
|
it('splits "2026-05-01" into {year, month, day}', () => {
|
||||||
|
expect(parseCivilDate('2026-05-01')).toEqual({ year: 2026, month: 5, day: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not use new Date() (no UTC creep)', () => {
|
||||||
|
const result = parseCivilDate('2026-01-01');
|
||||||
|
expect(result.year).toBe(2026);
|
||||||
|
expect(result.month).toBe(1);
|
||||||
|
expect(result.day).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prevCivilDate (BUG-FE-04 — fecha cierre NuevaVigencia)', () => {
|
||||||
|
it('returns day before (2026-05-01 → 2026-04-30)', () => {
|
||||||
|
expect(prevCivilDate('2026-05-01')).toBe('2026-04-30');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('crosses month boundary (2026-05-31 → 2026-05-30)', () => {
|
||||||
|
expect(prevCivilDate('2026-06-01')).toBe('2026-05-31');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('crosses year boundary (2026-01-01 → 2025-12-31)', () => {
|
||||||
|
expect(prevCivilDate('2026-01-01')).toBe('2025-12-31');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles leap year (2024-03-01 → 2024-02-29)', () => {
|
||||||
|
expect(prevCivilDate('2024-03-01')).toBe('2024-02-29');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatCurrency (ARS)', () => {
|
||||||
|
it('formats a whole number with 2 decimal places', () => {
|
||||||
|
const result = formatCurrency(1500);
|
||||||
|
expect(result).toMatch(/1[\.,]500/);
|
||||||
|
expect(result).toMatch(/00/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats a decimal amount', () => {
|
||||||
|
const result = formatCurrency(1234.56);
|
||||||
|
expect(result).toMatch(/56/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user