diff --git a/frontend/e2e/mobile-journeys.spec.ts b/frontend/e2e/mobile-journeys.spec.ts index 1db5145..f37ce88 100644 --- a/frontend/e2e/mobile-journeys.spec.ts +++ b/frontend/e2e/mobile-journeys.spec.ts @@ -20,6 +20,44 @@ async function assertInsideViewport(locator: Locator, page: Page) { expect(box!.y + box!.height).toBeLessThanOrEqual(viewport!.height + 1) } +test('Today paper composition has no overlap or horizontal overflow', async ({ page }) => { + await page.goto('/') + await expect(await bottomTab(page, '今天')).toHaveAttribute('aria-current', 'page') + const board = page.locator('.today-board') + const environment = page.locator('.today-environment') + const tracks = board.locator('.today-track') + await expect(board).toBeVisible() + await expect(environment).toBeVisible() + await expect(tracks).toHaveCount(2) + + const geometry = await board.evaluate((node) => { + const boardRect = node.getBoundingClientRect() + const environmentRect = node.querySelector('.today-environment')!.getBoundingClientRect() + const trackRects = [...node.querySelectorAll('.today-track')].map(item => item.getBoundingClientRect()) + return { + boardRight: boardRect.right, + boardBottom: boardRect.bottom, + boardScrollWidth: node.scrollWidth, + boardClientWidth: node.clientWidth, + environmentBottom: environmentRect.bottom, + trackTops: trackRects.map(rect => rect.top), + trackRights: trackRects.map(rect => rect.right), + trackBottoms: trackRects.map(rect => rect.bottom), + } + }) + expect(geometry.boardScrollWidth).toBeLessThanOrEqual(geometry.boardClientWidth) + expect(Math.min(...geometry.trackTops)).toBeGreaterThanOrEqual(geometry.environmentBottom - 1) + expect(Math.max(...geometry.trackRights)).toBeLessThanOrEqual(geometry.boardRight + 1) + expect(Math.max(...geometry.trackBottoms)).toBeLessThanOrEqual(geometry.boardBottom + 1) + + for (const track of await tracks.all()) { + const head = track.locator('.today-track-head') + const labels = head.locator('strong, span') + await expect(labels).toHaveCount(2) + const boxes = await labels.evaluateAll(nodes => nodes.map(node => node.getBoundingClientRect())) + expect(Math.abs(boxes[0].top - boxes[1].top)).toBeLessThan(4) + } +}) test('Today task persists through detail, completion and reopen', async ({ page }, testInfo) => { const title = `E2E 今日任务 ${testInfo.project.name}` await page.goto('/') diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 4d4ee4e..a710015 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -322,6 +322,8 @@ const activeName = computed(() => { return lists.value.find((item) => item.id === activeList.value)?.name || '收集箱' }) const todayHabitProgress = computed(() => `${todayHabitCompleted.value} / ${todayHabitTotal.value}`) +const todayTaskRemaining = computed(() => Math.max(0, todayTaskTotal.value - todayTaskCompleted.value)) +const todayHabitRemaining = computed(() => Math.max(0, todayHabitTotal.value - todayHabitCompleted.value)) const progressWidth = (completed: number, total: number) => `${total > 0 ? Math.min(100, Math.round(completed / total * 100)) : 0}%` const todayTaskProgressPercent = computed(() => progressWidth(todayTaskCompleted.value, todayTaskTotal.value)) const todayHabitProgressPercent = computed(() => progressWidth(todayHabitCompleted.value, todayHabitTotal.value)) @@ -1542,11 +1544,11 @@ onUnmounted(() => {
diff --git a/frontend/src/TodayEnvironment.test.ts b/frontend/src/TodayEnvironment.test.ts index 5169968..bd06cf9 100644 --- a/frontend/src/TodayEnvironment.test.ts +++ b/frontend/src/TodayEnvironment.test.ts @@ -39,33 +39,42 @@ describe('Today environment integration', () => { }) it('keeps mobile environment copy to two rows including refresh states', () => { - expect(css).toMatch(/@container\(max-width:559px\)\{\.today-environment\{[^}]*grid-template-rows:40px 66px/) - expect(css).toMatch(/\.today-environment__status\{[^}]*grid-column:auto;grid-row:auto/) + expect(css).toMatch(/@media\(max-width:720px\)\{\.today-environment\{[^}]*grid-template-rows:auto auto/) + expect(css).not.toMatch(/\.today-board\{[^}]*grid-template-rows:/) + expect(css).not.toContain('@container(max-width:720px){.today-environment') + expect(css).not.toContain('@container(min-width:721px){.today-environment') expect(css).not.toContain('.today-environment__refreshing') }) - it('renders方案 A as one labelled paper surface with a grouped progress region', () => { + it('renders方案 A as the selected landing-page paper composition', () => { expect(app.match(/class="today-board"/g)).toHaveLength(1) expect(app).toContain('
') expect(app).toContain('

今日纸笺

') expect(app).toContain('
') + expect(app).toContain('还有 {{ todayTaskRemaining }} 项') + expect(app).toContain('还有 {{ todayHabitRemaining }} 项') expect(app).toMatch(/today-board__progress[\s\S]*today-task-track[\s\S]*today-habit-track[\s\S]*<\/div>/) - expect(css).toMatch(/\.today-board\{[^}]*grid-template-rows:76px 62px[^}]*gap:1px/) - expect(css).toMatch(/\.today-board__progress\{[^}]*min-height:62px[^}]*display:grid[^}]*grid-template-columns:repeat\(2,minmax\(0,1fr\)\)/) - expect(css).toContain('@media(max-width:559px){.today-board{grid-template-rows:106px 70px;padding:0 12px}.today-board__progress{min-height:70px}') + expect(css).toMatch(/\.today-board\{[^}]*padding:0 18px 16px/) + expect(css).toMatch(/\.today-board__progress\{[^}]*display:grid[^}]*grid-template-columns:repeat\(2,minmax\(0,1fr\)\)/) + expect(css).toContain('.today-track:first-child{padding-left:0;padding-right:18px}') + expect(css).toContain('.today-track:last-child{padding-right:0;padding-left:18px;border-left:1px solid var(--line)}') + expect(css).not.toMatch(/@media\(max-width:720px\)\{\.today-board\{[^}]*padding:/) + expect(css).not.toMatch(/@media\(max-width:930px\)[\s\S]*?\.today-track-head\{[^}]*flex-direction:column/) + expect(css).toContain('.today-track-rail{display:block;height:5px;overflow:hidden;border-radius:999px;background:#eee5d9}') + expect(css).toContain('.today-track-fill{display:block;width:0;height:100%;border-radius:inherit;background:var(--success)') expect(css).not.toMatch(/\.today-(?:board|track)[^{]*\{[^}]*(?:gradient|backdrop-filter)/) }) it('keeps the editorial environment layout readable at desktop and narrow widths', () => { expect(css).toMatch(/\.today-environment\{[^}]*grid-column:1\/-1/) - expect(css).toMatch(/@container\(min-width:560px\)\{\.today-environment\{[^}]*grid-template-columns:minmax\(0,1\.45fr\) minmax\(0,1fr\) minmax\(0,1fr\)/) - expect(css).toMatch(/\.today-environment__calendar strong\{[^}]*font-size:25px/) - expect(css).toMatch(/@container\(max-width:559px\)\{\.today-environment\{[^}]*grid-template-columns:minmax\(0,1fr\) minmax\(0,1fr\)[^}]*grid-template-rows:40px 66px/) + expect(css).toMatch(/@media\(min-width:721px\)\{\.today-environment\{[^}]*grid-template-columns:minmax\(0,1\.25fr\) minmax\(0,1fr\) minmax\(0,1fr\)/) + expect(css).toMatch(/\.today-environment__calendar strong\{[^}]*font-size:23px/) + expect(css).toMatch(/@media\(max-width:720px\)\{\.today-environment\{[^}]*grid-template-columns:minmax\(0,1fr\) minmax\(0,1fr\)[^}]*grid-template-rows:auto auto/) expect(css).toMatch(/\.today-environment__calendar\{[^}]*grid-column:1\/-1[^}]*grid-row:1/) expect(css).toMatch(/\.today-environment__weather\{[^}]*grid-column:1[^}]*grid-row:2/) expect(css).toMatch(/\.today-environment__gold\{[^}]*grid-column:2[^}]*grid-row:2/) expect(css).toMatch(/\.today-environment__weather,\.today-environment__gold\{[^}]*flex-direction:column/) expect(css).not.toMatch(/\.today-environment[^}]*overflow-x:auto/) - expect(css).not.toMatch(/@container\(max-width:559px\)[\s\S]*?\.today-environment[^}]*text-overflow:ellipsis/) + expect(css).not.toMatch(/@media\(max-width:720px\)[\s\S]*?\.today-environment[^}]*text-overflow:ellipsis/) }) }) diff --git a/frontend/src/components/TodayEnvironmentStrip.test.ts b/frontend/src/components/TodayEnvironmentStrip.test.ts index d3d700c..1a67dc2 100644 --- a/frontend/src/components/TodayEnvironmentStrip.test.ts +++ b/frontend/src/components/TodayEnvironmentStrip.test.ts @@ -28,15 +28,13 @@ describe('TodayEnvironmentStrip', () => { it('renders date, fixed-location weather, and delayed SGE Au99.99 quote', async () => { const host = await mountStrip({ environment }) expect(host.querySelector('.today-environment')?.getAttribute('aria-label')).toBe('今日环境信息') - expect(host.textContent).toContain('2026年9月16日') - expect(host.textContent).toContain('星期三') + expect(host.textContent).toContain('9月16日 周三') expect(host.textContent).toContain('农历八月初六') expect(host.textContent).toContain('宁波海曙') - expect(host.textContent).toContain('多云') - expect(host.textContent).toContain('27°C') - expect(host.textContent).toContain('Au99.99 ¥782.35/g') - expect(host.textContent).toContain('上金所延时') - expect(host.textContent).toContain('2026-09-15') + expect(host.textContent).toContain('27° 多云') + expect(host.textContent).toContain('Au99.99 延时') + expect(host.textContent).toContain('¥782.35/g') + expect(host.textContent).toContain('市场日期 09-15') expect(host.querySelector('.today-environment__gold')?.classList.contains('is-stale')).toBe(true) }) @@ -46,10 +44,12 @@ describe('TodayEnvironmentStrip', () => { weather: { temperature_c: 28.4, weather_code: 2, observed_at: '2026-09-16T14:15:00+08:00', source: 'Open-Meteo', stale: false }, gold: { contract: 'Au99.99', latest_price: '935.990', market_date: '2026-09-16', delayed: true, source: '上海黄金交易所', stale: true }, } as TodayEnvironment }) - expect(host.textContent).toContain('2026-09-16 星期三') - expect(host.textContent).toMatch(/宁波海曙 · 多云\s+28\.4°C/) - expect(host.textContent).toContain('Au99.99 ¥935.99/g') - expect(host.textContent).toContain('上金所延时 · 2026-09-16') + expect(host.textContent).toContain('9月16日 周三') + expect(host.textContent).toContain('宁波海曙') + expect(host.textContent).toMatch(/28\.4°\s+多云/) + expect(host.textContent).toContain('Au99.99 延时') + expect(host.textContent).toContain('¥935.99/g') + expect(host.textContent).toContain('市场日期 09-16') expect(host.querySelector('.today-environment__gold')?.classList.contains('is-stale')).toBe(true) }) @@ -70,14 +70,16 @@ describe('TodayEnvironmentStrip', () => { it('keeps existing values visible while a refresh is in progress', async () => { const host = await mountStrip({ environment, loading: true }) - expect(host.textContent).toContain('27°C') - expect(host.textContent).toContain('Open-Meteo · 09:00') + expect(host.textContent).toContain('27° 多云') + expect(host.textContent).toContain('Open-Meteo 09:00') + expect(host.textContent).not.toContain('Open-Meteo · 09:00') expect(host.textContent).toContain('¥782.35/g') expect(host.querySelector('.today-environment')?.getAttribute('aria-busy')).toBe('true') const status = host.querySelector('.today-environment__status') expect(status?.getAttribute('role')).toBe('status') expect(status?.getAttribute('aria-live')).toBe('polite') expect(status?.textContent).toContain('更新中') + expect(status?.classList.contains('sr-only')).toBe(true) }) it('announces refresh failure through the same live status slot', async () => { @@ -91,12 +93,15 @@ describe('TodayEnvironmentStrip', () => { it('renders compact mobile date copy while preserving desktop and gold date semantics', async () => { const host = await mountStrip({ environment }) - expect(host.querySelector('.today-environment__calendar-main--desktop')?.textContent).toBe('2026年9月16日 星期三') + expect(host.querySelector('.today-environment__calendar-main--desktop')?.textContent).toBe('9月16日 周三') expect(host.querySelector('.today-environment__calendar-main--mobile')?.textContent).toBe('9月16日 周三') const goldDate = host.querySelector('.today-environment__gold-date--mobile') expect(goldDate?.textContent).toBe('09-15') expect(goldDate?.getAttribute('title')).toBe('市场日期 2026-09-15') expect(goldDate?.getAttribute('aria-label')).toBe('市场日期 2026-09-15') + const desktopGoldDate = host.querySelector('.today-environment__gold-date--desktop') + expect(desktopGoldDate?.textContent).toBe('09-15') + expect(desktopGoldDate?.getAttribute('aria-label')).toBe('市场日期 2026-09-15') }) it('shows edit-line state only when attention is required', async () => { @@ -124,28 +129,28 @@ describe('TodayEnvironmentStrip', () => { expect(host.querySelectorAll('.today-environment__gold > .today-environment__skeleton')).toHaveLength(2) }) - it('uses the approved cream edit-line typography and separators', () => { + it('uses the selected landing-page typography and full-height separators', () => { const css = readFileSync(resolve(process.cwd(), 'src/style.css'), 'utf8') - expect(css).toContain('.today-environment{grid-column:1/-1;position:relative;min-width:0;min-height:76px;') - expect(css).toContain('color:#302a24;font-size:25px;line-height:1;font-weight:680') - expect(css).toContain('color:#655b50;font-size:12px;font-weight:450') - expect(css).toContain('color:#8b8275;font-size:11px;font-weight:400') - expect(css).toContain('.today-environment:after{content:"";position:absolute;left:10px;right:10px;bottom:0;height:1px;background:#e4d5c0}') - expect(css).toContain('.today-environment.has-attention:before{content:"";position:absolute;left:0;top:50%;width:2px;height:14px;background:#f15a29') - expect(css).not.toContain('border-left:1px solid var(--border-cream)') + expect(css).toContain('.today-environment{grid-column:1/-1;position:relative;min-width:0;display:grid;grid-template-columns:minmax(0,1.25fr) minmax(0,1fr) minmax(0,1fr);align-items:stretch') + expect(css).toContain('color:#302a24;font-size:23px;line-height:1.15;font-weight:760') + expect(css).toContain('margin-top:6px;color:#655b50;font-size:12px;font-weight:450') + expect(css).toContain('.today-environment__eyeline{color:#8b8275;font-size:11px') + expect(css).toContain('.today-environment__item+.today-environment__item{border-left:1px solid var(--line)}') expect(css).not.toMatch(/\.today-environment[^}]*gradient|\.today-environment[^}]*box-shadow|\.today-environment[^}]*backdrop-filter/) }) - it('keeps mobile content to exactly two parent rows with stacked source columns', () => { + it('matches the selected two-row mobile composition', () => { const css = readFileSync(resolve(process.cwd(), 'src/style.css'), 'utf8') - expect(css).toContain('@container(max-width:559px){.today-environment{min-height:106px;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);grid-template-rows:40px 66px;column-gap:14px;') - expect(css).toContain('.today-environment__calendar{grid-column:1/-1;grid-row:1;display:flex;flex-direction:row;') - expect(css).toContain('.today-environment__weather,.today-environment__gold{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:4px;overflow:visible;white-space:normal}') - expect(css).toContain('.today-environment__gold{grid-column:2;grid-row:2;padding-left:14px}') - expect(css).toContain('.today-environment__gold:before{content:"";position:absolute;left:0;top:8px;width:5px;height:1px;background:#cbbda9}') + expect(css).toContain('.today-track-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;color:#776c60;font-size:12px}') + expect(css).toContain('.today-track-head strong{font-size:14px;line-height:1.2;color:#403a32}') + expect(css).toContain('@media(max-width:720px){.today-environment{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);grid-template-rows:auto auto}') + expect(css).toContain('.today-environment__calendar{grid-column:1/-1;grid-row:1;display:flex;flex-direction:column;align-items:flex-start') + expect(css).toContain('.today-environment__weather{grid-column:1;grid-row:2;border-top:1px solid var(--line);border-left:0!important}') + expect(css).toContain('.today-environment__gold{grid-column:2;grid-row:2;border-top:1px solid var(--line)}') + expect(css).toContain('.today-environment__weather,.today-environment__gold{padding:13px 10px;display:flex;flex-direction:column;') expect(css).toContain('.today-environment__calendar-main--desktop,.today-environment__gold-date--desktop{display:none}') expect(css).toContain('.today-environment__calendar-main--mobile,.today-environment__gold-date--mobile{display:inline}') - const mobileBlock = css.slice(css.indexOf('@container(max-width:559px){.today-environment'), css.indexOf('/* Solid cream material system')) + const mobileBlock = css.slice(css.indexOf('@media(max-width:720px){.today-environment'), css.indexOf('/* Solid cream material system')) expect(mobileBlock).not.toContain('text-overflow:ellipsis') }) }) diff --git a/frontend/src/components/TodayEnvironmentStrip.vue b/frontend/src/components/TodayEnvironmentStrip.vue index 39bb8ac..207a085 100644 --- a/frontend/src/components/TodayEnvironmentStrip.vue +++ b/frontend/src/components/TodayEnvironmentStrip.vue @@ -95,7 +95,7 @@ const weatherObservation = computed(() => { if (!value.observed_at) return source const observed = new Date(value.observed_at) if (Number.isNaN(observed.getTime())) return source - return `${source} · ${new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour: '2-digit', minute: '2-digit', hour12: false }).format(observed)}` + return `${source} ${new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', hour: '2-digit', minute: '2-digit', hour12: false }).format(observed)}` }) @@ -110,14 +110,15 @@ const weatherObservation = computed(() => { 环境信息暂不可用