feat: 补写 weather 测试 — 29 测试(结构/城市管理/搜索建议/天气展示/侧边栏/持久化/卸载)
This commit is contained in:
parent
0df029272b
commit
9ab470a867
363
tests/cases/30-weather.test.ts
Normal file
363
tests/cases/30-weather.test.ts
Normal file
@ -0,0 +1,363 @@
|
||||
/**
|
||||
* 30-weather: Weather Vue 组件化测试
|
||||
*
|
||||
* 覆盖: DOM 结构(搜索框/城市列表/天气主面板)、城市管理(选择/添加/删除/持久化)、
|
||||
* 搜索建议、天气数据展示(当前温度/逐小时/7日预报)、WMO 天气码映射、
|
||||
* 模拟数据、加载状态、卸载清理
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { h, render, nextTick } from 'vue'
|
||||
import WeatherComponent from '../../src/apps/weather/Weather.vue'
|
||||
import { store } from '../../src/composables/useStore'
|
||||
import { setupDOM } from '../helpers'
|
||||
|
||||
function makeMockWin() {
|
||||
const el = document.createElement('div'); el.className = 'window'
|
||||
document.getElementById('window-layer')?.appendChild(el)
|
||||
const body = document.createElement('div'); body.className = 'win-body'
|
||||
el.appendChild(body)
|
||||
return { id: 'weather', appId: 'weather', el, body, timers: [] as any[], data: {} }
|
||||
}
|
||||
|
||||
describe('30-weather — Weather Vue 组件化', () => {
|
||||
let win: any
|
||||
beforeEach(() => { localStorage.clear(); store.set('weather', null); setupDOM(); win = makeMockWin() })
|
||||
afterEach(() => { render(null, win.body); win.el.remove() })
|
||||
|
||||
function mount() { const v = h(WeatherComponent, { win }); render(v, win.body) }
|
||||
|
||||
// ==================== DOM 结构 ====================
|
||||
describe('DOM 结构', () => {
|
||||
it('搜索输入框存在', () => {
|
||||
mount()
|
||||
expect(win.body.querySelector('.weather-search')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('城市侧边栏存在', () => {
|
||||
mount()
|
||||
expect(win.body.querySelector('.weather-side')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('天气主面板存在', () => {
|
||||
mount()
|
||||
expect(win.body.querySelector('.weather-main')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('初始城市列表包含默认城市', () => {
|
||||
mount()
|
||||
const cities = win.body.querySelectorAll('.weather-city')
|
||||
expect(cities.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('首个城市有 sel 类(选中态)', async () => {
|
||||
mount()
|
||||
await nextTick()
|
||||
const first = win.body.querySelector('.weather-city')
|
||||
expect(first?.classList.contains('sel')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== 城市管理 ====================
|
||||
describe('城市管理', () => {
|
||||
it('点击城市切换选中', async () => {
|
||||
mount()
|
||||
await nextTick()
|
||||
const cities = win.body.querySelectorAll('.weather-city')
|
||||
if (cities.length >= 2) {
|
||||
(cities[1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
await nextTick()
|
||||
expect(cities[1].classList.contains('sel')).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('搜索添加新城市', async () => {
|
||||
mount()
|
||||
const input = win.body.querySelector('.weather-search') as HTMLInputElement
|
||||
input.value = '杭州'
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await nextTick()
|
||||
const sug = win.body.querySelector('.weather-sug')
|
||||
expect(sug).toBeTruthy()
|
||||
// 点击建议添加
|
||||
const item = win.body.querySelector('.maps-result') as HTMLElement
|
||||
if (item) {
|
||||
item.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
await nextTick()
|
||||
const names = [...win.body.querySelectorAll('.weather-city b')].map((e: any) => e.textContent)
|
||||
expect(names).toContain('杭州')
|
||||
}
|
||||
})
|
||||
|
||||
it('重复城市不添加', async () => {
|
||||
mount()
|
||||
await nextTick()
|
||||
const beforeCount = win.body.querySelectorAll('.weather-city').length
|
||||
const input = win.body.querySelector('.weather-search') as HTMLInputElement
|
||||
const firstCity = win.body.querySelector('.weather-city b')?.textContent || ''
|
||||
input.value = firstCity
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await nextTick()
|
||||
// 已存在的城市不应出现在建议中
|
||||
const items = win.body.querySelectorAll('.maps-result')
|
||||
const names = [...items].map((e: any) => e.textContent)
|
||||
expect(names).not.toContain(firstCity)
|
||||
})
|
||||
|
||||
it('删除城市按钮存在', () => {
|
||||
mount()
|
||||
const delBtns = win.body.querySelectorAll('.weather-del')
|
||||
expect(delBtns.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('删除城市后从列表移除', async () => {
|
||||
// 先添加两个城市
|
||||
store.set('weather', { cities: ['北京', '上海'], cur: '北京', cache: {} })
|
||||
mount()
|
||||
await nextTick()
|
||||
const beforeCount = win.body.querySelectorAll('.weather-city').length
|
||||
const delBtn = win.body.querySelector('.weather-del') as HTMLElement
|
||||
delBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
await nextTick()
|
||||
const afterCount = win.body.querySelectorAll('.weather-city').length
|
||||
expect(afterCount).toBeLessThan(beforeCount)
|
||||
})
|
||||
|
||||
it('删除当前选中城市后自动切换', async () => {
|
||||
store.set('weather', { cities: ['北京', '上海'], cur: '北京', cache: {} })
|
||||
mount()
|
||||
await nextTick()
|
||||
const delBtn = win.body.querySelector('.weather-del') as HTMLElement
|
||||
delBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
await nextTick()
|
||||
// 当前城市应变更为剩余城市
|
||||
const selCity = win.body.querySelector('.weather-city.sel b')
|
||||
expect(selCity).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== 搜索建议 ====================
|
||||
describe('搜索建议', () => {
|
||||
it('空输入隐藏建议', () => {
|
||||
mount()
|
||||
const sug = win.body.querySelector('.weather-sug')
|
||||
expect(sug?.classList.contains('hidden')).toBe(true)
|
||||
})
|
||||
|
||||
it('输入关键字显示匹配城市', async () => {
|
||||
store.set('weather', { cities: ['北京'], cur: '北京', cache: {} })
|
||||
mount()
|
||||
await nextTick(); await nextTick() // 等待 onMounted 完成
|
||||
const input = win.body.querySelector('.weather-search') as HTMLInputElement
|
||||
input.value = '上海'
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await nextTick()
|
||||
const items = win.body.querySelectorAll('.weather-sug .maps-result')
|
||||
expect(items.length).toBeGreaterThanOrEqual(1)
|
||||
expect([...items].some(e => e.textContent?.includes('上海'))).toBe(true)
|
||||
})
|
||||
|
||||
it('无匹配时建议为空', async () => {
|
||||
mount()
|
||||
const input = win.body.querySelector('.weather-search') as HTMLInputElement
|
||||
input.value = 'zzz_no_city'
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await nextTick()
|
||||
const items = win.body.querySelectorAll('.weather-sug .maps-result')
|
||||
expect(items.length).toBe(0)
|
||||
})
|
||||
|
||||
it('已添加的城市不在建议中', async () => {
|
||||
mount()
|
||||
await nextTick()
|
||||
const input = win.body.querySelector('.weather-search') as HTMLInputElement
|
||||
const existingCity = win.body.querySelector('.weather-city b')?.textContent || ''
|
||||
input.value = existingCity
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
await nextTick()
|
||||
const items = [...win.body.querySelectorAll('.maps-result')].map(e => e.textContent)
|
||||
expect(items).not.toContain(existingCity)
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== 天气数据展示 ====================
|
||||
describe('天气数据展示', () => {
|
||||
// ⚠️ onMounted 立即调用 fetchCity(同步 simulate),数据瞬间就绪;
|
||||
// loading 状态仅在 fetchCity 异步场景可见,此处验证当前城市正常渲染
|
||||
it('无缓存时 onMounted 自动模拟数据,城市名称正确显示', async () => {
|
||||
store.set('weather', { cities: ['北京'], cur: '北京', cache: {} })
|
||||
mount()
|
||||
await nextTick()
|
||||
// fetchCity 同步执行 simulate,数据已就绪
|
||||
const city = win.body.querySelector('.weather-now-city')
|
||||
expect(city).toBeTruthy()
|
||||
expect(city?.textContent).toContain('北京')
|
||||
})
|
||||
|
||||
it('有缓存数据显示温度', async () => {
|
||||
// 预设模拟数据
|
||||
store.set('weather', {
|
||||
cities: ['北京'],
|
||||
cur: '北京',
|
||||
cache: {
|
||||
'北京': {
|
||||
temp: 25, code: 0,
|
||||
hourly: Array.from({ length: 12 }, (_, i) => ({ t: i, temp: 23 + Math.round(Math.sin(i / 3) * 4), code: 0 })),
|
||||
daily: [
|
||||
{ day: '今天', hi: 28, lo: 18, code: 0 },
|
||||
{ day: '周一', hi: 30, lo: 20, code: 1 },
|
||||
{ day: '周二', hi: 27, lo: 19, code: 2 },
|
||||
{ day: '周三', hi: 26, lo: 17, code: 3 },
|
||||
{ day: '周四', hi: 29, lo: 21, code: 61 },
|
||||
{ day: '周五', hi: 31, lo: 22, code: 0 },
|
||||
{ day: '周六', hi: 28, lo: 18, code: 80 },
|
||||
],
|
||||
simulated: true,
|
||||
}
|
||||
}
|
||||
})
|
||||
mount()
|
||||
await nextTick()
|
||||
expect(win.body.querySelector('.weather-now-temp')?.textContent).toContain('25°')
|
||||
})
|
||||
|
||||
it('缓存数据显示逐小时预报', async () => {
|
||||
store.set('weather', {
|
||||
cities: ['北京'], cur: '北京',
|
||||
cache: { '北京': { temp: 22, code: 2, hourly: Array.from({ length: 12 }, (_, i) => ({ t: i, temp: 20 + i % 5, code: i % 3 })), daily: [{ day: '今天', hi: 28, lo: 18, code: 0 }], simulated: true } }
|
||||
})
|
||||
mount()
|
||||
await nextTick()
|
||||
const hours = win.body.querySelectorAll('.weather-hour')
|
||||
expect(hours.length).toBe(12)
|
||||
})
|
||||
|
||||
it('缓存数据显示 7 日预报', async () => {
|
||||
store.set('weather', {
|
||||
cities: ['北京'], cur: '北京',
|
||||
cache: { '北京': { temp: 22, code: 2, hourly: Array.from({ length: 12 }, (_, i) => ({ t: i, temp: 20 + i % 5, code: 0 })), daily: Array.from({ length: 7 }, (_, i) => ({ day: ['今天', '周一', '周二', '周三', '周四', '周五', '周六'][i], hi: 25 + i, lo: 15 + i, code: i % 4 })), simulated: true } }
|
||||
})
|
||||
mount()
|
||||
await nextTick()
|
||||
const days = win.body.querySelectorAll('.weather-day')
|
||||
expect(days.length).toBe(7)
|
||||
})
|
||||
|
||||
it('模拟数据显示(模拟数据)标签', async () => {
|
||||
store.set('weather', {
|
||||
cities: ['北京'], cur: '北京',
|
||||
cache: { '北京': { temp: 22, code: 0, hourly: [{ t: 12, temp: 22, code: 0 }], daily: [{ day: '今天', hi: 28, lo: 18, code: 0 }], simulated: true } }
|
||||
})
|
||||
mount()
|
||||
await nextTick()
|
||||
expect(win.body.querySelector('.weather-now-city')?.textContent).toContain('模拟数据')
|
||||
})
|
||||
|
||||
it('非模拟数据不显示"模拟数据"标签', async () => {
|
||||
store.set('weather', {
|
||||
cities: ['北京'], cur: '北京',
|
||||
cache: { '北京': { temp: 22, code: 0, hourly: [{ t: 12, temp: 22, code: 0 }], daily: [{ day: '今天', hi: 28, lo: 18, code: 0 }] } }
|
||||
})
|
||||
mount()
|
||||
await nextTick()
|
||||
expect(win.body.querySelector('.weather-now-city')?.textContent).not.toContain('模拟数据')
|
||||
})
|
||||
|
||||
it('缓存数据显示最高/最低温度', async () => {
|
||||
store.set('weather', {
|
||||
cities: ['北京'], cur: '北京',
|
||||
cache: { '北京': { temp: 22, code: 0, hourly: [{ t: 12, temp: 22, code: 0 }], daily: [{ day: '今天', hi: 28, lo: 18, code: 0 }], simulated: true } }
|
||||
})
|
||||
mount()
|
||||
await nextTick()
|
||||
expect(win.body.querySelector('.weather-now-hl')?.textContent).toContain('28°')
|
||||
expect(win.body.querySelector('.weather-now-hl')?.textContent).toContain('18°')
|
||||
})
|
||||
|
||||
it('天气描述文本正确映射(code: 0 = 晴)', async () => {
|
||||
store.set('weather', {
|
||||
cities: ['北京'], cur: '北京',
|
||||
cache: { '北京': { temp: 25, code: 0, hourly: [{ t: 12, temp: 25, code: 0 }], daily: [{ day: '今天', hi: 28, lo: 18, code: 0 }], simulated: true } }
|
||||
})
|
||||
mount()
|
||||
await nextTick()
|
||||
expect(win.body.querySelector('.weather-now-desc')?.textContent).toContain('晴')
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== 城市侧边栏显示 ====================
|
||||
describe('城市侧边栏', () => {
|
||||
it('每个城市显示名称和温度', async () => {
|
||||
store.set('weather', {
|
||||
cities: ['北京', '上海'], cur: '北京',
|
||||
cache: {
|
||||
'北京': { temp: 25, code: 0, hourly: [{ t: 12, temp: 25, code: 0 }], daily: [{ day: '今天', hi: 28, lo: 18, code: 0 }], simulated: true },
|
||||
'上海': { temp: 28, code: 1, hourly: [{ t: 12, temp: 28, code: 1 }], daily: [{ day: '今天', hi: 32, lo: 24, code: 1 }], simulated: true },
|
||||
}
|
||||
})
|
||||
mount()
|
||||
await nextTick()
|
||||
const cities = win.body.querySelectorAll('.weather-city')
|
||||
expect(cities.length).toBe(2)
|
||||
expect(cities[0].querySelector('b')?.textContent).toBe('北京')
|
||||
expect(cities[1].querySelector('b')?.textContent).toBe('上海')
|
||||
expect(cities[0].querySelector('.weather-city-temp')?.textContent).toContain('25°')
|
||||
expect(cities[1].querySelector('.weather-city-temp')?.textContent).toContain('28°')
|
||||
})
|
||||
|
||||
// ⚠️ onMounted 调用 fetchCity 同步填充缓存;
|
||||
// 未预设缓存的城市会被立即模拟数据覆盖
|
||||
it('无预设缓存的城市在 onMounted 后获得模拟数据', async () => {
|
||||
store.set('weather', { cities: ['北京'], cur: '北京', cache: {} })
|
||||
mount()
|
||||
await nextTick()
|
||||
// fetchCity 同步生成模拟数据,温度不再是 …
|
||||
const temp = win.body.querySelector('.weather-city-temp')
|
||||
expect(temp).toBeTruthy()
|
||||
// 应为数字°格式
|
||||
expect(temp?.textContent).toMatch(/\d+°/)
|
||||
})
|
||||
|
||||
it('城市描述文本显示(通过 WMO 映射)', async () => {
|
||||
store.set('weather', {
|
||||
cities: ['北京'], cur: '北京',
|
||||
cache: { '北京': { temp: 22, code: 61, hourly: [{ t: 12, temp: 22, code: 61 }], daily: [{ day: '今天', hi: 25, lo: 18, code: 61 }], simulated: true } }
|
||||
})
|
||||
mount()
|
||||
await nextTick()
|
||||
expect(win.body.querySelector('.weather-city small')?.textContent).toBe('小雨')
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== 持久化 ====================
|
||||
describe('持久化', () => {
|
||||
it('store 初始数据来自 localStorage', () => {
|
||||
localStorage.setItem('weather', JSON.stringify({ cities: ['深圳'], cur: '深圳', cache: {} }))
|
||||
mount()
|
||||
const cities = win.body.querySelectorAll('.weather-city')
|
||||
expect(cities.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('选择城市后 store 更新', async () => {
|
||||
store.set('weather', { cities: ['北京', '上海'], cur: '北京', cache: {} })
|
||||
mount()
|
||||
await nextTick()
|
||||
const cities = win.body.querySelectorAll('.weather-city')
|
||||
const sh = [...cities].find(c => c.querySelector('b')?.textContent === '上海') as HTMLElement
|
||||
if (sh) {
|
||||
sh.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
await nextTick()
|
||||
}
|
||||
const data = store.get('weather', {})
|
||||
expect(data.cur).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
// ==================== 卸载 ====================
|
||||
describe('卸载', () => {
|
||||
it('卸载后 DOM 为空', () => {
|
||||
mount()
|
||||
render(null, win.body)
|
||||
expect(win.body.children.length).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user