🔧test: util and lib unit tests
This commit is contained in:
parent
5edc773535
commit
f70bb2705d
@ -76,6 +76,10 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@tanstack/router-plugin": "^1.116.1",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/culori": "^2.1.1",
|
||||
"@types/lodash.debounce": "^4",
|
||||
"@types/node": "^22.14.1",
|
||||
@ -87,6 +91,7 @@
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.26.1",
|
||||
|
||||
225
web-app/src/lib/__tests__/models.test.ts
Normal file
225
web-app/src/lib/__tests__/models.test.ts
Normal file
@ -0,0 +1,225 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import {
|
||||
defaultModel,
|
||||
extractDescription,
|
||||
removeYamlFrontMatter,
|
||||
extractModelName,
|
||||
extractModelRepo,
|
||||
} from '../models'
|
||||
|
||||
// Mock the token.js module
|
||||
vi.mock('token.js', () => ({
|
||||
models: {
|
||||
openai: {
|
||||
models: ['gpt-3.5-turbo', 'gpt-4'],
|
||||
},
|
||||
anthropic: {
|
||||
models: ['claude-3-sonnet', 'claude-3-haiku'],
|
||||
},
|
||||
mistral: {
|
||||
models: ['mistral-7b', 'mistral-8x7b'],
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
describe('defaultModel', () => {
|
||||
it('returns first OpenAI model when no provider is given', () => {
|
||||
expect(defaultModel()).toBe('gpt-3.5-turbo')
|
||||
})
|
||||
|
||||
it('returns first OpenAI model when unknown provider is given', () => {
|
||||
expect(defaultModel('unknown')).toBe('gpt-3.5-turbo')
|
||||
})
|
||||
|
||||
it('returns first model for known providers', () => {
|
||||
expect(defaultModel('anthropic')).toBe('claude-3-sonnet')
|
||||
expect(defaultModel('mistral')).toBe('mistral-7b')
|
||||
})
|
||||
|
||||
it('handles empty string provider', () => {
|
||||
expect(defaultModel('')).toBe('gpt-3.5-turbo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractDescription', () => {
|
||||
it('returns undefined for falsy input', () => {
|
||||
expect(extractDescription()).toBeUndefined()
|
||||
expect(extractDescription('')).toBe('')
|
||||
})
|
||||
|
||||
it('extracts overview section from markdown', () => {
|
||||
const markdown = `# Model Title
|
||||
## Overview
|
||||
This is the model overview section.
|
||||
It has multiple lines.
|
||||
## Features
|
||||
This is another section.`
|
||||
|
||||
expect(extractDescription(markdown)).toBe(
|
||||
'This is the model overview section.\nIt has multiple lines.'
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to first 500 characters when no overview section', () => {
|
||||
const longText = 'A'.repeat(600)
|
||||
expect(extractDescription(longText)).toBe('A'.repeat(500))
|
||||
})
|
||||
|
||||
it('removes YAML front matter before extraction', () => {
|
||||
const markdownWithYaml = `---
|
||||
title: Model
|
||||
author: Test
|
||||
---
|
||||
# Model Title
|
||||
## Overview
|
||||
This is the overview.`
|
||||
|
||||
expect(extractDescription(markdownWithYaml)).toBe('This is the overview.')
|
||||
})
|
||||
|
||||
it('removes image markdown syntax', () => {
|
||||
const markdownWithImages = `## Overview
|
||||
This is text with  image.
|
||||
More text here.`
|
||||
|
||||
expect(extractDescription(markdownWithImages)).toBe(
|
||||
'This is text with image.\nMore text here.'
|
||||
)
|
||||
})
|
||||
|
||||
it('removes HTML img tags', () => {
|
||||
const markdownWithHtmlImages = `## Overview
|
||||
This is text with <img src="image.png" alt="alt"> image.
|
||||
More text here.`
|
||||
|
||||
expect(extractDescription(markdownWithHtmlImages)).toBe(
|
||||
'This is text with image.\nMore text here.'
|
||||
)
|
||||
})
|
||||
|
||||
it('handles text without overview section', () => {
|
||||
const simpleText = 'This is a simple description without sections.'
|
||||
expect(extractDescription(simpleText)).toBe(
|
||||
'This is a simple description without sections.'
|
||||
)
|
||||
})
|
||||
|
||||
it('extracts overview that ends at file end', () => {
|
||||
const markdown = `# Model Title
|
||||
## Overview
|
||||
This is the overview at the end.`
|
||||
|
||||
expect(extractDescription(markdown)).toBe(
|
||||
'This is the overview at the end.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeYamlFrontMatter', () => {
|
||||
it('removes YAML front matter from content', () => {
|
||||
const contentWithYaml = `---
|
||||
title: Test
|
||||
author: John
|
||||
---
|
||||
# Main Content
|
||||
This is the main content.`
|
||||
|
||||
const expected = `# Main Content
|
||||
This is the main content.`
|
||||
|
||||
expect(removeYamlFrontMatter(contentWithYaml)).toBe(expected)
|
||||
})
|
||||
|
||||
it('returns content unchanged when no YAML front matter', () => {
|
||||
const content = `# Main Content
|
||||
This is the main content.`
|
||||
|
||||
expect(removeYamlFrontMatter(content)).toBe(content)
|
||||
})
|
||||
|
||||
it('handles empty content', () => {
|
||||
expect(removeYamlFrontMatter('')).toBe('')
|
||||
})
|
||||
|
||||
it('handles content with only YAML front matter', () => {
|
||||
const yamlOnly = `---
|
||||
title: Test
|
||||
author: John
|
||||
---
|
||||
`
|
||||
|
||||
expect(removeYamlFrontMatter(yamlOnly)).toBe('')
|
||||
})
|
||||
|
||||
it('does not remove YAML-like content in middle of text', () => {
|
||||
const content = `# Title
|
||||
Some content here.
|
||||
---
|
||||
This is not front matter
|
||||
---
|
||||
More content.`
|
||||
|
||||
expect(removeYamlFrontMatter(content)).toBe(content)
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractModelName', () => {
|
||||
it('extracts model name from repo path', () => {
|
||||
expect(extractModelName('cortexso/tinyllama')).toBe('tinyllama')
|
||||
expect(extractModelName('microsoft/DialoGPT-medium')).toBe(
|
||||
'DialoGPT-medium'
|
||||
)
|
||||
expect(extractModelName('huggingface/CodeBERTa-small-v1')).toBe(
|
||||
'CodeBERTa-small-v1'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns the input when no slash is present', () => {
|
||||
expect(extractModelName('tinyllama')).toBe('tinyllama')
|
||||
expect(extractModelName('single-model-name')).toBe('single-model-name')
|
||||
})
|
||||
|
||||
it('handles undefined input', () => {
|
||||
expect(extractModelName()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles empty string', () => {
|
||||
expect(extractModelName('')).toBe('')
|
||||
})
|
||||
|
||||
it('handles multiple slashes', () => {
|
||||
expect(extractModelName('org/sub/model')).toBe('sub')
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractModelRepo', () => {
|
||||
it('extracts repo path from HuggingFace URL', () => {
|
||||
expect(extractModelRepo('https://huggingface.co/cortexso/tinyllama')).toBe(
|
||||
'cortexso/tinyllama'
|
||||
)
|
||||
expect(
|
||||
extractModelRepo('https://huggingface.co/microsoft/DialoGPT-medium')
|
||||
).toBe('microsoft/DialoGPT-medium')
|
||||
})
|
||||
|
||||
it('returns input unchanged when not a HuggingFace URL', () => {
|
||||
expect(extractModelRepo('cortexso/tinyllama')).toBe('cortexso/tinyllama')
|
||||
expect(extractModelRepo('https://github.com/user/repo')).toBe(
|
||||
'https://github.com/user/repo'
|
||||
)
|
||||
})
|
||||
|
||||
it('handles undefined input', () => {
|
||||
expect(extractModelRepo()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('handles empty string', () => {
|
||||
expect(extractModelRepo('')).toBe('')
|
||||
})
|
||||
|
||||
it('handles URLs with trailing slashes', () => {
|
||||
expect(extractModelRepo('https://huggingface.co/cortexso/tinyllama/')).toBe(
|
||||
'cortexso/tinyllama/'
|
||||
)
|
||||
})
|
||||
})
|
||||
237
web-app/src/lib/__tests__/utils.test.ts
Normal file
237
web-app/src/lib/__tests__/utils.test.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import {
|
||||
getProviderLogo,
|
||||
getProviderTitle,
|
||||
getReadableLanguageName,
|
||||
fuzzySearch,
|
||||
toGigabytes,
|
||||
formatMegaBytes,
|
||||
formatDuration,
|
||||
} from '../utils'
|
||||
|
||||
describe('getProviderLogo', () => {
|
||||
it('returns correct logo paths for known providers', () => {
|
||||
expect(getProviderLogo('llamacpp')).toBe(
|
||||
'/images/model-provider/llamacpp.svg'
|
||||
)
|
||||
expect(getProviderLogo('anthropic')).toBe(
|
||||
'/images/model-provider/anthropic.svg'
|
||||
)
|
||||
expect(getProviderLogo('openai')).toBe('/images/model-provider/openai.svg')
|
||||
expect(getProviderLogo('gemini')).toBe('/images/model-provider/gemini.svg')
|
||||
})
|
||||
|
||||
it('returns undefined for unknown providers', () => {
|
||||
expect(getProviderLogo('unknown')).toBeUndefined()
|
||||
expect(getProviderLogo('')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getProviderTitle', () => {
|
||||
it('returns formatted titles for special providers', () => {
|
||||
expect(getProviderTitle('llamacpp')).toBe('Llama.cpp')
|
||||
expect(getProviderTitle('openai')).toBe('OpenAI')
|
||||
expect(getProviderTitle('openrouter')).toBe('OpenRouter')
|
||||
expect(getProviderTitle('gemini')).toBe('Gemini')
|
||||
})
|
||||
|
||||
it('capitalizes first letter for unknown providers', () => {
|
||||
expect(getProviderTitle('anthropic')).toBe('Anthropic')
|
||||
expect(getProviderTitle('mistral')).toBe('Mistral')
|
||||
expect(getProviderTitle('test')).toBe('Test')
|
||||
})
|
||||
|
||||
it('handles empty strings', () => {
|
||||
expect(getProviderTitle('')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getReadableLanguageName', () => {
|
||||
it('returns full language names for known languages', () => {
|
||||
expect(getReadableLanguageName('js')).toBe('JavaScript')
|
||||
expect(getReadableLanguageName('ts')).toBe('TypeScript')
|
||||
expect(getReadableLanguageName('jsx')).toBe('React JSX')
|
||||
expect(getReadableLanguageName('py')).toBe('Python')
|
||||
expect(getReadableLanguageName('cpp')).toBe('C++')
|
||||
expect(getReadableLanguageName('yml')).toBe('YAML')
|
||||
})
|
||||
|
||||
it('capitalizes first letter for unknown languages', () => {
|
||||
expect(getReadableLanguageName('rust')).toBe('Rust')
|
||||
expect(getReadableLanguageName('unknown')).toBe('Unknown')
|
||||
expect(getReadableLanguageName('test')).toBe('Test')
|
||||
})
|
||||
|
||||
it('handles empty strings', () => {
|
||||
expect(getReadableLanguageName('')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fuzzySearch', () => {
|
||||
it('returns true for exact matches', () => {
|
||||
expect(fuzzySearch('hello', 'hello')).toBe(true)
|
||||
expect(fuzzySearch('test', 'test')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true for subsequence matches', () => {
|
||||
expect(fuzzySearch('hlo', 'hello')).toBe(true)
|
||||
expect(fuzzySearch('js', 'javascript')).toBe(true)
|
||||
expect(fuzzySearch('abc', 'aabbcc')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when needle is longer than haystack', () => {
|
||||
expect(fuzzySearch('hello', 'hi')).toBe(false)
|
||||
expect(fuzzySearch('test', 'te')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for non-matching patterns', () => {
|
||||
expect(fuzzySearch('xyz', 'hello')).toBe(false)
|
||||
expect(fuzzySearch('ba', 'abc')).toBe(false)
|
||||
})
|
||||
|
||||
it('handles empty strings', () => {
|
||||
expect(fuzzySearch('', '')).toBe(true)
|
||||
expect(fuzzySearch('', 'hello')).toBe(true)
|
||||
expect(fuzzySearch('h', '')).toBe(false)
|
||||
})
|
||||
|
||||
it('is case sensitive', () => {
|
||||
expect(fuzzySearch('H', 'hello')).toBe(false)
|
||||
expect(fuzzySearch('h', 'Hello')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toGigabytes', () => {
|
||||
it('returns empty string for falsy inputs', () => {
|
||||
expect(toGigabytes(0)).toBe('')
|
||||
expect(toGigabytes(null as unknown as number)).toBe('')
|
||||
expect(toGigabytes(undefined as unknown as number)).toBe('')
|
||||
})
|
||||
|
||||
it('formats bytes correctly', () => {
|
||||
expect(toGigabytes(500)).toBe('500B')
|
||||
expect(toGigabytes(1000)).toBe('1000B')
|
||||
})
|
||||
|
||||
it('formats kilobytes correctly', () => {
|
||||
expect(toGigabytes(1025)).toBe('1.00KB')
|
||||
expect(toGigabytes(2048)).toBe('2.00KB')
|
||||
expect(toGigabytes(1536)).toBe('1.50KB')
|
||||
})
|
||||
|
||||
it('formats exactly 1024 bytes as bytes', () => {
|
||||
expect(toGigabytes(1024)).toBe('1024B')
|
||||
})
|
||||
|
||||
it('formats megabytes correctly', () => {
|
||||
expect(toGigabytes(1024 ** 2 + 1)).toBe('1.00MB')
|
||||
expect(toGigabytes(1024 ** 2 * 2.5)).toBe('2.50MB')
|
||||
})
|
||||
|
||||
it('formats exactly 1024^2 bytes as KB', () => {
|
||||
expect(toGigabytes(1024 ** 2)).toBe('1024.00KB')
|
||||
})
|
||||
|
||||
it('formats gigabytes correctly', () => {
|
||||
expect(toGigabytes(1024 ** 3 + 1)).toBe('1.00GB')
|
||||
expect(toGigabytes(1024 ** 3 * 1.5)).toBe('1.50GB')
|
||||
})
|
||||
|
||||
it('formats exactly 1024^3 bytes as MB', () => {
|
||||
expect(toGigabytes(1024 ** 3)).toBe('1024.00MB')
|
||||
})
|
||||
|
||||
it('respects hideUnit option', () => {
|
||||
expect(toGigabytes(1025, { hideUnit: true })).toBe('1.00')
|
||||
expect(toGigabytes(1024 ** 2 + 1, { hideUnit: true })).toBe('1.00')
|
||||
expect(toGigabytes(500, { hideUnit: true })).toBe('500')
|
||||
expect(toGigabytes(1024, { hideUnit: true })).toBe('1024')
|
||||
})
|
||||
|
||||
it('respects toFixed option', () => {
|
||||
expect(toGigabytes(1536, { toFixed: 1 })).toBe('1.5KB')
|
||||
expect(toGigabytes(1536, { toFixed: 3 })).toBe('1.500KB')
|
||||
expect(toGigabytes(1024 ** 2 * 1.5, { toFixed: 0 })).toBe('2MB')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatMegaBytes', () => {
|
||||
it('formats values less than 1024 MB as GB', () => {
|
||||
expect(formatMegaBytes(512)).toBe('0.50 GB')
|
||||
expect(formatMegaBytes(1000)).toBe('0.98 GB')
|
||||
expect(formatMegaBytes(1023)).toBe('1.00 GB')
|
||||
})
|
||||
|
||||
it('formats values 1024*1024 MB and above as TB', () => {
|
||||
expect(formatMegaBytes(1024 * 1024)).toBe('1.00 TB')
|
||||
expect(formatMegaBytes(1024 * 1024 * 2.5)).toBe('2.50 TB')
|
||||
})
|
||||
|
||||
it('formats exactly 1024 MB as GB', () => {
|
||||
expect(formatMegaBytes(1024)).toBe('1.00 GB')
|
||||
})
|
||||
|
||||
it('handles zero and small values', () => {
|
||||
expect(formatMegaBytes(0)).toBe('0.00 GB')
|
||||
expect(formatMegaBytes(1)).toBe('0.00 GB')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatDuration', () => {
|
||||
it('formats milliseconds when duration is less than 1 second', () => {
|
||||
const start = Date.now()
|
||||
const end = start + 500
|
||||
expect(formatDuration(start, end)).toBe('500ms')
|
||||
})
|
||||
|
||||
it('formats seconds when duration is less than 1 minute', () => {
|
||||
const start = Date.now()
|
||||
const end = start + 30000 // 30 seconds
|
||||
expect(formatDuration(start, end)).toBe('30s')
|
||||
})
|
||||
|
||||
it('formats minutes and seconds when duration is less than 1 hour', () => {
|
||||
const start = Date.now()
|
||||
const end = start + 150000 // 2 minutes 30 seconds
|
||||
expect(formatDuration(start, end)).toBe('2m 30s')
|
||||
})
|
||||
|
||||
it('formats hours, minutes and seconds when duration is less than 1 day', () => {
|
||||
const start = Date.now()
|
||||
const end = start + 7890000 // 2 hours 11 minutes 30 seconds
|
||||
expect(formatDuration(start, end)).toBe('2h 11m 30s')
|
||||
})
|
||||
|
||||
it('formats days, hours, minutes and seconds for longer durations', () => {
|
||||
const start = Date.now()
|
||||
const end = start + 180000000 // 2 days 2 hours
|
||||
expect(formatDuration(start, end)).toBe('2d 2h 0m 0s')
|
||||
})
|
||||
|
||||
it('uses current time when endTime is not provided', () => {
|
||||
vi.useFakeTimers()
|
||||
const now = new Date('2023-01-01T12:00:00Z').getTime()
|
||||
vi.setSystemTime(now)
|
||||
|
||||
const start = now - 5000 // 5 seconds ago
|
||||
expect(formatDuration(start)).toBe('5s')
|
||||
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('handles negative durations (future start time)', () => {
|
||||
const start = Date.now() + 1000 // 1 second in the future
|
||||
const end = Date.now()
|
||||
expect(formatDuration(start, end)).toBe(
|
||||
'Invalid duration (start time is in the future)'
|
||||
)
|
||||
})
|
||||
|
||||
it('handles exact time boundaries', () => {
|
||||
const start = 0
|
||||
expect(formatDuration(start, 1000)).toBe('1s') // exactly 1 second
|
||||
expect(formatDuration(start, 60000)).toBe('1m 0s') // exactly 1 minute
|
||||
expect(formatDuration(start, 3600000)).toBe('1h 0m 0s') // exactly 1 hour
|
||||
expect(formatDuration(start, 86400000)).toBe('1d 0h 0m 0s') // exactly 1 day
|
||||
})
|
||||
})
|
||||
@ -1,2 +0,0 @@
|
||||
|
||||
|
||||
11
web-app/src/test/setup.ts
Normal file
11
web-app/src/test/setup.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { expect, afterEach } from 'vitest'
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import * as matchers from '@testing-library/jest-dom/matchers'
|
||||
|
||||
// extends Vitest's expect method with methods from react-testing-library
|
||||
expect.extend(matchers)
|
||||
|
||||
// runs a cleanup after each test case (e.g. clearing jsdom)
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
84
web-app/src/utils/__tests__/formatDate.test.ts
Normal file
84
web-app/src/utils/__tests__/formatDate.test.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { formatDate } from '../formatDate'
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('formats Date objects correctly', () => {
|
||||
const date = new Date('2023-12-25T15:30:45Z')
|
||||
const formatted = formatDate(date)
|
||||
|
||||
// The exact format depends on the system locale, but it should include key components
|
||||
expect(formatted).toMatch(/Dec.*25.*2023/i)
|
||||
expect(formatted).toMatch(/\d{1,2}:\d{2}/i) // time format
|
||||
expect(formatted).toMatch(/(AM|PM)/i)
|
||||
})
|
||||
|
||||
it('formats ISO string dates correctly', () => {
|
||||
const isoString = '2023-01-15T09:45:30Z'
|
||||
const formatted = formatDate(isoString)
|
||||
|
||||
expect(formatted).toMatch(/Jan.*15.*2023/i)
|
||||
expect(formatted).toMatch(/\d{1,2}:\d{2}/i)
|
||||
expect(formatted).toMatch(/(AM|PM)/i)
|
||||
})
|
||||
|
||||
it('formats timestamp numbers correctly', () => {
|
||||
const timestamp = 1703519445000 // Dec 25, 2023 15:30:45 UTC
|
||||
const formatted = formatDate(timestamp)
|
||||
|
||||
expect(formatted).toMatch(/Dec.*25.*2023/i)
|
||||
expect(formatted).toMatch(/\d{1,2}:\d{2}/i)
|
||||
expect(formatted).toMatch(/(AM|PM)/i)
|
||||
})
|
||||
|
||||
it('handles different months correctly', () => {
|
||||
const dates = [
|
||||
'2023-01-01T12:00:00Z',
|
||||
'2023-02-01T12:00:00Z',
|
||||
'2023-03-01T12:00:00Z',
|
||||
'2023-12-01T12:00:00Z'
|
||||
]
|
||||
|
||||
const formatted = dates.map(formatDate)
|
||||
|
||||
expect(formatted[0]).toMatch(/Jan.*1.*2023/i)
|
||||
expect(formatted[1]).toMatch(/Feb.*1.*2023/i)
|
||||
expect(formatted[2]).toMatch(/Mar.*1.*2023/i)
|
||||
expect(formatted[3]).toMatch(/Dec.*1.*2023/i)
|
||||
})
|
||||
|
||||
it('shows 12-hour format with AM/PM', () => {
|
||||
const morningDate = '2023-06-15T09:30:00Z'
|
||||
const eveningDate = '2023-06-15T21:30:00Z'
|
||||
|
||||
const morningFormatted = formatDate(morningDate)
|
||||
const eveningFormatted = formatDate(eveningDate)
|
||||
|
||||
// Note: The exact AM/PM depends on timezone, but both should have AM or PM
|
||||
expect(morningFormatted).toMatch(/(AM|PM)/i)
|
||||
expect(eveningFormatted).toMatch(/(AM|PM)/i)
|
||||
})
|
||||
|
||||
it('handles edge cases', () => {
|
||||
// Test with very old and very new dates
|
||||
const oldDate = '1900-01-01T00:00:00Z'
|
||||
const futureDate = '2099-12-31T23:59:59Z'
|
||||
|
||||
expect(() => formatDate(oldDate)).not.toThrow()
|
||||
expect(() => formatDate(futureDate)).not.toThrow()
|
||||
|
||||
expect(formatDate(oldDate)).toMatch(/Jan.*1.*1900/i)
|
||||
// The futureDate might be affected by timezone - let's just check it doesn't throw
|
||||
const futureDateResult = formatDate(futureDate)
|
||||
expect(futureDateResult).toMatch(/\d{4}/) // Should contain a year
|
||||
})
|
||||
|
||||
it('uses en-US locale formatting', () => {
|
||||
const date = '2023-07-04T12:00:00Z'
|
||||
const formatted = formatDate(date)
|
||||
|
||||
// Should use US-style date formatting (Month Day, Year)
|
||||
expect(formatted).toMatch(/Jul.*4.*2023/i)
|
||||
// Should include abbreviated month name
|
||||
expect(formatted).toMatch(/Jul/i)
|
||||
})
|
||||
})
|
||||
69
web-app/src/utils/__tests__/number.test.ts
Normal file
69
web-app/src/utils/__tests__/number.test.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { toNumber } from '../number'
|
||||
|
||||
describe('toNumber', () => {
|
||||
it('converts valid number strings to numbers', () => {
|
||||
expect(toNumber('123')).toBe(123)
|
||||
expect(toNumber('0')).toBe(0)
|
||||
expect(toNumber('-45')).toBe(-45)
|
||||
expect(toNumber('3.14')).toBe(3.14)
|
||||
expect(toNumber('-2.5')).toBe(-2.5)
|
||||
})
|
||||
|
||||
it('passes through actual numbers unchanged', () => {
|
||||
expect(toNumber(42)).toBe(42)
|
||||
expect(toNumber(0)).toBe(0)
|
||||
expect(toNumber(-17)).toBe(-17)
|
||||
expect(toNumber(3.14159)).toBe(3.14159)
|
||||
})
|
||||
|
||||
it('returns 0 for invalid number strings', () => {
|
||||
expect(toNumber('abc')).toBe(0)
|
||||
expect(toNumber('12abc')).toBe(0)
|
||||
expect(toNumber('hello')).toBe(0)
|
||||
expect(toNumber('')).toBe(0)
|
||||
expect(toNumber(' ')).toBe(0)
|
||||
})
|
||||
|
||||
it('returns 0 for null and undefined', () => {
|
||||
expect(toNumber(null)).toBe(0)
|
||||
expect(toNumber(undefined)).toBe(0)
|
||||
})
|
||||
|
||||
it('handles boolean values', () => {
|
||||
expect(toNumber(true)).toBe(1)
|
||||
expect(toNumber(false)).toBe(0)
|
||||
})
|
||||
|
||||
it('handles arrays and objects', () => {
|
||||
expect(toNumber([])).toBe(0)
|
||||
expect(toNumber([1])).toBe(1)
|
||||
expect(toNumber([1, 2])).toBe(0) // NaN case
|
||||
expect(toNumber({})).toBe(0)
|
||||
expect(toNumber({ a: 1 })).toBe(0)
|
||||
})
|
||||
|
||||
it('handles special number cases', () => {
|
||||
expect(toNumber(Infinity)).toBe(Infinity)
|
||||
expect(toNumber(-Infinity)).toBe(-Infinity)
|
||||
expect(toNumber(NaN)).toBe(0) // NaN gets converted to 0
|
||||
})
|
||||
|
||||
it('handles scientific notation strings', () => {
|
||||
expect(toNumber('1e5')).toBe(100000)
|
||||
expect(toNumber('2.5e-3')).toBe(0.0025)
|
||||
expect(toNumber('1E10')).toBe(10000000000)
|
||||
})
|
||||
|
||||
it('handles hex and octal strings', () => {
|
||||
expect(toNumber('0x10')).toBe(16)
|
||||
expect(toNumber('0o10')).toBe(8)
|
||||
expect(toNumber('0b10')).toBe(2)
|
||||
})
|
||||
|
||||
it('handles whitespace in strings', () => {
|
||||
expect(toNumber(' 123 ')).toBe(123)
|
||||
expect(toNumber('\t42\n')).toBe(42)
|
||||
expect(toNumber('\r\n -5.5 \t')).toBe(-5.5)
|
||||
})
|
||||
})
|
||||
30
web-app/vitest.config.ts
Normal file
30
web-app/vitest.config.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
globals: true,
|
||||
css: true,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
IS_TAURI: JSON.stringify('false'),
|
||||
IS_MACOS: JSON.stringify('false'),
|
||||
IS_WINDOWS: JSON.stringify('false'),
|
||||
IS_LINUX: JSON.stringify('false'),
|
||||
IS_IOS: JSON.stringify('false'),
|
||||
IS_ANDROID: JSON.stringify('false'),
|
||||
PLATFORM: JSON.stringify('web'),
|
||||
VERSION: JSON.stringify('test'),
|
||||
POSTHOG_KEY: JSON.stringify(''),
|
||||
POSTHOG_HOST: JSON.stringify(''),
|
||||
},
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user