155 lines
9.6 KiB
TypeScript
155 lines
9.6 KiB
TypeScript
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('9月16日 周三')
|
|
expect(host.textContent).toContain('农历八月初六')
|
|
const weather = host.querySelector('.today-environment__weather')!
|
|
expect(weather.children).toHaveLength(2)
|
|
expect(weather.querySelector('strong')?.textContent).toBe('宁波 27°C 多云')
|
|
expect(weather.querySelector('small')?.textContent).toBe('海曙 09:00')
|
|
expect(host.textContent).toContain('Au99.99 ¥782.35/g')
|
|
expect(host.textContent).toContain('上金所延迟价 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('9月16日 周三')
|
|
expect(host.textContent).toContain('宁波 28.4°C 多云')
|
|
expect(host.textContent).toContain('Au99.99 ¥935.99/g')
|
|
expect(host.textContent).toContain('上金所延迟价 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('海曙 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 () => {
|
|
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('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('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 () => {
|
|
const fresh = await mountStrip({ environment: { ...environment, gold: { ...environment.gold!, status: 'fresh' } } })
|
|
expect(fresh.querySelector('.today-environment')?.classList.contains('has-attention')).toBe(false)
|
|
expect(fresh.querySelector('.today-environment__status')?.textContent).toBe('')
|
|
|
|
const refreshing = await mountStrip({ environment, loading: true })
|
|
expect(refreshing.querySelector('.today-environment')?.classList.contains('is-refreshing')).toBe(true)
|
|
expect(refreshing.querySelector('.today-environment__status')?.textContent).toContain('更新中')
|
|
|
|
const failed = await mountStrip({ environment, failed: true })
|
|
expect(failed.querySelector('.today-environment')?.classList.contains('has-attention')).toBe(true)
|
|
expect(failed.querySelector('.today-environment__status')?.textContent).toContain('更新失败 · 上次信息')
|
|
})
|
|
|
|
it('renders unavailable sources as two-line placeholders without adding a status row', async () => {
|
|
const host = await mountStrip({ environment: {
|
|
calendar: environment.calendar,
|
|
weather: { status: 'unavailable' },
|
|
gold: { status: 'unavailable' },
|
|
} })
|
|
expect(host.querySelectorAll('.today-environment__skeleton')).toHaveLength(4)
|
|
expect(host.querySelectorAll('.today-environment__weather > .today-environment__skeleton')).toHaveLength(2)
|
|
expect(host.querySelectorAll('.today-environment__gold > .today-environment__skeleton')).toHaveLength(2)
|
|
})
|
|
|
|
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;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.toContain('.today-environment.has-attention:before')
|
|
expect(css).not.toMatch(/\.today-environment[^}]*gradient|\.today-environment[^}]*box-shadow|\.today-environment[^}]*backdrop-filter/)
|
|
})
|
|
|
|
it('matches the selected two-row mobile composition', () => {
|
|
const css = readFileSync(resolve(process.cwd(), 'src/style.css'), 'utf8')
|
|
expect(css).not.toContain('.today-track-head{')
|
|
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:row;align-items:baseline;justify-content:space-between')
|
|
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('@media(max-width:720px){.today-environment'), css.indexOf('/* Solid cream material system'))
|
|
expect(mobileBlock).not.toContain('text-overflow:ellipsis')
|
|
})
|
|
})
|