/** * 23-photos: Photos Vue 组件化测试 * * 覆盖: 挂载、照片网格、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: {} } } describe('23-photos — Photos Vue 组件化', () => { 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('挂载后渲染照片网格', () => { mount() expect(win.body.querySelector('.photos-grid')).toBeTruthy() expect(win.body.querySelectorAll('.photo-cell').length).toBeGreaterThan(0) }) it('侧边栏包含图库选项', () => { mount() const items = win.body.querySelectorAll('.fb-side-item') expect(items.length).toBeGreaterThanOrEqual(3) }) it('每张照片有名称', () => { mount() const names = [...win.body.querySelectorAll('.photo-name')].map(e => e.textContent) expect(names.length).toBeGreaterThan(0) expect(names.every(n => !!n)).toBe(true) }) }) // ==================== Lightbox ==================== describe('Lightbox', () => { it('点击照片打开 lightbox', async () => { mount() const cell = win.body.querySelector('.photo-cell') as HTMLElement cell.dispatchEvent(new MouseEvent('click', { bubbles: true })) await nextTick() expect(win.body.querySelector('.lightbox')).toBeTruthy() }) it('lightbox 显示计数器', async () => { mount() const cell = win.body.querySelector('.photo-cell') as HTMLElement cell.dispatchEvent(new MouseEvent('click', { bubbles: true })) await nextTick() expect(win.body.querySelector('.lb-counter')?.textContent).toContain('/') }) it('关闭按钮关闭 lightbox', async () => { mount(); (win.body.querySelector('.photo-cell') as HTMLElement) .dispatchEvent(new MouseEvent('click', { bubbles: true })) await nextTick() const closeBtn = win.body.querySelector('.lb-close') as HTMLElement closeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true })) await nextTick() expect(win.body.querySelector('.lightbox')).toBeNull() }) it('Escape 关闭 lightbox', async () => { mount(); (win.body.querySelector('.photo-cell') as HTMLElement) .dispatchEvent(new MouseEvent('click', { bubbles: true })) await nextTick() const lb = win.body.querySelector('.lightbox') as HTMLElement lb.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })) await nextTick() expect(win.body.querySelector('.lightbox')).toBeNull() }) it('ArrowRight 切换到下一张', async () => { mount(); (win.body.querySelector('.photo-cell') as HTMLElement) .dispatchEvent(new MouseEvent('click', { bubbles: true })) await nextTick() const counter = win.body.querySelector('.lb-counter')?.textContent || '' const m = counter.match(/(\d+) \/ (\d+)/) if (m && +m[1] < +m[2]) { const lb = win.body.querySelector('.lightbox') as HTMLElement lb.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })) await nextTick() const newCounter = win.body.querySelector('.lb-counter')?.textContent expect(newCounter).not.toBe(counter) } }) }) // ==================== 卸载 ==================== describe('卸载', () => { it('卸载后 DOM 为空', () => { mount(); render(null, win.body) expect(win.body.children.length).toBe(0) }) }) })