import { mount, type VueWrapper } from '@vue/test-utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { PublicAd } from '../api/client'; import { createViewerI18n } from '../i18n'; import AdCarousel from '../components/ads/AdCarousel.vue'; // 广告轮播组件(Task 10 Step 3):圆点跳页、关闭 emit、slide 点击新标签打开、 // 4s 自动轮播与指针悬停暂停、单条静态(无圆点)。会话级关闭记忆在 ActivityPage //(组件只 emit close,无 sessionStorage 副作用)。 function makeAd(id: string): PublicAd { return { adId: id, imageUrl: `https://ad.test/${id}.jpg`, linkUrl: `https://link.test/${id}` }; } const ADS = [makeAd('a'), makeAd('b'), makeAd('c')]; const mounted: VueWrapper[] = []; /** window.open 桩(记录调用即可):经工厂推导 spy 类型,绕开 spyOn 泛型约束。 */ function spyWindowOpen() { return vi.spyOn(window, 'open').mockReturnValue({} as Window); } function mountCarousel(ads: PublicAd[] = ADS): VueWrapper { const wrapper = mount(AdCarousel, { props: { ads }, global: { plugins: [createViewerI18n('zh-CN')] }, }); mounted.push(wrapper); return wrapper; } function currentSrc(wrapper: VueWrapper): string | null { const img = wrapper.find('.ad-slide img').element as HTMLImageElement | undefined; return img?.getAttribute('src') ?? null; } describe('AdCarousel', () => { let openSpy: ReturnType; beforeEach(() => { vi.useFakeTimers(); openSpy = spyWindowOpen(); }); afterEach(() => { openSpy.mockRestore(); while (mounted.length > 0) mounted.pop()?.unmount(); vi.useRealTimers(); }); it('renders the ad badge and switches to the clicked dot', async () => { const wrapper = mountCarousel(); expect(wrapper.get('.ad-badge').text()).toBe('广告'); expect(currentSrc(wrapper)).toBe('https://ad.test/a.jpg'); const dots = wrapper.findAll('.ad-dots button'); expect(dots.length).toBe(3); expect(dots[0].attributes('aria-current')).toBe('true'); // 当前项程序化状态 expect(dots[1].attributes('aria-current')).toBeUndefined(); await dots[1].trigger('click'); expect(currentSrc(wrapper)).toBe('https://ad.test/b.jpg'); expect(dots[1].classes()).toContain('active'); expect(dots[1].attributes('aria-current')).toBe('true'); expect(dots[0].attributes('aria-current')).toBeUndefined(); }); it('emits close when the ✕ button is clicked', async () => { const wrapper = mountCarousel(); const close = wrapper.get('.ad-close'); expect(close.attributes('aria-label')).toBe('关闭广告'); await close.trigger('click'); expect(wrapper.emitted('close')?.length).toBe(1); }); it('opens the current ad link in a new tab via window.open', async () => { const wrapper = mountCarousel(); await wrapper.get('.ad-slide').trigger('click'); expect(openSpy).toHaveBeenCalledWith('https://link.test/a', '_blank', 'noopener,noreferrer'); }); it('auto-advances every 4s, pauses on pointer enter and resumes on leave', async () => { const wrapper = mountCarousel(); expect(currentSrc(wrapper)).toBe('https://ad.test/a.jpg'); await vi.advanceTimersByTimeAsync(4000); expect(currentSrc(wrapper)).toBe('https://ad.test/b.jpg'); await wrapper.find('.ad-carousel').trigger('pointerenter'); await vi.advanceTimersByTimeAsync(8000); expect(currentSrc(wrapper)).toBe('https://ad.test/b.jpg'); // 悬停期间不切换 await wrapper.find('.ad-carousel').trigger('pointerleave'); await vi.advanceTimersByTimeAsync(4000); expect(currentSrc(wrapper)).toBe('https://ad.test/c.jpg'); // 离开自动恢复 }); it('switches by horizontal swipe (touch) and cancels the synthetic click', async () => { const wrapper = mountCarousel(); const root = wrapper.find('.ad-carousel'); await root.trigger('touchstart', { touches: [{ clientX: 200, clientY: 20 }] }); await root.trigger('touchend', { changedTouches: [{ clientX: 80, clientY: 25 }] }); expect(currentSrc(wrapper)).toBe('https://ad.test/b.jpg'); // 左滑 → 下一张 expect(openSpy).not.toHaveBeenCalled(); // 滑动不触发广告打开 await root.trigger('touchstart', { touches: [{ clientX: 100, clientY: 10 }] }); await root.trigger('touchend', { changedTouches: [{ clientX: 190, clientY: 12 }] }); expect(currentSrc(wrapper)).toBe('https://ad.test/a.jpg'); // 右滑 → 上一张 }); it('renders a single ad statically without dots or auto rotation', async () => { const wrapper = mountCarousel([makeAd('solo')]); expect(wrapper.find('.ad-dots').exists()).toBe(false); await vi.advanceTimersByTimeAsync(12000); expect(currentSrc(wrapper)).toBe('https://ad.test/solo.jpg'); }); });