/** * 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 = '