fix: harden habit input and lifecycle
This commit is contained in:
@@ -97,9 +97,9 @@ describe('mobile sheet contract', () => {
|
||||
expect(css).toContain('padding-bottom:calc(16px + env(safe-area-inset-bottom))')
|
||||
})
|
||||
|
||||
it('keeps destructive actions in a separate bottom danger zone', () => {
|
||||
expect(mvpPanel).toContain('class="app-sheet__danger"')
|
||||
expect(mvpPanel).toMatch(/app-sheet__danger[\s\S]*?deleteHabit\(selectedHabit\)/)
|
||||
it('keeps the danger zone reserved for archived habit deletion', () => {
|
||||
expect(mvpPanel).toContain('v-if="selectedHabit.archived_at" class="app-sheet__danger"')
|
||||
expect(mvpPanel).toContain('deleteHabit(selectedHabit)')
|
||||
expect(css).toContain('.app-sheet__danger{border-top:1px solid #f1d4cd;')
|
||||
})
|
||||
})
|
||||
@@ -117,19 +117,35 @@ describe('mobile list row language', () => {
|
||||
expect(css).toContain('.habit-progress{font-size:16px}')
|
||||
})
|
||||
|
||||
it('keeps destructive habit actions in a detail sheet instead of the list row', () => {
|
||||
it('offers edit and archive on active detail, with permanent delete only on archived detail', () => {
|
||||
expect(mvpPanel).not.toContain('<button class="icon ghost" aria-label="归档习惯"')
|
||||
expect(mvpPanel).toContain('class="habit-detail-mask app-sheet-mask"')
|
||||
expect(mvpPanel).toContain('class="habit-detail-sheet app-sheet app-sheet--detail"')
|
||||
expect(mvpPanel).toContain('@click="editHabit(selectedHabit)"')
|
||||
expect(mvpPanel).toContain('@click="archiveHabit(selectedHabit)"')
|
||||
expect(mvpPanel).toContain('v-if="selectedHabit.archived_at"')
|
||||
expect(mvpPanel).toContain('@click="deleteHabit(selectedHabit)"')
|
||||
expect(mvpPanel).toContain('永久删除')
|
||||
expect(mvpPanel).toContain("request(`/habits/${h.id}/permanent`, { method: 'DELETE' })")
|
||||
expect(mvpPanel).toContain('v-if="!selectedHabit.archived_at"')
|
||||
expect(mvpPanel).toContain('@click="openHabitDetail(h, $event.currentTarget as HTMLElement)"')
|
||||
expect(mvpPanel).toContain('@keydown.enter.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)"')
|
||||
expect(mvpPanel).toContain('ref="habitDetailSheet"')
|
||||
expect(mvpPanel).toContain("habitDetailSheet.value?.focus()")
|
||||
})
|
||||
|
||||
it('provides a minimal archived-habit viewing path', () => {
|
||||
expect(mvpPanel).toContain("request('/habits?archived=true')")
|
||||
expect(mvpPanel).toContain('已归档({{ archivedHabits.length }})')
|
||||
expect(mvpPanel).toContain('v-for="h in archivedHabits"')
|
||||
})
|
||||
|
||||
it('reuses the habit composer for edits and patches only changed fields', () => {
|
||||
expect(mvpPanel).toContain('const editingHabit = ref<Habit | null>(null)')
|
||||
expect(mvpPanel).toContain('changedHabitFields(originalHabitForm.value, normalized)')
|
||||
expect(mvpPanel).toContain("method: 'PATCH'")
|
||||
expect(mvpPanel).toContain("habitComposerTitle")
|
||||
expect(mvpPanel).toContain("habitFormError.value = reason instanceof Error")
|
||||
})
|
||||
})
|
||||
|
||||
describe('task and habit row decoration', () => {
|
||||
@@ -228,7 +244,7 @@ describe('task and habit row decoration', () => {
|
||||
expect(mvpPanel.match(/<Check v-if="isDone\(h, todayKey\)"/g)?.length).toBe(2)
|
||||
expect(css).toContain('.habit-check{')
|
||||
expect(mvpPanel).toContain('@pointerdown="startHabitPointer')
|
||||
const toggleBlock = mvpPanel.slice(mvpPanel.indexOf('async function toggleHabitFromButton'), mvpPanel.indexOf('async function addHabit'))
|
||||
const toggleBlock = mvpPanel.slice(mvpPanel.indexOf('async function toggleHabitFromButton'), mvpPanel.indexOf('function currentHabitForm'))
|
||||
expect(toggleBlock).toContain('setLocalHabitValue(h, next)')
|
||||
expect(toggleBlock).toContain('setLocalHabitValue(h, previous)')
|
||||
expect(toggleBlock).not.toContain('await loadHabits()')
|
||||
@@ -395,9 +411,46 @@ describe('mobile touch targets', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('approved habit safety and U2 title hierarchy', () => {
|
||||
it('keeps one page title and upgrades settings card headings without changing the card class', () => {
|
||||
expect(mvpPanel).not.toContain('<h2>习惯</h2>')
|
||||
expect(mvpPanel).not.toContain('<h2>设置与数据</h2>')
|
||||
expect(mvpPanel).toContain('<h2>数据导出与恢复</h2>')
|
||||
expect(mvpPanel).toContain('<h2>修改密码</h2>')
|
||||
expect(mvpPanel).toContain('<h2>登录会话</h2>')
|
||||
expect(mvpPanel).toContain('<h2>最近活动</h2>')
|
||||
expect(css).toContain('.tool-card>h2{')
|
||||
})
|
||||
|
||||
it('keeps invalid forms visible, disables save, and still shows the reason', () => {
|
||||
expect(app).toContain('const modalError = ref')
|
||||
expect(app).toContain('role="alert" class="field-error"')
|
||||
expect(app).toContain('normalizeRequiredName')
|
||||
expect(mvpPanel).toContain('habitErrors.name')
|
||||
expect(mvpPanel).toContain('aria-describedby="habit-name-error"')
|
||||
expect(mvpPanel).toContain('const habitFormInvalid = computed')
|
||||
expect(mvpPanel).toContain(':disabled="busy || habitFormInvalid"')
|
||||
expect(mvpPanel).toContain('请修正表单中的错误后再保存')
|
||||
})
|
||||
|
||||
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 .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}')
|
||||
})
|
||||
|
||||
it('disables illegal habit writes and keeps optimistic rollback', () => {
|
||||
expect(mvpPanel).toContain('habitAction(h)')
|
||||
expect(mvpPanel).toContain(':disabled="!habitAction(h).writable"')
|
||||
expect(mvpPanel).toContain(':aria-disabled="!habitAction(h).writable"')
|
||||
expect(mvpPanel).toContain('setLocalHabitValue(h, previous)')
|
||||
expect(mvpPanel).toContain('formatHabitApiError')
|
||||
})
|
||||
})
|
||||
|
||||
describe('settings data tools', () => {
|
||||
it('keeps backup export and restore but removes the standalone import tool', () => {
|
||||
expect(mvpPanel).toContain('<h3>数据导出与恢复</h3>')
|
||||
expect(mvpPanel).toContain('<h2>数据导出与恢复</h2>')
|
||||
expect(mvpPanel).toContain("fetch('/api/v1/export.csv'")
|
||||
expect(mvpPanel).toContain("'dodo-export.csv'")
|
||||
expect(mvpPanel).toContain('导出 CSV')
|
||||
|
||||
Reference in New Issue
Block a user