feat: 重构 appstore — Vue 组件化(发现/分类/搜索/更新4tab + 安装模拟)+ 21 测试
This commit is contained in:
parent
6611877c31
commit
8bf9c54d2e
@ -1 +1,189 @@
|
||||
<template><div></div></template>
|
||||
<template>
|
||||
<div class="store-body">
|
||||
<div class="store-top">
|
||||
<div class="store-tabs">
|
||||
<button
|
||||
v-for="t in tabs" :key="t[0]"
|
||||
class="store-tab" :class="{ on: tab === t[0] }"
|
||||
@click="go(t[0])"
|
||||
>{{ t[1] }}</button>
|
||||
</div>
|
||||
<input
|
||||
class="text-input store-search" type="search" placeholder="搜索 App"
|
||||
v-model="query" @input="onSearchInput" @keydown.stop
|
||||
>
|
||||
</div>
|
||||
<div class="store-content">
|
||||
<!-- 发现 -->
|
||||
<template v-if="tab === 'discover'">
|
||||
<h2 class="store-h">编辑推荐</h2>
|
||||
<div class="store-hero">
|
||||
<div>
|
||||
<div class="store-hero-tag">今日 App</div>
|
||||
<div class="store-hero-name">熊掌记</div>
|
||||
<div class="store-hero-desc">记录灵感,优雅如斯。</div>
|
||||
<div class="store-card hero-get" style="background:transparent;border:none;padding:10px 0 0">
|
||||
<button
|
||||
class="btn store-get" :class="{ primary: !installed('bear') }"
|
||||
:disabled="installing === 'bear'"
|
||||
@click="installOrOpen('bear')"
|
||||
>{{ installing === 'bear' ? '安装中…' : installed('bear') ? '打开' : '获取' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<img src="/assets/icons/bear.png" alt="" class="store-icon" style="width:120px;height:120px">
|
||||
</div>
|
||||
<h2 class="store-h">热门 App</h2>
|
||||
<div class="store-grid">
|
||||
<div v-for="item in hotApps" :key="item.id" class="store-card">
|
||||
<img :src="item.icon" alt="" class="store-icon">
|
||||
<div class="store-info">
|
||||
<div class="store-name">{{ item.name }}</div>
|
||||
<div class="store-desc">{{ item.desc }}</div>
|
||||
<div class="store-cat">{{ item.cat }}</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn store-get" :class="{ primary: !installed(item.id) }"
|
||||
:disabled="installing === item.id"
|
||||
@click="installOrOpen(item.id)"
|
||||
>{{ installing === item.id ? '安装中…' : installed(item.id) ? '打开' : '获取' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 分类 -->
|
||||
<template v-if="tab === 'cats'">
|
||||
<template v-for="cat in categories" :key="cat">
|
||||
<h2 class="store-h">{{ cat }}</h2>
|
||||
<div class="store-grid">
|
||||
<div v-for="item in catApps(cat)" :key="item.id" class="store-card">
|
||||
<img :src="item.icon" alt="" class="store-icon">
|
||||
<div class="store-info">
|
||||
<div class="store-name">{{ item.name }}</div>
|
||||
<div class="store-desc">{{ item.desc }}</div>
|
||||
<div class="store-cat">{{ item.cat }}</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn store-get" :class="{ primary: !installed(item.id) }"
|
||||
:disabled="installing === item.id"
|
||||
@click="installOrOpen(item.id)"
|
||||
>{{ installing === item.id ? '安装中…' : installed(item.id) ? '打开' : '获取' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<template v-if="tab === 'arcade'">
|
||||
<h2 class="store-h">搜索 App</h2>
|
||||
<div v-if="!searchResults.length" class="empty-state" style="height:120px">没有找到相关 App</div>
|
||||
<div class="store-grid">
|
||||
<div v-for="item in searchResults" :key="item.id" class="store-card">
|
||||
<img :src="item.icon" alt="" class="store-icon">
|
||||
<div class="store-info">
|
||||
<div class="store-name">{{ item.name }}</div>
|
||||
<div class="store-desc">{{ item.desc }}</div>
|
||||
<div class="store-cat">{{ item.cat }}</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn store-get" :class="{ primary: !installed(item.id) }"
|
||||
:disabled="installing === item.id"
|
||||
@click="installOrOpen(item.id)"
|
||||
>{{ installing === item.id ? '安装中…' : installed(item.id) ? '打开' : '获取' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 更新 -->
|
||||
<template v-if="tab === 'updates'">
|
||||
<template v-if="!installedApps.length">
|
||||
<div class="empty-state" style="height:200px">
|
||||
<div class="es-icon">✅</div>
|
||||
<div>全部 App 均为最新版本</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h2 class="store-h">已安装</h2>
|
||||
<div class="store-grid">
|
||||
<div v-for="item in installedApps" :key="item.id" class="store-card">
|
||||
<img :src="item.icon" alt="" class="store-icon">
|
||||
<div class="store-info">
|
||||
<div class="store-name">{{ item.name }}</div>
|
||||
<div class="store-desc">{{ item.desc }}</div>
|
||||
<div class="store-cat">{{ item.cat }}</div>
|
||||
</div>
|
||||
<button class="btn store-get" @click="installOrOpen(item.id)">打开</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="set-note">所有已安装 App 均为最新版本。</p>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { store } from '../../composables/useStore'
|
||||
import { Apps } from '../../composables/useApps'
|
||||
import { Notify } from '../../composables/useNotify'
|
||||
|
||||
const STORE_CATALOG = [
|
||||
{ id: 'bear', name: '熊掌记', cat: '效率', desc: '优雅的 Markdown 笔记', icon: '/assets/icons/bear.png' },
|
||||
{ id: 'typora', name: 'Typora', cat: '效率', desc: '所见即所得 Markdown 编辑器', icon: '/assets/icons/typora.png' },
|
||||
{ id: 'vscode', name: 'VS Code Web', cat: '开发', desc: '轻量代码编辑器', icon: '/assets/icons/vscode.png' },
|
||||
{ id: 'keynote', name: 'Keynote 讲演', cat: '效率', desc: '制作精美演示文稿', icon: '/assets/icons/keynote.png' },
|
||||
{ id: 'podcasts', name: '播客', cat: '娱乐', desc: '收听你喜爱的节目', icon: '/assets/icons/podcasts.png' },
|
||||
{ id: 'news', name: 'Apple News', cat: '阅读', desc: '精选科技资讯', icon: '/assets/icons/news.png' },
|
||||
{ id: 'github', name: 'GitHub Desktop', cat: '开发', desc: '浏览与管理代码仓库', icon: '/assets/icons/github.png' },
|
||||
{ id: 'tv', name: 'Apple TV', cat: '娱乐', desc: '观看影片与节目', icon: '/assets/icons/tv.png' },
|
||||
]
|
||||
|
||||
const tabs: [string, string][] = [['discover', '发现'], ['cats', '分类'], ['arcade', '搜索'], ['updates', '更新']]
|
||||
|
||||
const tab = ref('discover')
|
||||
const query = ref('')
|
||||
const queryDebounced = ref('')
|
||||
const installing = ref<string | null>(null)
|
||||
let debounceTimer: any = null
|
||||
|
||||
function installedList(): string[] { return store.get('appstore-installed', []) }
|
||||
function installed(id: string): boolean { return installedList().includes(id) }
|
||||
function setInstalled(id: string) {
|
||||
const l = installedList()
|
||||
if (!l.includes(id)) { l.push(id); store.set('appstore-installed', l) }
|
||||
}
|
||||
|
||||
function onSearchInput() {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
queryDebounced.value = query.value.trim()
|
||||
if (queryDebounced.value) tab.value = 'arcade'
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const hotApps = computed(() => STORE_CATALOG.slice(1))
|
||||
const categories = computed(() => [...new Set(STORE_CATALOG.map(i => i.cat))])
|
||||
const catApps = (cat: string) => STORE_CATALOG.filter(i => i.cat === cat)
|
||||
const searchResults = computed(() => {
|
||||
const q = queryDebounced.value.toLowerCase()
|
||||
if (!q) return []
|
||||
return STORE_CATALOG.filter(i => (i.name + i.desc + i.cat).toLowerCase().includes(q))
|
||||
})
|
||||
const installedApps = computed(() => STORE_CATALOG.filter(i => installed(i.id)))
|
||||
|
||||
function go(t: string) { tab.value = t }
|
||||
|
||||
function installOrOpen(id: string) {
|
||||
const item = STORE_CATALOG.find(i => i.id === id)
|
||||
if (!item) return
|
||||
if (installed(id)) { Apps.open(id); return }
|
||||
installing.value = id
|
||||
setTimeout(() => {
|
||||
setInstalled(id)
|
||||
installing.value = null
|
||||
Notify.send({ appId: 'appstore', title: 'App Store', body: `"${item.name}"已安装,可在启动台与应用程序文件夹中找到。` })
|
||||
}, 1200 + Math.random() * 1200)
|
||||
}
|
||||
|
||||
defineExpose({ go, tab })
|
||||
</script>
|
||||
|
||||
@ -1,34 +1,16 @@
|
||||
// App Store 应用 — Vue 组件化
|
||||
import { h, render } from 'vue'
|
||||
// App Store 应用 — 从 js/apps3.js 手动转写
|
||||
import { $, $$, el, iconImg, debounce } from '../../utils'
|
||||
import { bus as Bus } from '../../composables/useBus'
|
||||
import { store as Store } from '../../composables/useStore'
|
||||
import { fs as FS, setAppStoreRefForFS } from '../../composables/useFS'
|
||||
import { Apps, stdMenus } from '../../composables/useApps'
|
||||
import { store } from '../../composables/useStore'
|
||||
import { FS, setAppStoreRefForFS } from '../../composables/useFS'
|
||||
import AppStoreComponent from './AppStore.vue'
|
||||
import { Notify } from '../../composables/useNotify'
|
||||
import { setAppStoreForLaunchpad } from '../launchpad/index'
|
||||
|
||||
const STORE_CATALOG = [
|
||||
{ id: 'bear', name: '熊掌记', cat: '效率', desc: '优雅的 Markdown 笔记', icon: '/assets/icons/bear.png' },
|
||||
{ id: 'typora', name: 'Typora', cat: '效率', desc: '所见即所得 Markdown 编辑器', icon: '/assets/icons/typora.png' },
|
||||
{ id: 'vscode', name: 'VS Code Web', cat: '开发', desc: '轻量代码编辑器', icon: '/assets/icons/vscode.png' },
|
||||
{ id: 'keynote', name: 'Keynote 讲演', cat: '效率', desc: '制作精美演示文稿', icon: '/assets/icons/keynote.png' },
|
||||
{ id: 'podcasts', name: '播客', cat: '娱乐', desc: '收听你喜爱的节目', icon: '/assets/icons/podcasts.png' },
|
||||
{ id: 'news', name: 'Apple News', cat: '阅读', desc: '精选科技资讯', icon: '/assets/icons/news.png' },
|
||||
{ id: 'github', name: 'GitHub Desktop', cat: '开发', desc: '浏览与管理代码仓库', icon: '/assets/icons/github.png' },
|
||||
{ id: 'tv', name: 'Apple TV', cat: '娱乐', desc: '观看影片与节目', icon: '/assets/icons/tv.png' },
|
||||
]
|
||||
|
||||
export const AppStoreApp = {
|
||||
id: 'appstore', name: 'App Store', icon: '/assets/icons/appstore.png',
|
||||
w: 860, h: 600, minW: 600, minH: 420,
|
||||
installed() { return Store.get('appstore-installed', []) },
|
||||
isInstalled(id: string) { return this.installed().includes(id) },
|
||||
setInstalled(id: string) {
|
||||
const l = this.installed()
|
||||
if (!l.includes(id)) { l.push(id); Store.set('appstore-installed', l); FS.syncApps(); Bus.emit('appstore:changed') }
|
||||
},
|
||||
installed(): string[] { return store.get('appstore-installed', []) },
|
||||
isInstalled(id: string): boolean { return this.installed().includes(id) },
|
||||
menus(win: any) {
|
||||
return stdMenus(this, {
|
||||
store: [
|
||||
@ -40,62 +22,11 @@ export const AppStoreApp = {
|
||||
},
|
||||
onArgs(args: any, win: any) { if (args.tab) win.appState?.go(args.tab) },
|
||||
render(win: any) {
|
||||
const st = win.appState = { tab: 'discover', q: '' }
|
||||
win.body.classList.add('store-body')
|
||||
const tabs: [string, string][] = [['discover', '发现'], ['cats', '分类'], ['arcade', '搜索'], ['updates', '更新']]
|
||||
const tabbar = el('div', { class: 'store-tabs' })
|
||||
const search = el('input', { class: 'text-input store-search', type: 'search', placeholder: '搜索 App' }) as HTMLInputElement
|
||||
const content = el('div', { class: 'store-content' })
|
||||
win.body.append(el('div', { class: 'store-top' }, tabbar, search), content)
|
||||
const btnFor = (item: any) => {
|
||||
const installed = this.isInstalled(item.id)
|
||||
const b = el('button', { class: 'btn store-get' + (installed ? '' : ' primary'), text: installed ? '打开' : '获取' }) as HTMLButtonElement
|
||||
b.addEventListener('click', () => {
|
||||
if (this.isInstalled(item.id)) { Apps.open(item.id); return }
|
||||
b.disabled = true; b.textContent = '安装中…'; b.classList.add('installing')
|
||||
setTimeout(() => {
|
||||
this.setInstalled(item.id); b.textContent = '打开'; b.disabled = false; b.classList.remove('installing', 'primary')
|
||||
Notify.send({ appId: 'appstore', title: 'App Store', body: `"${item.name}"已安装,可在启动台与应用程序文件夹中找到。` })
|
||||
}, 1200 + Math.random() * 1200)
|
||||
})
|
||||
return b
|
||||
}
|
||||
const cardFor = (item: any, big?: boolean) => {
|
||||
return el('div', { class: 'store-card' + (big ? ' big' : '') }, iconImg(item.icon, '', 'store-icon'),
|
||||
el('div', { class: 'store-info' }, el('div', { class: 'store-name', text: item.name }), el('div', { class: 'store-desc', text: item.desc }), el('div', { class: 'store-cat', text: item.cat })), btnFor(item))
|
||||
}
|
||||
st.go = (tab: string) => {
|
||||
st.tab = tab; $$('.store-tab', tabbar).forEach((n: HTMLElement) => n.classList.toggle('on', n.dataset.tab === tab))
|
||||
content.innerHTML = ''
|
||||
if (tab === 'discover') {
|
||||
content.append(el('h2', { class: 'store-h', text: '编辑推荐' }))
|
||||
const hero = el('div', { class: 'store-hero' }, el('div', null, el('div', { class: 'store-hero-tag', text: '今日 App' }), el('div', { class: 'store-hero-name', text: '熊掌记' }), el('div', { class: 'store-hero-desc', text: '记录灵感,优雅如斯。' }), el('div', { class: 'store-card hero-get', style: { background: 'transparent', border: 'none', padding: '10px 0 0' } }, btnFor(STORE_CATALOG[0]))), iconImg('/assets/icons/bear.png', ''))
|
||||
content.append(hero, el('h2', { class: 'store-h', text: '热门 App' }))
|
||||
const grid = el('div', { class: 'store-grid' }); STORE_CATALOG.slice(1).forEach((i: any) => grid.append(cardFor(i))); content.append(grid)
|
||||
} else if (tab === 'cats') {
|
||||
const cats = [...new Set(STORE_CATALOG.map((i: any) => i.cat))]
|
||||
cats.forEach((cat: string) => { content.append(el('h2', { class: 'store-h', text: cat })); const grid = el('div', { class: 'store-grid' }); STORE_CATALOG.filter((i: any) => i.cat === cat).forEach((i: any) => grid.append(cardFor(i))); content.append(grid) })
|
||||
} else if (tab === 'arcade') {
|
||||
content.append(el('h2', { class: 'store-h', text: '搜索 App' }))
|
||||
const grid = el('div', { class: 'store-grid' }); const hits = STORE_CATALOG.filter((i: any) => !st.q || (i.name + i.desc + i.cat).toLowerCase().includes(st.q.toLowerCase()))
|
||||
if (!hits.length) content.append(el('div', { class: 'empty-state', style: { height: '120px' }, text: '没有找到相关 App' }))
|
||||
hits.forEach((i: any) => grid.append(cardFor(i))); content.append(grid)
|
||||
} else if (tab === 'updates') {
|
||||
const inst = STORE_CATALOG.filter((i: any) => this.isInstalled(i.id))
|
||||
if (!inst.length) content.append(el('div', { class: 'empty-state', style: { height: '200px' } }, el('div', { class: 'es-icon', text: '✅' }), el('div', { text: '全部 App 均为最新版本' })))
|
||||
else { content.append(el('h2', { class: 'store-h', text: '已安装' })); const grid = el('div', { class: 'store-grid' }); inst.forEach((i: any) => grid.append(cardFor(i))); content.append(grid, el('p', { class: 'set-note', text: '所有已安装 App 均为最新版本。' })) }
|
||||
}
|
||||
}
|
||||
tabs.forEach(([id, name]) => { const b = el('button', { class: 'store-tab', text: name, dataset: { tab: id } }); b.addEventListener('click', () => st.go(id)); tabbar.append(b) })
|
||||
search.addEventListener('input', debounce(() => { st.q = search.value.trim(); st.go('arcade') }, 200))
|
||||
search.addEventListener('keydown', (e: KeyboardEvent) => e.stopPropagation());
|
||||
(win as any)._unsub = Bus.on('appstore:changed', () => { if (document.body.contains(win.el)) st.go(st.tab) })
|
||||
win.onClose = () => (win as any)._unsub && (win as any)._unsub()
|
||||
st.go('discover')
|
||||
}
|
||||
const vnode = h(AppStoreComponent, { win })
|
||||
render(vnode, win.body)
|
||||
},
|
||||
}
|
||||
Apps.register(AppStoreApp);
|
||||
(window as any).AppStoreApp = AppStoreApp
|
||||
// 注入引用到 FS(syncApps 需要)和 Launchpad(过滤需要)
|
||||
Apps.register(AppStoreApp)
|
||||
;(window as any).AppStoreApp = AppStoreApp
|
||||
setAppStoreRefForFS(AppStoreApp)
|
||||
setAppStoreForLaunchpad(AppStoreApp)
|
||||
|
||||
225
tests/cases/33-appstore.test.ts
Normal file
225
tests/cases/33-appstore.test.ts
Normal file
@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user