macos-web/tests/cases/33-appstore.test.ts

226 lines
8.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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