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('[data-date="2026-09-13"]')! next.click() await nextTick() expect(updates).toEqual([]) document.querySelector('[data-action="done"]')!.click() expect(updates).toEqual(['2026-09-13']) }) it('cancels without changing the original value', async () => { const { updates } = await mountPicker() document.querySelector('[data-date="2026-09-13"]')!.click() document.querySelector('[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('[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('[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() }) })