fix: restore numeric habit progress bars
ci / gitleaks (push) Successful in 9s
ci / docker (push) Successful in 3m34s

This commit is contained in:
2026-09-20 06:41:57 +08:00
parent 4973d56a64
commit 0fb98e894e
5 changed files with 47 additions and 12 deletions
+22 -5
View File
@@ -77,6 +77,7 @@ test('boolean habits complete in place while paused and unscheduled rows explain
if ((page.viewportSize()?.width ?? 0) <= 930) await bottomTab(page, '习惯').click()
else await page.locator('.sidebar').getByRole('button', { name: '习惯', exact: true }).click()
const booleanRow = page.locator('.habit-row').filter({ hasText: booleanName })
await expect(booleanRow.getByRole('progressbar')).toHaveCount(0)
const booleanCheck = booleanRow.getByRole('button', { name: `完成${booleanName}一次` })
const checkGeometry = await booleanCheck.evaluate((element) => {
const control = element.getBoundingClientRect()
@@ -153,16 +154,17 @@ test('approved polish keeps dense rows, title-only memos, and unique detail titl
const transparent = 'rgba(0, 0, 0, 0)'
await expect(taskRow).toHaveCSS('background-color', transparent)
await taskRow.hover()
await expect(taskRow).toHaveCSS('background-color', transparent)
await expect(taskRow).toHaveCSS('background-color', testInfo.project.name === 'desktop-1440' ? 'rgb(255, 248, 238)' : transparent)
await taskRow.locator('.task-main').click()
await expect(taskRow).toHaveClass(/selected/)
await expect(taskRow).toHaveCSS('background-color', transparent)
await expect(taskRow).toHaveCSS('background-color', 'rgb(255, 244, 229)')
const taskDetail = page.getByRole('dialog', { name: '任务详情' })
await expect(taskDetail.getByRole('button', { name: '保存更改' })).toBeEnabled()
await page.keyboard.press('Escape')
await taskDetail.getByRole('button', { name: '关闭详情' }).click()
await expect(taskDetail).toBeHidden()
await bottomTab(page, '习惯').click()
if ((page.viewportSize()?.width ?? 0) <= 930) await bottomTab(page, '习惯').click()
else await page.locator('.sidebar').getByRole('button', { name: '习惯', exact: true }).click()
const mobileHabitGeometry = await page.evaluate(() => {
const heading = document.querySelector<HTMLElement>('.habit-section-heading')!.getBoundingClientRect()
const list = document.querySelector<HTMLElement>('.habit-list')!.getBoundingClientRect()
@@ -178,14 +180,29 @@ test('approved polish keeps dense rows, title-only memos, and unique detail titl
await page.getByRole('button', { name: '添加习惯', exact: true }).click()
const habitRow = page.locator('.habit-row').filter({ hasText: habitName })
await expect(habitRow).toHaveCount(1)
await expect(habitRow.getByRole('progressbar', { name: `${habitName}进度:0 / 8` })).toHaveAttribute('max', '8')
await expect(habitRow.getByRole('progressbar', { name: `${habitName}进度:0 / 8` })).toHaveAttribute('value', '0')
await expect(habitRow.locator('.habit-row-meta')).toHaveText('0 / 8')
await expectHeightInRange(habitRow, 57, 59)
await expectNotClipped(habitRow)
await habitRow.getByRole('button', { name: `查看习惯详情:${habitName}` }).click()
await habitRow.getByRole('button', { name: `完成${habitName}一次` }).click()
await expect(habitRow.getByRole('progressbar', { name: `${habitName}进度:1 / 8` })).toHaveAttribute('value', '1')
await expect(habitRow.locator('.habit-row-meta')).toHaveText('1 / 8')
if ((page.viewportSize()?.width ?? 0) <= 930) await bottomTab(page, '今天').click()
else await page.locator('.sidebar').getByRole('button', { name: '今天', exact: true }).click()
const todayHabitRow = page.locator('.today-habit-row').filter({ hasText: habitName })
await expect(todayHabitRow.getByRole('progressbar', { name: `${habitName}进度:1 / 8` })).toHaveAttribute('max', '8')
await expect(todayHabitRow.getByRole('progressbar', { name: `${habitName}进度:1 / 8` })).toHaveAttribute('value', '1')
await expect(todayHabitRow.locator('.habit-row-meta')).toHaveText('1 / 8')
await expectHeightInRange(todayHabitRow, 57, 59)
await expectNotClipped(todayHabitRow)
await todayHabitRow.getByRole('button', { name: `查看习惯详情:${habitName}` }).click()
const habitDetail = page.getByRole('dialog', { name: habitName })
await expect(habitDetail.getByRole('heading', { name: habitName, exact: true })).toHaveCount(1)
await expect(habitDetail).not.toContainText('习惯详情')
await habitDetail.getByRole('button', { name: '关闭习惯详情' }).click()
await page.setViewportSize({ width: 1440, height: 900 })
await page.locator('.sidebar').getByRole('button', { name: '习惯', exact: true }).click()
const desktopHabitGeometry = await page.evaluate(() => {
const view = document.querySelector<HTMLElement>('main.list-main > .mvp-view')!.getBoundingClientRect()
const heading = document.querySelector<HTMLElement>('.habit-section-heading')!.getBoundingClientRect()
+8
View File
@@ -253,6 +253,12 @@ function habitProgressText(h: Habit) {
if (h.kind !== 'numeric') return ''
return `${Number(logFor(h, todayKey.value)?.value ?? 0)} / ${h.target ?? 1}`
}
function habitProgressMax(h: Habit) {
return Math.max(Number(h.target ?? 1), 1)
}
function habitProgressValue(h: Habit) {
return Math.min(Math.max(Number(logFor(h, todayKey.value)?.value ?? 0), 0), habitProgressMax(h))
}
function habitRowStatus(h: Habit) {
return habitAction(h).reason || habitProgressText(h) || (isDone(h, todayKey.value) ? '已完成' : '未完成')
}
@@ -813,6 +819,7 @@ onBeforeUnmount(() => {
<div class="habit-main" role="button" tabindex="0" :aria-label="`查看习惯详情:${h.name}`" @click="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.enter.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.space.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)">
<span class="habit-name" :title="h.name">{{ h.name }}</span>
</div>
<progress v-if="h.kind === 'numeric'" class="habit-progress-bar" :value="habitProgressValue(h)" :max="habitProgressMax(h)" :aria-label="`${h.name}进度:${habitProgressText(h)}`"></progress>
<small class="habit-row-meta" :title="habitRowStatus(h)" :aria-label="habitRowStatus(h)">{{ habitRowStatus(h) }}</small>
</article>
<div v-if="!visibleTodayHabits.length && !busy" class="empty-panel today-empty-panel"><span>{{ !showCompleted && todayHabits.length ? '已完成的习惯已隐藏。' : '今天没有安排习惯,轻松一下吧。' }}</span><button v-if="showCompleted || !todayHabits.length" class="soft-button empty-action" @click="openHabitComposer"><Check/>添加习惯</button></div>
@@ -846,6 +853,7 @@ onBeforeUnmount(() => {
<div class="habit-main" role="button" tabindex="0" :aria-label="`查看习惯详情:${h.name}`" @click="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.enter.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)" @keydown.space.prevent="openHabitDetail(h, $event.currentTarget as HTMLElement)">
<span class="habit-name" :title="h.name">{{ h.name }}</span>
</div>
<progress v-if="h.kind === 'numeric'" class="habit-progress-bar" :value="habitProgressValue(h)" :max="habitProgressMax(h)" :aria-label="`${h.name}进度:${habitProgressText(h)}`"></progress>
<small class="habit-row-meta" :title="habitRowStatus(h)" :aria-label="habitRowStatus(h)">{{ habitRowStatus(h) }}</small>
</article>
<div v-if="!visibleHabits.length && !busy" class="empty-panel">{{ !showCompleted && habits.length ? '已完成的习惯已隐藏' : '还没有习惯从一件容易坚持的小事开始' }}</div>
+10 -3
View File
@@ -101,10 +101,17 @@ input,select,textarea{background:var(--surface-raised);border-color:var(--border
.plain-list>.task-row:not(.task-row--trash).selected::before,.plain-list>.habit-row.selected::before{background:var(--accent)}
.plain-list .habit-check{margin-left:0}
.task-row{grid-template-columns:44px minmax(0,1fr) auto}
.habit-row{grid-template-columns:44px minmax(0,1fr) auto}
.habit-row{grid-template-columns:44px minmax(0,1fr) auto;grid-template-rows:minmax(0,1fr) 4px;row-gap:5px}
.task-row,.habit-row{display:grid}
.task-row:has(>.drag-handle),.habit-row:has(>.drag-handle){grid-template-columns:44px 44px minmax(0,1fr) auto}
.task-row:has(>.drag-handle){grid-template-columns:44px 44px minmax(0,1fr) auto}
.habit-row:has(>.drag-handle){grid-template-columns:44px 44px minmax(0,1fr) auto}
.task-main,.habit-main{min-width:0;padding:0}
.habit-row>.habit-check,.habit-row>.habit-drag-handle,.habit-row>.habit-row-meta{grid-row:1/3}
.habit-row>.habit-main{grid-row:1;align-self:end}
.habit-row>.habit-progress-bar{grid-column:2;grid-row:2;align-self:start;width:100%;height:4px}
.habit-row:has(>.habit-drag-handle)>.habit-progress-bar{grid-column:3}
.habit-row:not(:has(>.habit-progress-bar))>.habit-main{grid-row:1/3;align-self:center}
.habit-row>.habit-row-meta{grid-column:-2;align-self:center}
.task-main strong,.habit-name{display:block;min-width:0;font-size:15px;font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.task-tail,.habit-row-meta{min-width:0;max-width:132px;padding-left:12px;font-size:12px;font-weight:400;white-space:nowrap;text-align:right;overflow:hidden;text-overflow:ellipsis}
.plain-list .task-due--timed .task-due__text{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.2}.plain-list .task-due--timed .task-due__separator{display:inline}.plain-list>.task-row:has(>.task-tail .task-due--timed){height:58px;min-height:58px;max-height:58px}
@@ -148,7 +155,7 @@ main.today-main .topbar{margin-bottom:12px}
.today-environment__item small{margin-top:2px;font-size:10.5px}
.today-main :is(.task-row,.habit-row){height:58px;min-height:58px;max-height:58px;padding:0;background:transparent;border:0;border-bottom:1px solid #e8e0d5;border-radius:0;box-shadow:none}
.today-main .task-row{gap:0}.today-main .task-main{padding:0}.today-main .task-main strong,.today-main .habit-name{font-size:15px;font-weight:400}
.today-main .task-tail{padding-left:12px;font-size:12px}.today-main .habit-row{gap:0}.today-main .habit-main{display:flex;align-items:center;min-width:0;padding:0}
.today-main .task-tail{padding-left:12px;font-size:12px}.today-main .habit-row{gap:0;row-gap:5px}.today-main .habit-main{min-width:0;padding:0}
.shell.today-active .unified-fab{box-shadow:0 8px 18px rgba(174,65,29,.22)}.shell.today-active .unified-fab>svg{width:30px;height:30px;stroke-width:1.5}
@media(max-width:930px){main.today-main{padding-left:max(44px,calc((100% - 630px)/2));padding-right:max(44px,calc((100% - 630px)/2))}}
@media(max-width:720.98px){main.today-main{padding-left:29px!important;padding-right:29px!important;padding-bottom:calc(78px + var(--safe-area-bottom))}.today-environment{grid-template-columns:minmax(0,1fr) minmax(0,1fr);grid-template-rows:21px auto;align-items:stretch;column-gap:0;row-gap:7px;padding-bottom:10px}.today-environment__calendar{grid-column:1/-1;grid-row:1;height:21px!important;padding:0;justify-content:space-between;align-items:start;border-left:0}.today-environment__weather,.today-environment__gold{grid-row:2;height:clamp(38px,calc(25vw - 55.75px),42px);padding-top:7px;border-top:1px solid #e8e0d5}.today-environment__weather{grid-column:1;padding-left:0;padding-right:10px;border-left:0}.today-environment__gold{grid-column:2;padding-left:12px}.today-environment__weather strong,.today-environment__gold strong,.today-environment__gold-primary{line-height:16px}.today-environment__item small{line-height:13px}.today-heading{margin-top:16px}.today-page-title{font-size:24px}.today-heading .today-remaining{margin-top:4px}.today-main .task-row,.today-main .habit-row{height:58px;min-height:58px;max-height:58px}.today-context .completed-filter-pill{width:87px;height:44px;min-height:44px}.today-context .completed-filter-pill__track{width:30px;height:18px}.today-context .completed-filter-pill__thumb{width:14px;height:14px}.today-context .completed-filter-pill[aria-checked="true"] .completed-filter-pill__thumb{transform:translateX(12px)}}
+5 -2
View File
@@ -921,8 +921,11 @@ describe('task and habit row decoration', () => {
expect(mvpPanel).not.toMatch(/today-habit-list[\s\S]*?habit-drag-handle[\s\S]*?<!-- 完整习惯列表 -->/)
})
it('uses the same visible text status tail in Today and full habits', () => {
expect(mvpPanel).not.toContain('<progress v-if="h.kind === \'numeric\'" class="habit-progress-bar"')
it('shows independently accessible progress tracks for numeric habits in Today and full habits', () => {
expect(mvpPanel.match(/<progress v-if="h.kind === 'numeric'" class="habit-progress-bar"/g)?.length).toBe(2)
expect(mvpPanel.match(/:value="habitProgressValue\(h\)"/g)?.length).toBe(2)
expect(mvpPanel.match(/:max="habitProgressMax\(h\)"/g)?.length).toBe(2)
expect(mvpPanel.match(/<\/div>\n\s*<progress v-if="h.kind === 'numeric'" class="habit-progress-bar"/g)?.length).toBe(2)
expect(mvpPanel.match(/class="habit-row-meta"/g)?.length).toBe(2)
expect(mvpPanel.match(/:aria-label="habitRowStatus\(h\)"/g)?.length).toBe(2)
expect(mvpPanel).not.toContain('class="habit-state-note"')
+2 -2
View File
@@ -135,7 +135,7 @@ describe('approved five-detail polish', () => {
expect(css).toContain('.plain-list{display:grid;gap:0;background:transparent;border:0;border-radius:0;box-shadow:none;overflow:visible}')
expect(css).toContain('.plain-list-row,.plain-list>.task-row,.plain-list>.habit-row{height:58px;min-height:58px;max-height:58px;background:transparent;border:0;border-bottom:1px solid #e8e0d5;border-radius:0;box-shadow:none}')
expect(css).toContain('.task-row{grid-template-columns:44px minmax(0,1fr) auto}')
expect(css).toContain('.habit-row{grid-template-columns:44px minmax(0,1fr) auto}')
expect(css).toContain('.habit-row{grid-template-columns:44px minmax(0,1fr) auto;grid-template-rows:minmax(0,1fr) 4px;row-gap:5px}')
expect(css).toContain('.task-main strong,.habit-name{display:block;min-width:0;font-size:15px;font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}')
expect(css).toContain('.task-tail,.habit-row-meta{min-width:0;max-width:132px;padding-left:12px;font-size:12px;font-weight:400;white-space:nowrap;text-align:right;overflow:hidden;text-overflow:ellipsis}')
expect(css).toContain('.task-check{width:44px;')
@@ -148,7 +148,7 @@ describe('approved five-detail polish', () => {
const fullRows = habits.slice(habits.indexOf('class="habit-list plain-list">'), habits.indexOf('class="habit-archive-section"'))
for (const rows of [todayRows, fullRows]) {
expect(rows).not.toContain('habit-week')
expect(rows).not.toContain('habit-progress-bar')
expect(rows).toContain('<progress v-if="h.kind === \'numeric\'" class="habit-progress-bar"')
expect(rows).not.toContain('habit-state-note')
expect(rows).toContain('class="habit-row-meta"')
expect(rows).toContain(':aria-label="habitRowStatus(h)"')