226 lines
8.7 KiB
TypeScript
226 lines
8.7 KiB
TypeScript
/**
|
||
* 33-appstore: AppStore Vue 组件化测试
|
||
*
|
||
* 覆盖: DOM 结构、4 个 tab 切换、发现页(Hero/热门)、分类页(分组显示)、
|
||
* 搜索(输入/结果)、更新页(已安装列表/空状态)、
|
||
* 安装/打开按钮、Store 持久化、卸载清理
|
||
*
|
||
* ⚠️ 安装模拟的 setTimeout 延迟在 jsdom 中可 with fake timers 测试;
|
||
* 此处验证按钮状态切换逻辑,定时器行为标注即可。
|
||
*/
|
||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||
import { h, render, nextTick } from 'vue'
|
||
import AppStoreComponent from '../../src/apps/appstore/AppStore.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: 'appstore', appId: 'appstore', el, body, timers: [] as any[], data: {} }
|
||
}
|
||
|
||
describe('33-appstore — AppStore Vue 组件化', () => {
|
||
let win: any
|
||
beforeEach(() => { localStorage.clear(); store.set('appstore-installed', null); setupDOM(); win = makeMockWin() })
|
||
afterEach(() => { render(null, win.body); win.el.remove() })
|
||
|
||
function mount() { const v = h(AppStoreComponent, { win }); render(v, win.body) }
|
||
|
||
// ==================== DOM 结构 ====================
|
||
describe('DOM 结构', () => {
|
||
it('4 个 tab 按钮存在', () => {
|
||
mount()
|
||
expect(win.body.querySelectorAll('.store-tab').length).toBe(4)
|
||
})
|
||
|
||
it('搜索框存在', () => {
|
||
mount()
|
||
expect(win.body.querySelector('.store-search')).toBeTruthy()
|
||
})
|
||
|
||
it('默认 tab 为发现(discover)', () => {
|
||
mount()
|
||
expect(win.body.querySelector('.store-tab.on')?.textContent).toContain('发现')
|
||
})
|
||
|
||
it('store-content 存在', () => {
|
||
mount()
|
||
expect(win.body.querySelector('.store-content')).toBeTruthy()
|
||
})
|
||
})
|
||
|
||
// ==================== Tab 切换 ====================
|
||
describe('Tab 切换', () => {
|
||
it('点击分类 tab 切换', async () => {
|
||
mount()
|
||
const tabs = win.body.querySelectorAll('.store-tab')
|
||
const catsTab = [...tabs].find(t => t.textContent.includes('分类')) as HTMLElement
|
||
catsTab.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||
await nextTick()
|
||
expect(win.body.querySelector('.store-tab.on')?.textContent).toContain('分类')
|
||
})
|
||
|
||
it('点击更新 tab 切换', async () => {
|
||
mount()
|
||
const tabs = win.body.querySelectorAll('.store-tab')
|
||
const updatesTab = [...tabs].find(t => t.textContent.includes('更新')) as HTMLElement
|
||
updatesTab.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||
await nextTick()
|
||
expect(win.body.querySelector('.store-tab.on')?.textContent).toContain('更新')
|
||
})
|
||
|
||
it('点击搜索 tab 切换', async () => {
|
||
mount()
|
||
const tabs = win.body.querySelectorAll('.store-tab')
|
||
const searchTab = [...tabs].find(t => t.textContent.includes('搜索')) as HTMLElement
|
||
searchTab.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||
await nextTick()
|
||
expect(win.body.querySelector('.store-tab.on')?.textContent).toContain('搜索')
|
||
})
|
||
})
|
||
|
||
// ==================== 发现页 ====================
|
||
describe('发现页', () => {
|
||
it('Hero 区域显示熊掌记', () => {
|
||
mount()
|
||
expect(win.body.querySelector('.store-hero')).toBeTruthy()
|
||
expect(win.body.querySelector('.store-hero-name')?.textContent).toBe('熊掌记')
|
||
})
|
||
|
||
it('热门 App 网格有 7 个卡片', () => {
|
||
mount()
|
||
// 除 Hero(熊掌记) 外,热门有 7 个
|
||
expect(win.body.querySelectorAll('.store-card').length).toBeGreaterThanOrEqual(7)
|
||
})
|
||
|
||
it('每个卡片有名称/描述/分类', () => {
|
||
mount()
|
||
// 仅检查 store-grid 内的卡片(排除 Hero 区域)
|
||
const cards = win.body.querySelectorAll('.store-grid .store-card')
|
||
expect(cards.length).toBeGreaterThan(0)
|
||
const first = cards[0] as HTMLElement
|
||
expect(first.querySelector('.store-name')).toBeTruthy()
|
||
expect(first.querySelector('.store-desc')).toBeTruthy()
|
||
expect(first.querySelector('.store-cat')).toBeTruthy()
|
||
})
|
||
})
|
||
|
||
// ==================== 分类页 ====================
|
||
describe('分类页', () => {
|
||
it('分类页按类别分组', async () => {
|
||
mount()
|
||
const tabs = win.body.querySelectorAll('.store-tab')
|
||
;([...tabs].find(t => t.textContent.includes('分类')) as HTMLElement)
|
||
.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||
await nextTick()
|
||
const titles = [...win.body.querySelectorAll('.store-h')].map(e => e.textContent)
|
||
expect(titles.length).toBeGreaterThanOrEqual(2)
|
||
})
|
||
})
|
||
|
||
// ==================== 搜索 ====================
|
||
describe('搜索', () => {
|
||
it('输入关键字自动切换到搜索 tab', async () => {
|
||
mount()
|
||
const input = win.body.querySelector('.store-search') as HTMLInputElement
|
||
input.value = '熊掌记'
|
||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||
// 等待 debounce
|
||
await new Promise(r => setTimeout(r, 250))
|
||
await nextTick()
|
||
expect(win.body.querySelector('.store-tab.on')?.textContent).toContain('搜索')
|
||
})
|
||
|
||
it('搜索匹配结果显示', async () => {
|
||
mount()
|
||
const input = win.body.querySelector('.store-search') as HTMLInputElement
|
||
input.value = '熊掌记'
|
||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||
await new Promise(r => setTimeout(r, 250))
|
||
await nextTick()
|
||
expect(win.body.querySelectorAll('.store-card').length).toBeGreaterThanOrEqual(1)
|
||
})
|
||
|
||
it('无匹配显示"没有找到相关 App"', async () => {
|
||
mount()
|
||
const input = win.body.querySelector('.store-search') as HTMLInputElement
|
||
input.value = 'zzz_nonexistent_app'
|
||
input.dispatchEvent(new Event('input', { bubbles: true }))
|
||
await new Promise(r => setTimeout(r, 250))
|
||
await nextTick()
|
||
expect(win.body.querySelector('.empty-state')?.textContent).toContain('没有找到')
|
||
})
|
||
})
|
||
|
||
// ==================== 安装/打开 ====================
|
||
describe('安装/打开', () => {
|
||
it('未安装时显示"获取"按钮', () => {
|
||
mount()
|
||
const btns = win.body.querySelectorAll('.store-get')
|
||
const getBtns = [...btns].filter(b => b.textContent === '获取')
|
||
expect(getBtns.length).toBeGreaterThanOrEqual(1)
|
||
})
|
||
|
||
it('已安装时显示"打开"按钮', () => {
|
||
store.set('appstore-installed', ['bear'])
|
||
mount()
|
||
// Hero 区域的熊掌记按钮应为"打开"
|
||
const heroBtn = win.body.querySelector('.hero-get .store-get')
|
||
expect(heroBtn?.textContent).toBe('打开')
|
||
})
|
||
|
||
it('点击"获取"后变为"安装中…"', async () => {
|
||
mount()
|
||
const getBtn = [...win.body.querySelectorAll('.store-get')].find(b => b.textContent === '获取') as HTMLElement
|
||
getBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||
await nextTick()
|
||
expect(getBtn.textContent).toBe('安装中…')
|
||
})
|
||
})
|
||
|
||
// ==================== 更新页 ====================
|
||
describe('更新页', () => {
|
||
it('无已安装时显示"全部 App 均为最新版本"', async () => {
|
||
mount()
|
||
const tabs = win.body.querySelectorAll('.store-tab')
|
||
;([...tabs].find(t => t.textContent.includes('更新')) as HTMLElement)
|
||
.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||
await nextTick()
|
||
expect(win.body.querySelector('.empty-state')?.textContent).toContain('最新版本')
|
||
})
|
||
|
||
it('有已安装时显示已安装列表', async () => {
|
||
store.set('appstore-installed', ['bear', 'vscode'])
|
||
mount()
|
||
const tabs = win.body.querySelectorAll('.store-tab')
|
||
;([...tabs].find(t => t.textContent.includes('更新')) as HTMLElement)
|
||
.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||
await nextTick()
|
||
expect(win.body.querySelectorAll('.store-card').length).toBeGreaterThanOrEqual(2)
|
||
})
|
||
})
|
||
|
||
// ==================== 持久化 ====================
|
||
describe('持久化', () => {
|
||
it('从 store 加载已安装列表', () => {
|
||
store.set('appstore-installed', ['bear', 'vscode'])
|
||
mount()
|
||
// 验证 bear 按钮文字为"打开"
|
||
const heroBtn = win.body.querySelector('.hero-get .store-get')
|
||
expect(heroBtn?.textContent).toBe('打开')
|
||
})
|
||
})
|
||
|
||
// ==================== 卸载 ====================
|
||
describe('卸载', () => {
|
||
it('卸载后 DOM 为空', () => {
|
||
mount()
|
||
render(null, win.body)
|
||
expect(win.body.children.length).toBe(0)
|
||
})
|
||
})
|
||
})
|