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