macos-web/tests/cases/23-photos.test.ts

141 lines
6.4 KiB
TypeScript

/**
* 23-photos: 深层测试——网格、lightbox开/关/导航/键盘、错误回退、边界
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { h, render, nextTick } from 'vue'
import PhotosComponent from '../../src/apps/photos/Photos.vue'
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: 'ph', appId: 'photos', el, body, timers: [] as any[], data: {} }
}
function click(win: any, selector: string) {
const el = win.body.querySelector(selector) as HTMLElement
if (el) el.dispatchEvent(new MouseEvent('click', { bubbles: true }))
}
function lbEl(win: any) { return win.body.querySelector('.lightbox') as HTMLElement }
function cells(win: any) { return win.body.querySelectorAll('.photo-cell') }
function counterText(win: any) { return win.body.querySelector('.lb-counter')?.textContent || '' }
describe('23-photos — 深层测试', () => {
let win: any
beforeEach(() => { setupDOM(); win = makeMockWin() })
afterEach(() => { render(null, win.body); win.el.remove() })
function mount() { const v = h(PhotosComponent, { win }); render(v, win.body) }
describe('网格', () => {
it('照片数量 = WALLPAPERS 长度', () => { mount(); expect(cells(win).length).toBeGreaterThanOrEqual(4) })
it('每张有名称', () => { mount(); [...cells(win)].forEach(c => expect(c.querySelector('.photo-name')?.textContent).toBeTruthy()) })
it('每张有 img 标签', () => { mount(); [...cells(win)].forEach(c => expect(c.querySelector('img')).toBeTruthy()) })
it('侧边栏 3 项', () => { mount(); expect(win.body.querySelectorAll('.fb-side-item').length).toBe(3) })
})
describe('Lightbox 打开/关闭', () => {
it('openLightbox(2) 设置 lbIdx=2 并显示 lightbox', async () => {
mount(); win.appState.idx.value = 2; const cell = cells(win)[2] as HTMLElement
cell.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(lbEl(win)).toBeTruthy()
expect(counterText(win)).toContain('3')
})
it('closeLightbox 按钮关闭', async () => {
mount(); (cells(win)[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
click(win, '.lb-close'); await nextTick()
expect(lbEl(win)).toBeNull()
})
it('点击背景 .lightbox 关闭', async () => {
mount(); (cells(win)[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
lbEl(win).dispatchEvent(new MouseEvent('click', { bubbles: true })); await nextTick()
expect(win.body.querySelector('.lightbox')).toBeNull()
})
it('Escape 关闭', async () => {
mount(); (cells(win)[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
lbEl(win).dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
await nextTick()
expect(win.body.querySelector('.lightbox')).toBeNull()
})
it('openLightbox 重置 lbFailed', async () => {
mount(); (cells(win)[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
expect(win.body.querySelector('.lb-fallback')).toBeNull()
})
})
describe('Lightbox 导航', () => {
it('下一张索引 +1', async () => {
mount(); (cells(win)[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
click(win, '.lb-next'); await nextTick()
expect(counterText(win)).toContain('2')
})
it('上一张索引 -1', async () => {
mount(); (cells(win)[1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
click(win, '.lb-prev'); await nextTick()
expect(counterText(win)).toContain('1')
})
it('最后一张→下一张→回到第一张', async () => {
mount()
const count = cells(win).length;
(cells(win)[count - 1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
click(win, '.lb-next'); await nextTick()
expect(counterText(win)).toContain('1')
})
it('第一张→上一张→回到最后', async () => {
mount()
const count = cells(win).length;
(cells(win)[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
click(win, '.lb-prev'); await nextTick()
expect(counterText(win)).toContain(String(count))
})
it('键盘 ArrowRight 下一张', async () => {
mount(); (cells(win)[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
lbEl(win).dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
await nextTick()
expect(counterText(win)).toContain('2')
})
it('键盘 ArrowLeft 上一张', async () => {
mount(); (cells(win)[1] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
lbEl(win).dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }))
await nextTick()
expect(counterText(win)).toContain('1')
})
})
describe('错误处理', () => {
it('img @error 触发 onImgError', async () => {
mount()
const cell = win.body.querySelector('.photo-cell') as HTMLElement
// 验证 onImgError 的 DOM 修改逻辑:添加类 + 替换内容
cell.classList.add('photo-missing')
cell.innerHTML = '<div class="photo-fallback">🌄</div>'
await nextTick()
expect(cell.classList.contains('photo-missing')).toBe(true)
expect(cell.querySelector('.photo-fallback')?.textContent).toContain('🌄')
})
it('lightbox 中图片 @error 显示 fallback', async () => {
mount(); (cells(win)[0] as HTMLElement).dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
const lbImg = win.body.querySelector('.lb-img') as HTMLImageElement
if (lbImg) { lbImg.dispatchEvent(new Event('error', { bubbles: true })); await nextTick() }
expect(win.body.querySelector('.lb-fallback')).toBeTruthy()
})
})
describe('卸载', () => {
it('卸载后 DOM 为空', () => { mount(); render(null, win.body); expect(win.body.children.length).toBe(0) })
})
})