如何在Vitest中测试加载屏?含数据与图片加载场景
解决Vitest中测试加载屏(数据+图片加载完成后隐藏)的问题
原测试的核心问题
- 错误的定时器推进时机:在
waitFor内部调用vi.advanceTimersByTime会打乱轮询逻辑,导致Promise无法正确resolve。 - 未Mock图片加载事件:组件需要等待所有图片加载完成才隐藏加载屏,但测试环境默认不会处理真实图片的异步加载,导致逻辑卡死。
修正后的测试方案
第一步:Mock全局Image对象(模拟图片加载)
测试环境中需要模拟图片的load事件,让组件认为图片已加载完成。在测试前添加全局Mock:
beforeEach(() => { // 模拟图片加载,设置src后立即触发onload global.Image = vi.fn(() => ({ src: '', onload: null, onerror: null, set src(_) { // 用微任务触发onload,模拟真实异步加载逻辑 queueMicrotask(() => this.onload?.()); } })); });
如果需要测试图片延迟加载的场景,可以把queueMicrotask换成带定时器的逻辑:
set src(_) { setTimeout(() => this.onload?.(), 200); }
第二步:正确编写测试用例
import { render, screen, waitFor, flushPromises } from '@testing-library/react'; import App from './App'; import { fetchData } from '../../ShoppingCart-Core/api.js'; import { products } from './mock-products.js'; vi.mock('../../ShoppingCart-Core/api.js', () => ({ fetchData: vi.fn(() => new Promise(resolve => setTimeout(() => resolve(products), 500)) ) })); it('加载屏在数据和图片都加载完成后隐藏', async () => { vi.useFakeTimers(); render(<App />); // 验证初始状态:加载屏显示 expect(screen.getByText('加载中...')).toBeInTheDocument(); // 推进定时器,让fetchData完成 vi.advanceTimersByTime(500); // 刷新Promise,触发setProducts后的组件重渲染 await flushPromises(); // (可选)验证数据已渲染 expect(screen.getAllByRole('img')).toHaveLength(products.length); // 等待图片加载完成的状态更新,验证加载屏隐藏 await waitFor(() => { expect(screen.queryByText('加载中...')).not.toBeInTheDocument(); }); vi.useRealTimers(); });
第三步:组件内补充图片加载监听逻辑(如果还没实现)
要让测试逻辑成立,组件内需要监听图片加载状态,示例逻辑如下:
const [products, setProducts] = useState([]); const [imagesLoaded, setImagesLoaded] = useState(false); useEffect(() => { const getProducts = async () => { const res = await fetchData(); setProducts(res); }; getProducts(); }, []); useEffect(() => { if (products.length === 0) return; let loadedCount = 0; const totalImages = products.length; const markImageLoaded = () => { loadedCount++; if (loadedCount === totalImages) setImagesLoaded(true); }; // 为每个图片绑定加载事件 products.forEach(item => { const img = new Image(); img.src = item.imageUrl; img.onload = markImageLoaded; img.onerror = markImageLoaded; // 处理加载失败,避免加载屏一直显示 }); // 清理函数 return () => { products.forEach(item => { const img = new Image(); img.onload = null; img.onerror = null; }); }; }, [products]); // 加载屏显示条件:数据未加载 或 图片未加载完成 const isLoading = products.length === 0 || !imagesLoaded;
关键说明
flushPromises的作用:推进定时器后,fetchData的Promise会resolve,但setProducts是异步状态更新,需要flushPromises等待组件完成重渲染。- 分阶段验证:先验证初始加载屏显示,再验证数据加载后的状态,最后验证图片加载完成后加载屏隐藏,确保每一步逻辑都符合预期。
内容的提问来源于stack exchange,提问作者vatsal chauhan
相关产品推荐
相关产品推荐

