/** * 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) }) }) })