feat: 今天环境条新增下一个倒数日、浙江油价、上证指数、降雨/AQI
ci / gitleaks (push) Successful in 11s
ci / docker (push) Successful in 3m52s

- 后端新增三源:中石化浙江油价(3步会话)、新浪上证行情、Open-Meteo 空气质量(国标AQI换算),按源独立超时与状态
- 天气行改为 降雨概率% · AQI 等级(无雨无AQI时保留观测时间),stale 补缓存标记
- 前端 strip 六格两行(桌面)/四行(移动),倒数日取本地缓存最近一条,超12字截断
- 修复两处 AppSheet 动画竞态存量 e2e 失败(modal contract footer 时序、scrollable 重开残留 dialog)
This commit is contained in:
2026-09-25 09:50:32 +08:00
parent 4c145a2621
commit 273cd7aac7
11 changed files with 1212 additions and 34 deletions
@@ -2,11 +2,11 @@ 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'
import TodayEnvironmentStrip, { type TodayCountdown, type TodayEnvironment } from './TodayEnvironmentStrip.vue'
const cleanups: Array<() => void> = []
async function mountStrip(props: { environment: TodayEnvironment | null; loading?: boolean; failed?: boolean }) {
async function mountStrip(props: { environment: TodayEnvironment | null; countdown?: TodayCountdown | null; loading?: boolean; failed?: boolean }) {
const host = document.createElement('div')
document.body.append(host)
const app = createApp(() => h(TodayEnvironmentStrip, props))
@@ -151,4 +151,104 @@ describe('TodayEnvironmentStrip', () => {
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')
})
it('shows umbrella probability and Chinese AQI instead of the observation time', async () => {
const host = await mountStrip({ environment: {
calendar: environment.calendar,
weather: { status: 'fresh', text: '多云', temperature_c: 27, precipitation_probability: 60, observed_at: '2026-09-16T09:00:00+08:00' },
gold: environment.gold,
air: { status: 'fresh', aqi: 36, level: '优' },
} })
const weather = host.querySelector('.today-environment__weather')!
expect(weather.querySelector('strong')?.textContent).toBe('宁波 27°C 多云')
expect(weather.querySelector('small')?.textContent).toBe('降雨60% · AQI 36 优')
expect(host.textContent).not.toContain('海曙')
})
it('marks stale weather with cached data after the rain and AQI segments', async () => {
const host = await mountStrip({ environment: {
calendar: environment.calendar,
weather: { status: 'stale', text: '小雨', temperature_c: 24, precipitation_probability: 20 },
gold: environment.gold,
air: { status: 'stale', aqi: 85, level: '良' },
} })
expect(host.querySelector('.today-environment__weather small')?.textContent).toBe('降雨20% · AQI 85 良 · 缓存')
})
it('falls back to the observation time when rain and AQI are unavailable', async () => {
const host = await mountStrip({ environment: {
calendar: environment.calendar,
weather: { status: 'fresh', text: '晴', temperature_c: 30, observed_at: '2026-09-16T09:00:00+08:00' },
gold: environment.gold,
air: null,
} })
expect(host.querySelector('.today-environment__weather small')?.textContent).toBe('海曙 09:00')
})
it('renders Zhejiang oil and the SSE index with compact metadata', async () => {
const host = await mountStrip({ environment: {
calendar: environment.calendar,
weather: environment.weather,
gold: environment.gold,
oil: { status: 'fresh', gas_92: '8.58', gas_95: 9.12, gas_98: '11.12', diesel_0: '8.28', effective_at: '2026-09-24T00:12:00' },
ashare: { status: 'fresh', name: '上证指数', price: '3888.3738', prev_close: '3936.5199', change_percent: -1.22, as_of: '2026-09-24' },
} })
const oil = host.querySelector('.today-environment__oil')!
expect(oil.classList.contains('is-fresh')).toBe(true)
expect(oil.querySelector('strong')?.textContent).toBe('92# 8.58 · 95# 9.12')
expect(oil.querySelector('small')?.textContent).toBe('98# 11.12 · 09-24生效')
const ashare = host.querySelector('.today-environment__ashare')!
expect(ashare.querySelector('strong')?.textContent).toBe('上证 3888.37')
expect(ashare.querySelector('small')?.textContent).toBe('-1.22% · 09-24')
})
it('signs index gains and marks stale oil', async () => {
const host = await mountStrip({ environment: {
calendar: environment.calendar,
weather: environment.weather,
gold: environment.gold,
oil: { status: 'stale', gas_92: '8.58', gas_95: '9.12', gas_98: '11.12', effective_at: '2026-09-24T00:12:00', stale: true },
ashare: { status: 'fresh', name: '上证指数', price: '3900.5', change_percent: 0.35, as_of: '2026-09-25' },
} })
expect(host.querySelector('.today-environment__ashare small')?.textContent).toBe('+0.35% · 09-25')
const oil = host.querySelector('.today-environment__oil')!
expect(oil.classList.contains('is-stale')).toBe(true)
expect(oil.querySelector('small')?.textContent).toBe('98# 11.12 · 09-24生效 · 缓存')
})
it('renders unavailable oil and index sources as two-line placeholders', async () => {
const host = await mountStrip({ environment: {
calendar: environment.calendar,
weather: environment.weather,
gold: environment.gold,
oil: null,
ashare: null,
} })
expect(host.querySelectorAll('.today-environment__oil > .today-environment__skeleton')).toHaveLength(2)
expect(host.querySelectorAll('.today-environment__ashare > .today-environment__skeleton')).toHaveLength(2)
expect(host.textContent).toContain('浙江油价暂不可用')
expect(host.textContent).toContain('上证指数暂不可用')
})
it('hides the new slots when an older payload does not carry their keys', async () => {
const host = await mountStrip({ environment })
expect(host.querySelector('.today-environment__oil')).toBeNull()
expect(host.querySelector('.today-environment__ashare')).toBeNull()
expect(host.querySelector('.today-environment__countdown')).toBeNull()
})
it('shows the next countdown and truncates very long titles', async () => {
const host = await mountStrip({ environment, countdown: { title: '国庆节', days: 6, day_text: '还有 6 天' } })
const countdown = host.querySelector('.today-environment__countdown')!
expect(countdown.querySelector('strong')?.textContent).toBe('国庆节')
expect(countdown.querySelector('small')?.textContent).toBe('还有 6 天')
const long = await mountStrip({ environment, countdown: { title: '一二三四五六七八九十甲乙丙', days: 3, day_text: '还有 3 天' } })
expect(long.querySelector('.today-environment__countdown strong')?.textContent).toBe('一二三四五六七八九十甲…')
})
it('keeps a today countdown slot with its own copy', async () => {
const host = await mountStrip({ environment, countdown: { title: '中秋节', days: 0, day_text: '就是今天' } })
expect(host.querySelector('.today-environment__countdown small')?.textContent).toBe('就是今天')
})
})