fix: simplify date-only due labels
This commit is contained in:
@@ -24,26 +24,44 @@ describe('TaskDueDisplay', () => {
|
||||
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')).toBeNull()
|
||||
it('renders date-only visual text without an absolute date or explanatory prefixes', async () => {
|
||||
const cases = [
|
||||
{ dueAt: '2026-09-12', text: '2天', state: 'neutral', explanation: '还有2天' },
|
||||
{ dueAt: '2026-09-08', text: '2天', state: 'overdue', explanation: '已过期2天' },
|
||||
{ dueAt: '2026-09-10', text: '今天', state: 'neutral', explanation: '今天' },
|
||||
] as const
|
||||
for (const item of cases) {
|
||||
const { host } = await mountDue({ dueAt: item.dueAt, dueHasTime: false, completed: false, nowMs })
|
||||
const root = host.querySelector('.task-due')!
|
||||
const time = root.querySelector('time')!
|
||||
expect(root.classList.contains(`task-due--${item.state}`)).toBe(true)
|
||||
expect(time.textContent?.trim()).toBe(item.text)
|
||||
expect(time.textContent).not.toMatch(/2026|9月|还有|已过期/)
|
||||
expect(time.getAttribute('datetime')).toBe(item.dueAt)
|
||||
expect(time.getAttribute('title')).toBe(time.getAttribute('aria-label'))
|
||||
expect(time.getAttribute('title')).toContain(`2026年9月${Number(item.dueAt.slice(8))}日`)
|
||||
expect(time.getAttribute('title')).toContain(item.explanation)
|
||||
}
|
||||
})
|
||||
|
||||
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()
|
||||
it('keeps completed date-only deadlines neutral and visually minimal', async () => {
|
||||
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天前)')
|
||||
const root = completed.host.querySelector('.task-due')!
|
||||
const time = root.querySelector('time')!
|
||||
expect(root.classList.contains('task-due--completed')).toBe(true)
|
||||
expect(root.classList.contains('task-due--overdue')).toBe(false)
|
||||
expect(time.textContent?.trim()).toBe('已完成')
|
||||
expect(time.textContent).not.toMatch(/2026|9月|已过期|还有/)
|
||||
expect(time.getAttribute('title')).toContain('截止日期:2026年9月9日')
|
||||
expect(time.getAttribute('title')).toContain('已完成(原定1天前)')
|
||||
})
|
||||
|
||||
it('keeps timed task absolute and relative visual copy unchanged', async () => {
|
||||
const dueAt = new Date(nowMs + 2 * 60 * 60_000).toISOString()
|
||||
const { host } = await mountDue({ dueAt, dueHasTime: true, completed: false, nowMs })
|
||||
const time = host.querySelector('time')!
|
||||
expect(time.querySelector('.task-due__absolute')?.textContent).toMatch(/^9月10日 \d{2}:\d{2}$/)
|
||||
expect(time.querySelector('.task-due__relative')?.textContent).toBe('还有2小时')
|
||||
})
|
||||
|
||||
it('reacts when the shared nowMs prop advances', async () => {
|
||||
|
||||
@@ -13,15 +13,15 @@ const presentation = computed(() => getTaskDuePresentation(props))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="presentation" class="task-due" :class="`task-due--${presentation.state}`">
|
||||
<span v-if="presentation" class="task-due" :class="[`task-due--${presentation.state}`, { 'task-due--timed': dueHasTime }]">
|
||||
<time
|
||||
class="task-due__text"
|
||||
:datetime="presentation.datetime"
|
||||
:title="presentation.fullText"
|
||||
:aria-label="presentation.fullText"
|
||||
>
|
||||
<span class="task-due__absolute">{{ presentation.absoluteText }}</span>
|
||||
<span class="task-due__separator" aria-hidden="true"> · </span>
|
||||
<span v-if="dueHasTime" class="task-due__absolute">{{ presentation.absoluteText }}</span>
|
||||
<span v-if="dueHasTime" class="task-due__separator" aria-hidden="true"> · </span>
|
||||
<span class="task-due__relative">{{ presentation.relativeText }}</span>
|
||||
</time>
|
||||
</span>
|
||||
|
||||
@@ -66,15 +66,15 @@ describe('getTaskDuePresentation date-only tasks', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
['2026-09-09', '9月9日', '已过期1天', 'overdue'],
|
||||
['2026-09-10', '9月10日', '今天', 'neutral'],
|
||||
['2026-09-11', '9月11日', '还有1天', 'neutral'],
|
||||
['2027-01-02', '2027年1月2日', '还有114天', 'neutral'],
|
||||
] as const)('formats %s by local calendar day', (dueAt, absoluteText, relativeText, state) => {
|
||||
['2026-09-08', '2天', 'overdue', '已过期2天'],
|
||||
['2026-09-10', '今天', 'neutral', '今天'],
|
||||
['2026-09-12', '2天', 'neutral', '还有2天'],
|
||||
] as const)('uses compact visual copy for %s while preserving the full description', (dueAt, relativeText, state, fullRelativeText) => {
|
||||
const result = getTaskDuePresentation({ dueAt, dueHasTime: false, completed: false, nowMs })
|
||||
expect(result).toMatchObject({ datetime: dueAt, absoluteText, relativeText, state })
|
||||
expect(result?.fullText).toContain('截止日期:')
|
||||
expect(result?.fullText).toContain('2026年' === absoluteText.slice(0, 5) ? absoluteText : dueAt.startsWith('2026') ? `2026年${absoluteText}` : absoluteText)
|
||||
expect(result).toMatchObject({ datetime: dueAt, relativeText, state })
|
||||
expect(result?.relativeText).not.toMatch(/还有|已过期|月|日/)
|
||||
expect(result?.fullText).toContain(`截止日期:2026年${Number(dueAt.slice(5, 7))}月${Number(dueAt.slice(8, 10))}日`)
|
||||
expect(result?.fullText).toContain(fullRelativeText)
|
||||
})
|
||||
|
||||
it('compares calendar dates across a DST-sized 23 hour gap', () => {
|
||||
@@ -86,18 +86,18 @@ describe('getTaskDuePresentation date-only tasks', () => {
|
||||
completed: false,
|
||||
nowMs: before.getTime(),
|
||||
})
|
||||
expect(result?.relativeText).toBe('还有1天')
|
||||
expect(result?.relativeText).toBe('1天')
|
||||
expect(result?.fullText).toContain('还有1天')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['2026-09-09', '已完成(原定1天前)'],
|
||||
['2026-09-10', '已完成(原定今天)'],
|
||||
['2026-09-12', '已完成(原定2天后)'],
|
||||
])('uses completed neutral wording for %s', (dueAt, relativeText) => {
|
||||
expect(getTaskDuePresentation({ dueAt, dueHasTime: false, completed: true, nowMs })).toMatchObject({
|
||||
relativeText,
|
||||
state: 'completed',
|
||||
})
|
||||
])('uses minimal completed visual copy and preserves the detailed description for %s', (dueAt, fullRelativeText) => {
|
||||
const result = getTaskDuePresentation({ dueAt, dueHasTime: false, completed: true, nowMs })
|
||||
expect(result).toMatchObject({ relativeText: '已完成', state: 'completed' })
|
||||
expect(result?.fullText).toContain(fullRelativeText)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -161,13 +161,15 @@ export function getTaskDuePresentation(input: {
|
||||
const dayDelta = calendarDelta(parsed.date, now)
|
||||
const absoluteDateText = absoluteDate(parsed.date, now.getFullYear())
|
||||
const absoluteText = input.dueHasTime ? `${absoluteDateText} ${localTime(parsed.date)}` : absoluteDateText
|
||||
const completedDescription = completedRelative(dayDelta)
|
||||
const dateOnlyDescription = dayDelta === 0
|
||||
? '今天'
|
||||
: dayDelta > 0 ? `还有${dayDelta}天` : `已过期${Math.abs(dayDelta)}天`
|
||||
const relativeText = input.completed
|
||||
? completedRelative(dayDelta)
|
||||
? (input.dueHasTime ? completedDescription : '已完成')
|
||||
: input.dueHasTime
|
||||
? timedRelative(parsed.date.getTime() - input.nowMs)
|
||||
: dayDelta === 0
|
||||
? '今天'
|
||||
: dayDelta > 0 ? `还有${dayDelta}天` : `已过期${Math.abs(dayDelta)}天`
|
||||
: dayDelta === 0 ? '今天' : `${Math.abs(dayDelta)}天`
|
||||
const state: TaskDueState = input.completed
|
||||
? 'completed'
|
||||
: input.dueHasTime ? (parsed.date.getTime() < input.nowMs ? 'overdue' : 'neutral') : (dayDelta < 0 ? 'overdue' : 'neutral')
|
||||
@@ -178,7 +180,7 @@ export function getTaskDuePresentation(input: {
|
||||
datetime: parsed.datetime,
|
||||
absoluteText,
|
||||
relativeText,
|
||||
fullText: `${kind}:${fullAbsolute},${relativeText}`,
|
||||
fullText: `${kind}:${fullAbsolute},${input.completed ? completedDescription : input.dueHasTime ? relativeText : dateOnlyDescription}`,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -54,18 +54,27 @@ describe('unified task due display', () => {
|
||||
expect(css).toContain('text-align:right;white-space:nowrap')
|
||||
expect(css).toContain('.task-actions{display:flex;align-items:center;flex:0 0 auto}')
|
||||
expect(css).toContain('.task-row>.drag-handle,.task-row>.task-check,.task-actions>*{flex-shrink:0}')
|
||||
expect(css).toContain('@media(max-width:930px){.task-tail{flex:0 1 76px;max-width:76px;')
|
||||
expect(css).toContain('.task-row:has(>.task-tail){height:66px;max-height:68px;}')
|
||||
expect(css).toContain('.task-main{padding:4px 0}')
|
||||
expect(css).toContain('.task-due__text{display:grid;justify-items:end;overflow:hidden;text-overflow:ellipsis;')
|
||||
expect(css).toContain('.task-due__absolute,.task-due__relative{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}')
|
||||
expect(css).toContain('.task-due__separator{display:none}')
|
||||
expect(css).toContain('@media(max-width:930px){.task-tail{flex:0 1 auto;max-width:132px;')
|
||||
expect(css).toContain('.task-row:has(>.task-tail .task-due--timed){height:66px;max-height:68px;}')
|
||||
expect(css).toContain('.task-row:has(>.task-tail .task-due--timed) .task-main{padding:4px 0}')
|
||||
expect(css).toContain('.task-due--timed .task-due__text{display:grid;justify-items:end;overflow:hidden;text-overflow:ellipsis;')
|
||||
expect(css).toContain('.task-due--timed .task-due__absolute,.task-due--timed .task-due__relative{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}')
|
||||
expect(css).toContain('.task-due--timed .task-due__separator{display:none}')
|
||||
expect(css).not.toContain('.task-main .meta{display:flex;flex-wrap:wrap;')
|
||||
expect(css).toContain('.task-check{width:44px;height:44px;')
|
||||
expect(css).toContain('.icon,.ghost{min-width:44px;min-height:44px;')
|
||||
expect(css).toMatch(/\.restore\{[^}]*min-height:44px/)
|
||||
})
|
||||
|
||||
it('keeps date-only rows compact on mobile while preserving the timed two-line layout', () => {
|
||||
expect(css).toContain('@media(max-width:930px){.task-tail{flex:0 1 auto;max-width:132px;')
|
||||
expect(css).toContain('.task-tail:has(.task-due--timed){flex:0 1 76px;max-width:76px}')
|
||||
expect(css).toContain('.task-row:has(>.task-tail .task-due--timed){height:66px;max-height:68px;}')
|
||||
expect(css).not.toContain('.task-row:has(>.task-tail){height:66px;max-height:68px;}')
|
||||
expect(css).toContain('.task-due--timed .task-due__text{display:grid;justify-items:end;')
|
||||
expect(css).toContain('.task-due--timed .task-due__separator{display:none}')
|
||||
})
|
||||
|
||||
it('scopes content metadata and keeps date semantics without a visible calendar icon', () => {
|
||||
expect(css).toContain('.task-due--overdue{color:var(--danger)}')
|
||||
expect(css).toContain('.task-due--neutral,.task-due--completed{color:var(--muted)}')
|
||||
@@ -75,7 +84,7 @@ describe('unified task due display', () => {
|
||||
expect(taskDueDisplay).not.toContain('CalendarDays')
|
||||
expect(taskDueDisplay).toContain('<time')
|
||||
expect(taskDueDisplay).toContain(':aria-label="presentation.fullText"')
|
||||
expect(taskDueDisplay).toContain('class="task-due__separator"')
|
||||
expect(taskDueDisplay).toContain('v-if="dueHasTime" class="task-due__separator"')
|
||||
expect(css).not.toContain('.overdue-task .meta')
|
||||
expect(css).not.toContain('.overdue-badge')
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user