87 lines
4.4 KiB
TypeScript
87 lines
4.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { createApp, h, nextTick, ref } from 'vue'
|
|
import CalendarPicker from './CalendarPicker.vue'
|
|
|
|
const mounted: Array<() => void> = []
|
|
|
|
async function mountPicker(initial = '2026-09-12', anchorRect = { left: 100, top: 100, bottom: 144, width: 120, height: 44 }) {
|
|
const host = document.createElement('div')
|
|
const anchor = document.createElement('button')
|
|
document.body.append(host, anchor)
|
|
Object.defineProperty(anchor, 'getBoundingClientRect', { value: () => anchorRect })
|
|
const open = ref(true)
|
|
const updates: string[] = []
|
|
const app = createApp({ setup: () => () => h(CalendarPicker, { open: open.value, modelValue: initial, anchor, 'onUpdate:open': (value: boolean) => { open.value = value }, 'onUpdate:modelValue': (value: string) => updates.push(value) }) })
|
|
app.mount(host)
|
|
mounted.push(() => { app.unmount(); host.remove(); anchor.remove() })
|
|
await nextTick()
|
|
return { open, updates }
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.stubGlobal('matchMedia', vi.fn().mockReturnValue({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() }))
|
|
})
|
|
afterEach(() => { mounted.splice(0).forEach((cleanup) => cleanup()); document.body.innerHTML = ''; vi.unstubAllGlobals() })
|
|
|
|
describe('CalendarPicker', () => {
|
|
it('keeps selection as a draft until 完成', async () => {
|
|
const { updates } = await mountPicker()
|
|
const next = document.querySelector<HTMLButtonElement>('[data-date="2026-09-13"]')!
|
|
next.click()
|
|
await nextTick()
|
|
expect(updates).toEqual([])
|
|
document.querySelector<HTMLButtonElement>('[data-action="done"]')!.click()
|
|
expect(updates).toEqual(['2026-09-13'])
|
|
})
|
|
|
|
it('cancels without changing the original value', async () => {
|
|
const { updates } = await mountPicker()
|
|
document.querySelector<HTMLButtonElement>('[data-date="2026-09-13"]')!.click()
|
|
document.querySelector<HTMLButtonElement>('[data-action="cancel"]')!.click()
|
|
expect(updates).toEqual([])
|
|
})
|
|
|
|
it('supports grid keyboard navigation, selection and Escape', async () => {
|
|
const { open, updates } = await mountPicker()
|
|
const selected = document.querySelector<HTMLButtonElement>('[aria-selected="true"]')!
|
|
selected.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
|
|
await nextTick()
|
|
expect(document.activeElement?.getAttribute('data-date')).toBe('2026-09-13')
|
|
document.activeElement?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
|
await nextTick()
|
|
expect(updates).toEqual([])
|
|
document.querySelector('[role="dialog"]')!.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
await nextTick()
|
|
expect(open.value).toBe(false)
|
|
expect(updates).toEqual([])
|
|
})
|
|
|
|
it('positions from the rendered popover height and shifts vertically when space is tight', async () => {
|
|
vi.stubGlobal('innerHeight', 500)
|
|
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
|
|
if (this.classList.contains('calendar-picker')) return { left: 0, top: 0, bottom: 388, right: 328, width: 328, height: 388, x: 0, y: 0, toJSON: () => ({}) }
|
|
return { left: 0, top: 0, bottom: 0, right: 0, width: 0, height: 0, x: 0, y: 0, toJSON: () => ({}) }
|
|
})
|
|
await mountPicker('2026-09-12', { left: 100, top: 220, bottom: 264, width: 120, height: 44 })
|
|
expect((document.querySelector('[role="dialog"]') as HTMLElement).style.top).toBe('100px')
|
|
})
|
|
|
|
it('moves focus into the newly visible month when month controls are used', async () => {
|
|
await mountPicker('2026-09-30')
|
|
document.querySelector<HTMLButtonElement>('[aria-label="下个月"]')!.click()
|
|
await nextTick()
|
|
expect(document.querySelectorAll('[role="gridcell"][tabindex="0"]')).toHaveLength(1)
|
|
expect(document.activeElement?.getAttribute('data-date')).toBe('2026-10-30')
|
|
})
|
|
|
|
it('marks today and selected days with accessible grid state', async () => {
|
|
vi.setSystemTime(new Date(2026, 8, 9, 12))
|
|
await mountPicker()
|
|
expect(document.querySelector('[role="dialog"][aria-modal="true"]')).not.toBeNull()
|
|
expect(document.querySelector('[role="grid"]')).not.toBeNull()
|
|
expect(document.querySelector('[data-date="2026-09-09"]')?.getAttribute('aria-current')).toBe('date')
|
|
expect(document.querySelector('[data-date="2026-09-12"]')?.getAttribute('aria-selected')).toBe('true')
|
|
vi.useRealTimers()
|
|
})
|
|
})
|