dify
This commit is contained in:
376
dify/web/hooks/use-format-time-from-now.spec.ts
Normal file
376
dify/web/hooks/use-format-time-from-now.spec.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* 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 { useFormatTimeFromNow } from './use-format-time-from-now'
|
||||
|
||||
// Mock the i18n context
|
||||
jest.mock('@/context/i18n', () => ({
|
||||
useI18N: jest.fn(() => ({
|
||||
locale: 'en-US',
|
||||
})),
|
||||
}))
|
||||
|
||||
// Import after mock to get the mocked version
|
||||
import { useI18N } from '@/context/i18n'
|
||||
|
||||
describe('useFormatTimeFromNow', () => {
|
||||
beforeEach(() => {
|
||||
jest.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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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)', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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)', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: 'en-US' })
|
||||
rerender()
|
||||
const englishResult = result.current.formatTimeFromNow(oneHourAgo)
|
||||
|
||||
// Second render with Spanish
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: '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/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Locale mapping', () => {
|
||||
/**
|
||||
* Test that all supported locales in the localeMap are handled correctly
|
||||
* This ensures the mapping from app locales to dayjs locales works
|
||||
*/
|
||||
it('should handle all mapped locales', () => {
|
||||
const locales = [
|
||||
'en-US', 'zh-Hans', 'zh-Hant', 'pt-BR', 'es-ES', 'fr-FR',
|
||||
'de-DE', 'ja-JP', 'ko-KR', 'ru-RU', 'it-IT', 'th-TH',
|
||||
'id-ID', 'uk-UA', 'vi-VN', 'ro-RO', 'pl-PL', 'hi-IN',
|
||||
'tr-TR', 'fa-IR', 'sl-SI',
|
||||
]
|
||||
|
||||
const now = Date.now()
|
||||
const oneHourAgo = now - (60 * 60 * 1000)
|
||||
|
||||
locales.forEach((locale) => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale })
|
||||
|
||||
const { result } = renderHook(() => useFormatTimeFromNow())
|
||||
const formatted = result.current.formatTimeFromNow(oneHourAgo)
|
||||
|
||||
// Should return a non-empty string for each locale
|
||||
expect(typeof formatted).toBe('string')
|
||||
expect(formatted.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Performance', () => {
|
||||
/**
|
||||
* Test that the hook doesn't create new functions on every render
|
||||
* The formatTimeFromNow function should be memoized with useCallback
|
||||
*/
|
||||
it('should memoize formatTimeFromNow function', () => {
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: 'en-US' })
|
||||
|
||||
const { result, rerender } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
const firstFunction = result.current.formatTimeFromNow
|
||||
rerender()
|
||||
const secondFunction = result.current.formatTimeFromNow
|
||||
|
||||
// Same locale should return the same function reference
|
||||
expect(firstFunction).toBe(secondFunction)
|
||||
})
|
||||
|
||||
/**
|
||||
* Test that changing locale creates a new function
|
||||
* This ensures the memoization dependency on locale works correctly
|
||||
*/
|
||||
it('should create new function when locale changes', () => {
|
||||
const { result, rerender } = renderHook(() => useFormatTimeFromNow())
|
||||
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: 'en-US' })
|
||||
rerender()
|
||||
const englishFunction = result.current.formatTimeFromNow
|
||||
|
||||
;(useI18N as jest.Mock).mockReturnValue({ locale: 'es-ES' })
|
||||
rerender()
|
||||
const spanishFunction = result.current.formatTimeFromNow
|
||||
|
||||
// Different locale should return different function reference
|
||||
expect(englishFunction).not.toBe(spanishFunction)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user