dify/web/hooks/use-format-time-from-now.spec.ts

309 lines
9.6 KiB
TypeScript

import type { Mock } from 'vitest'
/**
* Test suite for useFormatTimeFromNow hook
*
* This hook provides internationalized relative time formatting (e.g., "2 hours ago", "3 days ago")
* using dayjs with the relativeTime plugin. It automatically uses the correct locale based on
* the user's i18n settings.
*
* Key features:
* - Supports 20+ locales with proper translations
* - Automatically syncs with user's interface language
* - Uses dayjs for consistent time calculations
* - Returns human-readable relative time strings
*/
import { renderHook } from '@testing-library/react'
// Import after mock to get the mocked version
import { useLocale } from '@/context/i18n'
import { useFormatTimeFromNow } from './use-format-time-from-now'
vi.mock('@/context/i18n', () => ({
useLocale: vi.fn(() => 'en-US'),
}))
// Mock the i18n context
describe('useFormatTimeFromNow', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Basic functionality', () => {
/**
* Test that the hook returns a formatTimeFromNow function
* This is the primary interface of the hook
*/
it('should return formatTimeFromNow function', () => {
const { result } = renderHook(() => useFormatTimeFromNow())
expect(result.current).toHaveProperty('formatTimeFromNow')
expect(typeof result.current.formatTimeFromNow).toBe('function')
})
/**
* Test basic relative time formatting with English locale
* Should return human-readable relative time strings
*/
it('should format time from now in English', () => {
;(useLocale as Mock).mockReturnValue('en-US')
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const oneHourAgo = now - 60 * 60 * 1000
const formatted = result.current.formatTimeFromNow(oneHourAgo)
// Should contain "hour" or "hours" and "ago"
expect(formatted).toMatch(/hour|hours/)
expect(formatted).toMatch(/ago/)
})
/**
* Test that recent times are formatted as "a few seconds ago"
* Very recent timestamps should show seconds
*/
it('should format very recent times', () => {
;(useLocale as Mock).mockReturnValue('en-US')
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const fiveSecondsAgo = now - 5 * 1000
const formatted = result.current.formatTimeFromNow(fiveSecondsAgo)
expect(formatted).toMatch(/second|seconds|few seconds/)
})
/**
* Test formatting of times in the past (days ago)
* Should handle day-level granularity
*/
it('should format times from days ago', () => {
;(useLocale as Mock).mockReturnValue('en-US')
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const threeDaysAgo = now - 3 * 24 * 60 * 60 * 1000
const formatted = result.current.formatTimeFromNow(threeDaysAgo)
expect(formatted).toMatch(/day|days/)
expect(formatted).toMatch(/ago/)
})
/**
* Test formatting of future times
* dayjs fromNow also supports future times (e.g., "in 2 hours")
*/
it('should format future times', () => {
;(useLocale as Mock).mockReturnValue('en-US')
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const twoHoursFromNow = now + 2 * 60 * 60 * 1000
const formatted = result.current.formatTimeFromNow(twoHoursFromNow)
expect(formatted).toMatch(/in/)
expect(formatted).toMatch(/hour|hours/)
})
})
describe('Locale support', () => {
/**
* Test Chinese (Simplified) locale formatting
* Should use Chinese characters for time units
*/
it('should format time in Chinese (Simplified)', () => {
;(useLocale as Mock).mockReturnValue('zh-Hans')
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const oneHourAgo = now - 60 * 60 * 1000
const formatted = result.current.formatTimeFromNow(oneHourAgo)
// Chinese should contain Chinese characters
expect(formatted).toMatch(/[\u4E00-\u9FA5]/)
})
/**
* Test Spanish locale formatting
* Should use Spanish words for relative time
*/
it('should format time in Spanish', () => {
;(useLocale as Mock).mockReturnValue('es-ES')
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const oneHourAgo = now - 60 * 60 * 1000
const formatted = result.current.formatTimeFromNow(oneHourAgo)
// Spanish should contain "hace" (ago)
expect(formatted).toMatch(/hace/)
})
/**
* Test French locale formatting
* Should use French words for relative time
*/
it('should format time in French', () => {
;(useLocale as Mock).mockReturnValue('fr-FR')
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const oneHourAgo = now - 60 * 60 * 1000
const formatted = result.current.formatTimeFromNow(oneHourAgo)
// French should contain "il y a" (ago)
expect(formatted).toMatch(/il y a/)
})
/**
* Test Japanese locale formatting
* Should use Japanese characters
*/
it('should format time in Japanese', () => {
;(useLocale as Mock).mockReturnValue('ja-JP')
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const oneHourAgo = now - 60 * 60 * 1000
const formatted = result.current.formatTimeFromNow(oneHourAgo)
// Japanese should contain Japanese characters
expect(formatted).toMatch(/[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/)
})
/**
* Test Portuguese (Brazil) locale formatting
* Should use pt-br locale mapping
*/
it('should format time in Portuguese (Brazil)', () => {
;(useLocale as Mock).mockReturnValue('pt-BR')
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const oneHourAgo = now - 60 * 60 * 1000
const formatted = result.current.formatTimeFromNow(oneHourAgo)
// Portuguese should contain "há" (ago)
expect(formatted).toMatch(/há/)
})
/**
* Test fallback to English for unsupported locales
* Unknown locales should default to English
*/
it('should fallback to English for unsupported locale', () => {
;(useLocale as Mock).mockReturnValue('xx-XX' as any)
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const oneHourAgo = now - 60 * 60 * 1000
const formatted = result.current.formatTimeFromNow(oneHourAgo)
// Should still return a valid string (in English)
expect(typeof formatted).toBe('string')
expect(formatted.length).toBeGreaterThan(0)
})
})
describe('Edge cases', () => {
/**
* Test handling of timestamp 0 (Unix epoch)
* Should format as a very old date
*/
it('should handle timestamp 0', () => {
;(useLocale as Mock).mockReturnValue('en-US')
const { result } = renderHook(() => useFormatTimeFromNow())
const formatted = result.current.formatTimeFromNow(0)
expect(typeof formatted).toBe('string')
expect(formatted.length).toBeGreaterThan(0)
expect(formatted).toMatch(/year|years/)
})
/**
* Test handling of very large timestamps
* Should handle dates far in the future
*/
it('should handle very large timestamps', () => {
;(useLocale as Mock).mockReturnValue('en-US')
const { result } = renderHook(() => useFormatTimeFromNow())
const farFuture = Date.now() + 365 * 24 * 60 * 60 * 1000 // 1 year from now
const formatted = result.current.formatTimeFromNow(farFuture)
expect(typeof formatted).toBe('string')
expect(formatted).toMatch(/in/)
})
/**
* Test that the function is memoized based on locale
* Changing locale should update the function
*/
it('should update when locale changes', () => {
const { result, rerender } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
const oneHourAgo = now - 60 * 60 * 1000
// First render with English
;(useLocale as Mock).mockReturnValue('en-US')
rerender()
const englishResult = result.current.formatTimeFromNow(oneHourAgo)
// Second render with Spanish
;(useLocale as Mock).mockReturnValue('es-ES')
rerender()
const spanishResult = result.current.formatTimeFromNow(oneHourAgo)
// Results should be different
expect(englishResult).not.toBe(spanishResult)
})
})
describe('Time granularity', () => {
/**
* Test different time granularities (seconds, minutes, hours, days, months, years)
* dayjs should automatically choose the appropriate unit
*/
it('should use appropriate time units for different durations', () => {
;(useLocale as Mock).mockReturnValue('en-US')
const { result } = renderHook(() => useFormatTimeFromNow())
const now = Date.now()
// Seconds
const seconds = result.current.formatTimeFromNow(now - 30 * 1000)
expect(seconds).toMatch(/second/)
// Minutes
const minutes = result.current.formatTimeFromNow(now - 5 * 60 * 1000)
expect(minutes).toMatch(/minute/)
// Hours
const hours = result.current.formatTimeFromNow(now - 3 * 60 * 60 * 1000)
expect(hours).toMatch(/hour/)
// Days
const days = result.current.formatTimeFromNow(now - 5 * 24 * 60 * 60 * 1000)
expect(days).toMatch(/day/)
// Months
const months = result.current.formatTimeFromNow(now - 60 * 24 * 60 * 60 * 1000)
expect(months).toMatch(/month/)
})
})
})