Files
dodo/frontend/src/components/CalendarPicker.test.ts
T
bboysoul 30705e1cec
ci / gitleaks (push) Successful in 7s
ci / docker (push) Successful in 3m29s
feat: add responsive task calendar picker
2026-09-09 17:44:49 +08:00

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()
})
})