import { afterEach, describe, expect, it } from 'vitest' import { createApp, h, nextTick, ref } from 'vue' import TaskDueDisplay from './TaskDueDisplay.vue' const cleanups: Array<() => void> = [] const nowMs = new Date(2026, 8, 10, 12).getTime() async function mountDue(props: { dueAt: string | null; dueHasTime: boolean; completed: boolean; nowMs: number }) { const host = document.createElement('div') document.body.append(host) const values = ref(props) const app = createApp({ setup: () => () => h(TaskDueDisplay, values.value) }) app.mount(host) cleanups.push(() => { app.unmount(); host.remove() }) await nextTick() return { host, values } } afterEach(() => cleanups.splice(0).forEach((cleanup) => cleanup())) describe('TaskDueDisplay', () => { it.each([null, '', '2026-02-30'])('renders no DOM for %s', async (dueAt) => { const { host } = await mountDue({ dueAt, dueHasTime: false, completed: false, nowMs }) expect(host.innerHTML).toBe('') }) it('renders semantic canonical time with absolute and relative copy', async () => { const { host } = await mountDue({ dueAt: '2026-09-10', dueHasTime: false, completed: false, nowMs }) const root = host.querySelector('.task-due')! const time = root.querySelector('time')! expect(root.classList.contains('task-due--neutral')).toBe(true) expect(time.getAttribute('datetime')).toBe('2026-09-10') expect(time.textContent).toContain('9月10日') expect(time.textContent).toContain('今天') expect(time.getAttribute('title')).toBe(time.getAttribute('aria-label')) expect(time.getAttribute('title')).toContain('2026年9月10日') expect(root.querySelector('svg')?.getAttribute('aria-hidden')).toBe('true') }) it('distinguishes incomplete overdue and completed neutral states', async () => { const overdue = await mountDue({ dueAt: '2026-09-09', dueHasTime: false, completed: false, nowMs }) expect(overdue.host.querySelector('.task-due--overdue')).not.toBeNull() const completed = await mountDue({ dueAt: '2026-09-09', dueHasTime: false, completed: true, nowMs }) expect(completed.host.querySelector('.task-due--completed')).not.toBeNull() expect(completed.host.querySelector('.task-due--overdue')).toBeNull() expect(completed.host.textContent).toContain('已完成(原定1天前)') }) it('reacts when the shared nowMs prop advances', async () => { const dueAt = new Date(nowMs + 60_000).toISOString() const { host, values } = await mountDue({ dueAt, dueHasTime: true, completed: false, nowMs }) expect(host.textContent).toContain('还有1分钟') values.value = { ...values.value, nowMs: nowMs + 60_001 } await nextTick() expect(host.textContent).toContain('刚过期') }) })