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 TodayCountdown, type TodayEnvironment } from './TodayEnvironmentStrip.vue' const cleanups: Array<() => void> = [] 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)) 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:var(--text-primary);font-size:23px;line-height:1.15;font-weight:800') expect(css).toContain('margin-top:var(--space-6);color:var(--text-secondary);font-size:12px;font-weight:400') expect(css).toContain('.today-environment__eyeline{color:var(--text-secondary);font-size:11px') expect(css).toContain('.today-environment__item+.today-environment__item{border-left:1px solid var(--border-cream)}') 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(--border-cream);border-left:0!important}') expect(css).toContain('.today-environment__gold{grid-column:2;grid-row:2;border-top:1px solid var(--border-cream)}') expect(css).toContain('.today-environment__weather,.today-environment__gold{padding:var(--space-12) var(--space-10);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') }) 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') expect(oil.querySelector('small')?.textContent).toBe('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('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('就是今天') }) })