Skip to content

Commit 73d81f2

Browse files
committed
test(useDate): add coverage for inspection findings
- Add custom adapter type inference test - Add SSR tests for useDate/createDateFallback outside component - Add dynamic locale sync test with useLocale integration - Add leap year and invalid locale edge case tests - Add locale switching benchmarks (cache eviction, toggle)
1 parent b738da2 commit 73d81f2

File tree

3 files changed

+152
-0
lines changed

3 files changed

+152
-0
lines changed

packages/0/src/composables/useDate/index.bench.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,30 @@ describe('useDate benchmarks', () => {
114114
_date = adapter.setDate(_date, 1)
115115
})
116116
})
117+
118+
describe('locale switching', () => {
119+
bench('locale change + reformat', () => {
120+
const localAdapter = new Vuetify0DateAdapter('en-US')
121+
localAdapter.locale = 'de-DE'
122+
localAdapter.format(testDate, 'fullDate')
123+
})
124+
125+
bench('toggle locale 10 times', () => {
126+
const localAdapter = new Vuetify0DateAdapter('en-US')
127+
128+
for (let i = 0; i < 10; i++) {
129+
localAdapter.locale = i % 2 === 0 ? 'de-DE' : 'en-US'
130+
localAdapter.format(testDate, 'fullDate')
131+
}
132+
})
133+
134+
bench('cache eviction (60 unique formats)', () => {
135+
const localAdapter = new Vuetify0DateAdapter('en-US')
136+
137+
for (let i = 0; i < 60; i++) {
138+
localAdapter.formatByString(testDate, `YYYY-MM-DD-${i}`)
139+
}
140+
})
141+
})
117142
})
118143
})

packages/0/src/composables/useDate/index.ssr.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,49 @@ vi.mock('#v0/constants/globals', () => ({
1313

1414
// Import after mock is set up
1515
import { Vuetify0DateAdapter } from './adapters/v0'
16+
import { useDate, createDateFallback } from './index'
1617

1718
describe('useDate SSR', () => {
19+
describe('useDate outside component in SSR', () => {
20+
it('should return fallback context when called outside component', () => {
21+
// In SSR without getCurrentInstance(), useDate should return fallback
22+
const ctx = useDate()
23+
24+
expect(ctx).toBeDefined()
25+
expect(ctx.adapter).toBeInstanceOf(Vuetify0DateAdapter)
26+
expect(ctx.locale.value).toBe('en-US')
27+
})
28+
29+
it('should return fallback with custom namespace outside component', () => {
30+
const ctx = useDate('custom:namespace')
31+
32+
expect(ctx).toBeDefined()
33+
expect(ctx.adapter).toBeInstanceOf(Vuetify0DateAdapter)
34+
})
35+
36+
it('should return epoch from fallback adapter in SSR', () => {
37+
const ctx = useDate()
38+
const date = ctx.adapter.date()
39+
40+
// In SSR, date() without args returns epoch
41+
expect(date).not.toBeNull()
42+
expect(date!.year).toBe(1970)
43+
expect(date!.month).toBe(1)
44+
expect(date!.day).toBe(1)
45+
})
46+
47+
it('createDateFallback should work in SSR', () => {
48+
const fallback = createDateFallback()
49+
50+
expect(fallback.adapter).toBeInstanceOf(Vuetify0DateAdapter)
51+
expect(fallback.locale.value).toBe('en-US')
52+
53+
// Should use epoch for current date in SSR
54+
const date = fallback.adapter.date()
55+
expect(date!.year).toBe(1970)
56+
})
57+
})
58+
1859
describe('Vuetify0DateAdapter in SSR mode', () => {
1960
it('should return epoch when date() called without arguments', () => {
2061
const adapter = new Vuetify0DateAdapter('en-US')

packages/0/src/composables/useDate/index.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, expectTypeOf } from 'vitest'
22
import { Temporal } from '@js-temporal/polyfill'
33
import { createDate, createDateContext, createDateFallback, createDatePlugin, useDate } from './index'
44
import { Vuetify0DateAdapter } from './adapters/v0'
5+
import type { DateAdapter, DateContext } from './index'
56

67
describe('useDate', () => {
78
describe('Vuetify0DateAdapter', () => {
@@ -91,6 +92,21 @@ describe('useDate', () => {
9192
expect(date).toBeNull()
9293
})
9394

95+
it('should create Feb 29 in leap year', () => {
96+
const date = adapter.date('2024-02-29T10:30:00')
97+
98+
expect(date).not.toBeNull()
99+
expect(date!.year).toBe(2024)
100+
expect(date!.month).toBe(2)
101+
expect(date!.day).toBe(29)
102+
})
103+
104+
it('should return null for Feb 29 in non-leap year', () => {
105+
const date = adapter.date('2023-02-29T10:30:00')
106+
107+
expect(date).toBeNull()
108+
})
109+
94110
it('should convert to JavaScript Date', () => {
95111
const temporal = Temporal.PlainDateTime.from('2024-06-15T10:30:00')
96112
const jsDate = adapter.toJsDate(temporal)
@@ -156,6 +172,16 @@ describe('useDate', () => {
156172

157173
expect(month.toLowerCase()).toContain('juin')
158174
})
175+
176+
it('should handle invalid locale gracefully', () => {
177+
// Intl.DateTimeFormat falls back to default locale for invalid codes
178+
const invalidAdapter = new Vuetify0DateAdapter('invalid-XX')
179+
const date = invalidAdapter.date('2024-06-15T10:30:00')!
180+
181+
// Should not throw, Intl falls back gracefully
182+
expect(() => invalidAdapter.format(date, 'fullDate')).not.toThrow()
183+
expect(invalidAdapter.format(date, 'year')).toBe('2024')
184+
})
159185
})
160186

161187
describe('navigation - start/end boundaries', () => {
@@ -1373,6 +1399,59 @@ describe('useDate', () => {
13731399

13741400
wrapper.unmount()
13751401
})
1402+
1403+
it('should sync adapter locale dynamically when context created in component', async () => {
1404+
const { mount } = await import('@vue/test-utils')
1405+
const { defineComponent, nextTick } = await import('vue')
1406+
const { createLocalePlugin, useLocale } = await import('#v0/composables/useLocale')
1407+
1408+
// Locale plugin provides context at app level
1409+
const localePlugin = createLocalePlugin({
1410+
default: 'en',
1411+
messages: {
1412+
en: { hello: 'Hello' },
1413+
de: { hello: 'Hallo' },
1414+
},
1415+
})
1416+
1417+
let selectLocaleFn: ((id: string) => void) | null = null
1418+
1419+
const TestComponent = defineComponent({
1420+
setup () {
1421+
const localeContext = useLocale()
1422+
// Create date context INSIDE component for dynamic sync
1423+
const dateContext = createDate({
1424+
localeMap: { en: 'en-US', de: 'de-DE' },
1425+
})
1426+
1427+
selectLocaleFn = localeContext.select
1428+
1429+
return {
1430+
dateLocale: dateContext.locale,
1431+
adapterLocale: () => dateContext.adapter.locale,
1432+
}
1433+
},
1434+
template: '<div>{{ dateLocale }} - {{ adapterLocale() }}</div>',
1435+
})
1436+
1437+
const wrapper = mount(TestComponent, {
1438+
global: {
1439+
plugins: [localePlugin],
1440+
},
1441+
})
1442+
1443+
// Initial locale should be mapped from useLocale's selection
1444+
expect(wrapper.text()).toContain('en-US')
1445+
1446+
// Change locale dynamically
1447+
selectLocaleFn!('de')
1448+
await nextTick()
1449+
1450+
// Adapter locale should sync via watchEffect (context created in component)
1451+
expect(wrapper.text()).toContain('de-DE')
1452+
1453+
wrapper.unmount()
1454+
})
13761455
})
13771456

13781457
describe('type safety (compile-time)', () => {
@@ -1386,6 +1465,13 @@ describe('useDate', () => {
13861465
const customCtx = createDate({ adapter: customAdapter })
13871466
expectTypeOf(customCtx.adapter.date()).toEqualTypeOf<Temporal.PlainDateTime | null>()
13881467

1468+
// Mock adapter with different type: verifies generic inference (type-only)
1469+
const mockDateAdapter = {} as DateAdapter<Date>
1470+
const mockCtx = createDate({ adapter: mockDateAdapter })
1471+
// Type assertions only - mock adapter has no runtime implementation
1472+
expectTypeOf(mockCtx.adapter).toEqualTypeOf<DateAdapter<Date>>()
1473+
expectTypeOf(mockCtx).toEqualTypeOf<DateContext<Date>>()
1474+
13891475
// Context trinity returns correct type
13901476
const [, , ctx] = createDateContext()
13911477
expectTypeOf(ctx.adapter.date()).toEqualTypeOf<Temporal.PlainDateTime | null>()

0 commit comments

Comments
 (0)