feat: show task due distance
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
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('<!--v-if-->')
|
||||
})
|
||||
|
||||
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('刚过期')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { CalendarDays } from 'lucide-vue-next'
|
||||
import { getTaskDuePresentation } from '../lib/task-due'
|
||||
|
||||
const props = defineProps<{
|
||||
dueAt: string | null
|
||||
dueHasTime: boolean
|
||||
completed: boolean
|
||||
nowMs: number
|
||||
}>()
|
||||
|
||||
const presentation = computed(() => getTaskDuePresentation(props))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="presentation" class="task-due" :class="`task-due--${presentation.state}`">
|
||||
<CalendarDays class="task-due__icon" aria-hidden="true" />
|
||||
<time
|
||||
class="task-due__text"
|
||||
:datetime="presentation.datetime"
|
||||
:title="presentation.fullText"
|
||||
:aria-label="presentation.fullText"
|
||||
>
|
||||
<span class="task-due__absolute">{{ presentation.absoluteText }}</span>
|
||||
<span aria-hidden="true"> · </span>
|
||||
<span class="task-due__relative">{{ presentation.relativeText }}</span>
|
||||
</time>
|
||||
</span>
|
||||
</template>
|
||||
Reference in New Issue
Block a user