feat: add Today environment summary
ci / gitleaks (push) Successful in 51s
ci / docker (push) Successful in 4m34s

This commit is contained in:
2026-09-16 12:23:59 +08:00
parent ed6d999ffb
commit 0a4d5f2f05
12 changed files with 1405 additions and 7 deletions
@@ -0,0 +1,98 @@
import { afterEach, describe, expect, it } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { createApp, h, nextTick } from 'vue'
import TodayEnvironmentStrip, { type TodayEnvironment } from './TodayEnvironmentStrip.vue'
const cleanups: Array<() => void> = []
async function mountStrip(props: { environment: TodayEnvironment | null; loading?: boolean; failed?: boolean }) {
const host = document.createElement('div')
document.body.append(host)
const app = createApp(() => h(TodayEnvironmentStrip, props))
app.mount(host)
cleanups.push(() => { app.unmount(); host.remove() })
await nextTick()
return host
}
afterEach(() => cleanups.splice(0).forEach((cleanup) => cleanup()))
const environment: TodayEnvironment = {
calendar: { solar_date: '2026年9月16日', weekday: '星期三', lunar_date: '农历八月初六' },
weather: { status: 'fresh', location: '宁波海曙', text: '多云', temperature_c: 27, observed_at: '2026-09-16T09:00:00+08:00' },
gold: { status: 'stale', symbol: 'Au99.99', price_cny_per_gram: 782.35, market_date: '2026-09-15', source: '上海黄金交易所' },
}
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('农历八月初六')
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.querySelector('.today-environment__gold')?.classList.contains('is-stale')).toBe(true)
})
it('accepts the aggregation endpoint field names and derives source status', async () => {
const host = await mountStrip({ environment: {
date: { solar_date: '2026-09-16', weekday: '星期三', lunar: '农历八月初六', timezone: 'Asia/Shanghai' },
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.querySelector('.today-environment__gold')?.classList.contains('is-stale')).toBe(true)
})
it('shows local loading, cached, and per-source unavailable states', async () => {
const loading = await mountStrip({ environment: null, loading: true })
expect(loading.querySelector('.today-environment')?.getAttribute('aria-busy')).toBe('true')
expect(loading.textContent).toContain('环境信息加载中')
const partial = await mountStrip({ environment: {
calendar: environment.calendar,
weather: { status: 'stale', location: '宁波海曙', text: '小雨', temperature_c: 24 },
gold: { status: 'unavailable', symbol: 'Au99.99' },
} })
expect(partial.textContent).toContain('缓存')
expect(partial.textContent).toContain('Au99.99 暂不可用')
expect(partial.textContent).not.toContain('环境信息加载中')
})
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('¥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('更新中')
})
it('announces refresh failure through the same live status slot', async () => {
const host = await mountStrip({ environment, failed: 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(host.querySelectorAll('.today-environment__refreshing')).toHaveLength(0)
})
it('keeps refresh announcements out of the visual row layout', () => {
const css = readFileSync(resolve(process.cwd(), 'src/style.css'), 'utf8')
expect(css).toContain('.today-environment__status:not(.today-environment__state){position:absolute;width:1px;height:1px;')
expect(css).toContain('@container(max-width:559px){.today-environment{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);')
expect(css).toContain('.today-environment__item{display:flex;gap:4px;overflow:hidden}')
})
})