顶栏新增时区选择器:加载时自动检测用户所在时区(Intl.DateTimeFormat), 检测不到则默认澳大利亚墨尔本。选定时区作为新建事件的默认 tzid 与下载 ICS 的 X-WR-TIMEZONE,不做时间换算(应用内部为 wall-clock 字符串)。 - 新增 src/lib/timezone.js(detect/list/validate/默认值) - useCalendar 持 state.timezone、setTimezone、addEvent 传 tzid、serialize 注入 X-WR-TIMEZONE - HeaderBar 顶栏下拉选择器 + i18n 文案 - 新增 test/timezone.test.js(15 项),全量 57 项通过 - README 简述功能,细节移至 docs/timezone.md
128 lines
4.0 KiB
JavaScript
128 lines
4.0 KiB
JavaScript
import { describe, it, expect, afterEach } from 'vitest'
|
|
import {
|
|
DEFAULT_TIMEZONE,
|
|
COMMON_TIMEZONES,
|
|
detectTimezone,
|
|
listTimezones,
|
|
isValidTimezone,
|
|
} from '../src/lib/timezone.js'
|
|
import { createEvent } from '../src/lib/event-factory.js'
|
|
|
|
describe('DEFAULT_TIMEZONE', () => {
|
|
it('is Australia/Melbourne per requirement', () => {
|
|
expect(DEFAULT_TIMEZONE).toBe('Australia/Melbourne')
|
|
})
|
|
})
|
|
|
|
describe('isValidTimezone', () => {
|
|
it('accepts valid IANA zone ids', () => {
|
|
expect(isValidTimezone('Australia/Melbourne')).toBe(true)
|
|
expect(isValidTimezone('Asia/Shanghai')).toBe(true)
|
|
expect(isValidTimezone('America/New_York')).toBe(true)
|
|
expect(isValidTimezone('Europe/London')).toBe(true)
|
|
})
|
|
|
|
it('rejects empty / non-string / malformed', () => {
|
|
expect(isValidTimezone('')).toBe(false)
|
|
expect(isValidTimezone(null)).toBe(false)
|
|
expect(isValidTimezone(undefined)).toBe(false)
|
|
expect(isValidTimezone(123)).toBe(false)
|
|
expect(isValidTimezone('UTC')).toBe(false) // 单段, 本应用要求至少一段 '/'
|
|
expect(isValidTimezone('Not A Zone')).toBe(false)
|
|
expect(isValidTimezone('Bogus/Zone/With Space')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('detectTimezone', () => {
|
|
// 保存原始 Intl 引用以便恢复
|
|
const origDateTimeFormat = Intl.DateTimeFormat
|
|
afterEach(() => {
|
|
Intl.DateTimeFormat = origDateTimeFormat
|
|
})
|
|
|
|
it('returns a valid IANA zone in a normal environment', () => {
|
|
// Node 环境总能解析出系统时区
|
|
const tz = detectTimezone()
|
|
expect(isValidTimezone(tz)).toBe(true)
|
|
})
|
|
|
|
it('falls back to Melbourne when Intl.DateTimeFormat throws', () => {
|
|
Intl.DateTimeFormat = function () {
|
|
throw new Error('unsupported')
|
|
}
|
|
expect(detectTimezone()).toBe(DEFAULT_TIMEZONE)
|
|
})
|
|
|
|
it('falls back to Melbourne when resolved timeZone is empty', () => {
|
|
Intl.DateTimeFormat = function () {
|
|
return { resolvedOptions: () => ({ timeZone: '' }) }
|
|
}
|
|
expect(detectTimezone()).toBe(DEFAULT_TIMEZONE)
|
|
})
|
|
|
|
it('falls back to Melbourne when resolved timeZone is malformed', () => {
|
|
Intl.DateTimeFormat = function () {
|
|
return { resolvedOptions: () => ({ timeZone: 'Not A Zone' }) }
|
|
}
|
|
expect(detectTimezone()).toBe(DEFAULT_TIMEZONE)
|
|
})
|
|
|
|
it('returns the detected zone when it is valid', () => {
|
|
Intl.DateTimeFormat = function () {
|
|
return { resolvedOptions: () => ({ timeZone: 'Asia/Shanghai' }) }
|
|
}
|
|
expect(detectTimezone()).toBe('Asia/Shanghai')
|
|
})
|
|
})
|
|
|
|
describe('listTimezones', () => {
|
|
it('always includes Melbourne, Shanghai, UTC', () => {
|
|
const list = listTimezones()
|
|
expect(list).toContain('Australia/Melbourne')
|
|
expect(list).toContain('Asia/Shanghai')
|
|
expect(list).toContain('UTC')
|
|
})
|
|
|
|
it('has no duplicates', () => {
|
|
const list = listTimezones()
|
|
expect(new Set(list).size).toBe(list.length)
|
|
})
|
|
|
|
it('places UTC first, rest sorted', () => {
|
|
const list = listTimezones()
|
|
expect(list[0]).toBe('UTC')
|
|
const rest = list.slice(1)
|
|
const sorted = [...rest].sort((a, b) => a.localeCompare(b))
|
|
expect(rest).toEqual(sorted)
|
|
})
|
|
|
|
it('every entry is a valid IANA zone (UTC excepted)', () => {
|
|
const list = listTimezones()
|
|
for (const tz of list) {
|
|
if (tz === 'UTC') continue
|
|
expect(isValidTimezone(tz)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('includes all COMMON_TIMEZONES entries', () => {
|
|
const list = listTimezones()
|
|
for (const tz of COMMON_TIMEZONES) {
|
|
expect(list).toContain(tz)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('createEvent timezone override', () => {
|
|
// 确认 useCalendar.addEvent 依赖的覆盖路径:传入 { tzid } 后新建事件的
|
|
// tzid 即为该值(而非 event-factory 的独立默认 'UTC')。
|
|
it('honors the tzid passed via overrides', () => {
|
|
const ev = createEvent({ tzid: 'Asia/Shanghai' })
|
|
expect(ev.tzid).toBe('Asia/Shanghai')
|
|
})
|
|
|
|
it('falls back to UTC when no tzid override is given (standalone default)', () => {
|
|
const ev = createEvent()
|
|
expect(ev.tzid).toBe('UTC')
|
|
})
|
|
})
|