feat: repeat tasks after completion
This commit is contained in:
@@ -209,6 +209,31 @@ describe('completion feedback motion', () => {
|
||||
expect(css).toContain('@media(prefers-reduced-motion:reduce){.task-row.just-completed,.habit-row.just-completed,.task-row.just-completed .task-check-mark,.habit-row.just-completed .task-check-mark{animation:none}}')
|
||||
})
|
||||
|
||||
it('syncs the browser IANA timezone before loading user task data', () => {
|
||||
expect(app).toContain("Intl.DateTimeFormat().resolvedOptions().timeZone")
|
||||
expect(app).toContain("await api('/me', { method: 'PATCH', body: JSON.stringify({ timezone }) })")
|
||||
const bootstrapBlock = app.slice(app.indexOf('async function bootstrap()'), app.indexOf('async function submitAuth()'))
|
||||
expect(bootstrapBlock.indexOf("await syncBrowserTimezone(data.user?.timezone)")).toBeLessThan(bootstrapBlock.indexOf('await loadRestoredView()'))
|
||||
const authBlock = app.slice(app.indexOf('async function submitAuth()'), app.indexOf('async function loadTaskPages'))
|
||||
expect(authBlock.indexOf("await syncBrowserTimezone(data.user?.timezone)")).toBeLessThan(authBlock.indexOf('await loadRestoredView()'))
|
||||
})
|
||||
|
||||
it('saves due removal atomically without deleting recurrence a second time', () => {
|
||||
const saveTaskBlock = app.slice(app.indexOf('async function saveTask()'), app.indexOf('async function removeTask('))
|
||||
expect(saveTaskBlock).not.toContain("method: 'DELETE'")
|
||||
expect(saveTaskBlock).toContain('selectedTaskRecurrence.value = null')
|
||||
expect(saveTaskBlock).toContain("selectedTaskRepeat.value = 'none'")
|
||||
})
|
||||
|
||||
it('keeps all task composer touch controls at least 44px on mobile without horizontal overflow', () => {
|
||||
expect(css).toContain('@media(max-width:390px){.task-compose-sheet')
|
||||
expect(css).toContain('.task-compose-sheet .app-sheet__header>button{min-width:44px;min-height:44px}')
|
||||
expect(css).toContain('.task-compose-sheet input,.task-compose-sheet select,.task-compose-sheet button{min-height:44px}')
|
||||
expect(css).toContain('.task-compose-date-clear,.task-compose-time-remove{min-width:44px;min-height:44px}')
|
||||
expect(css).toContain('.task-compose-sheet .app-sheet__footer>button{min-height:44px}')
|
||||
expect(css).toContain('.task-compose-sheet{width:100%;max-width:100%;overflow-x:hidden}')
|
||||
})
|
||||
|
||||
it('restarts the pulse and ignores a stale timer on rapid repeat completion', () => {
|
||||
vi.useFakeTimers()
|
||||
const active = new Set<string>()
|
||||
@@ -774,7 +799,7 @@ describe('approved habit safety and U2 title hierarchy', () => {
|
||||
})
|
||||
|
||||
it('keeps the 390px habit sheets full width with 44px close and bottom actions', () => {
|
||||
expect(css).toContain('@media(max-width:390px){.habit-detail-sheet,.habit-compose-sheet{width:100%;max-width:100%}')
|
||||
expect(css).toContain('.habit-detail-sheet,.habit-compose-sheet{width:100%;max-width:100%}')
|
||||
expect(css).toContain('.habit-detail-sheet .app-sheet__header>button,.habit-compose-sheet .app-sheet__header>button{min-width:44px;min-height:44px}')
|
||||
expect(css).toContain('.habit-detail-sheet .app-sheet__footer>button,.habit-detail-sheet .app-sheet__danger>button,.habit-compose-sheet .app-sheet__footer>button{min-height:44px}')
|
||||
})
|
||||
@@ -852,8 +877,19 @@ describe('unified floating add interaction', () => {
|
||||
expect(app).toContain('const selectedTaskRepeat = ref')
|
||||
expect(app).toContain('重复<select v-model="composeRepeat"')
|
||||
expect(app).toContain('重复<select v-model="selectedTaskRepeat"')
|
||||
expect(app).toContain('rrule,')
|
||||
expect(app).toContain('<option value="yearly">每年</option><option value="after_completion">完成后重复</option><option value="custom">自定义…</option>')
|
||||
expect(app).toContain('完成后 <input v-model="composeAfterCompletionDays"')
|
||||
expect(app).toContain('完成后 <input v-model="selectedAfterCompletionDays"')
|
||||
expect(app).toContain('每次完成后,将截止时间顺延对应天数;首版永不结束')
|
||||
expect(app).toContain('buildTaskRecurrencePayload(composeRepeat.value')
|
||||
expect(app).toContain('buildTaskRecurrencePayload(value, { afterCompletionDays: selectedAfterCompletionDays.value')
|
||||
expect(app).toContain("api(`/tasks/${task.id}/recurrence`)")
|
||||
const createBlock = app.slice(app.indexOf('async function submitTaskCompose()'), app.indexOf('function toggleSidebar()'))
|
||||
expect(createBlock).toContain("api('/tasks',")
|
||||
expect(createBlock).not.toContain("api('/recurrences'")
|
||||
const saveBlock = app.slice(app.indexOf('async function updateSelectedTaskRepeat()'), app.indexOf('async function submitTaskCompose()'))
|
||||
expect(saveBlock).not.toContain('selectedTaskRepeat.value = parseTaskRecurrence')
|
||||
expect(css).toContain('.after-completion-fields input{width:76px;min-height:44px')
|
||||
expect(app).toContain('<option value="custom">自定义…</option>')
|
||||
expect(app).toContain('class="repeat-custom-fields"')
|
||||
expect(app).toContain('每隔')
|
||||
@@ -1078,7 +1114,9 @@ describe('sidebar layout', () => {
|
||||
})
|
||||
|
||||
it('loads archived lists during bootstrap so archived rows are visible after refresh', () => {
|
||||
expect(app).toContain('expandedFolders.value = new Set(folders.value.map((folder) => folder.id))\n await loadArchivedLists()\n void preloadCountdowns()\n await loadRestoredView()')
|
||||
const bootstrapBlock = app.slice(app.indexOf('async function bootstrap()'), app.indexOf('async function submitAuth()'))
|
||||
expect(bootstrapBlock).toContain('await loadArchivedLists()')
|
||||
expect(bootstrapBlock.indexOf('await loadArchivedLists()')).toBeLessThan(bootstrapBlock.indexOf('await loadRestoredView()'))
|
||||
})
|
||||
|
||||
it('styles archived rows through the compact disclosure structure', () => {
|
||||
|
||||
Reference in New Issue
Block a user