Vitest测试全通过却抛出未捕获异常求助(ApexCharts+React)
环境版本
- Vitest: 0.34.4
- apexcharts: ~3.41.0
- react-apexcharts: ~1.4.1
- React: ~18.2.0
- Ubuntu 22.04
问题描述
所有测试用例执行成功,但控制台仍抛出未捕获异常。尝试过dangerouslyIgnoreUnhandledErrors、组件unmount等方案,均无效。当前测试React项目中的ApexCharts组件,求能跳过该异常的解决思路。
错误日志
✓ src/react-ui/test/components/DeleteModal.test.tsx (4) ✓ src/react-ui/test/components/TableIdsSelectable.test.tsx (1) ✓ src/react-ui/test/components/TableRadio.test.tsx (1) ✓ src/react-ui/test/components/TableNotClickable.test.tsx (1) ✓ src/react-ui/test/components/TableCheckboxes.test.tsx (1) 425ms ✓ src/react-ui/test/components/Text.test.tsx (2) ✓ src/react-ui/test/components/Select.test.tsx (3) 477ms ✓ src/react-ui/test/components/Number.test.tsx (2) ✓ src/test/components/SubscriptionThickBarChart.test.tsx (1) Uncaught Exception TypeError: Cannot read properties of undefined (reading '0') ❯ t node_modules/apexcharts/dist/apexcharts.common.js:10:9056 ❯ Et.a.PathArray.morph node_modules/apexcharts/dist/apexcharts.common.js:10:12451 ❯ create.initAnimations node_modules/apexcharts/dist/apexcharts.common.js:6:422898 ❯ create.startCurrent node_modules/apexcharts/dist/apexcharts.common.js:6:422208 ❯ create.start node_modules/apexcharts/dist/apexcharts.common.js:6:422007 ❯ create.<anonymous> node_modules/apexcharts/dist/apexcharts.common.js:6:426560 ❯ listOnTimeout node:internal/timers:559:17 ❯ processTimers node:internal/timers:502:7 Test Files 9 passed (9) Tests 16 passed (16) Errors 1 error Start at 12:59:57 Duration 3.32s (transform 770ms, setup 853ms, collect 12.86s, tests 2.14s, environment 3.08s, prepare 1.28s)
测试代码
/* eslint-disable @typescript-eslint/no-empty-function */ import React from 'react'; import * as LocalQuery from '../../generated/RestUtilities2'; import * as LocalCurrentId from '../../utils/useCurrentId'; import { _MockSelector } from '../../react-ui/test/components/_MockRedux'; import { afterEach, beforeAll, describe, it, vi, expect } from 'vitest'; import ApexCharts from 'apexcharts'; import { render, waitFor } from '@testing-library/react'; import { BaseThickBarChart } from '../../components/charts/SubscriptionThickBarChart'; import '@testing-library/jest-dom'; vi.mock('apexcharts', async () => { const actual = (await vi.importActual('apexcharts')) as object; return { ...actual, exec: vi.fn(() => { return new Promise((resolve) => { resolve('uri'); }); }) }; }); Object.defineProperty(window, 'ResizeObserver', { writable: true, value: window.ResizeObserver || vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() })) }); Object.defineProperty(global.SVGElement.prototype, 'getScreenCTM', { writable: true, value: vi.fn(() => 10) }); Object.defineProperty(global.SVGElement.prototype, 'getBBox', { writable: true, value: vi.fn().mockReturnValue({ x: 10, y: 10 }) }); Object.defineProperty(global.SVGElement.prototype, 'getComputedTextLength', { writable: true, value: vi.fn().mockReturnValue(10) }); Object.defineProperty(global.SVGElement.prototype, 'createSVGMatrix', { writable: true, value: vi.fn().mockReturnValue({ x: 10, y: 10, inverse: () => 'what', multiply: () => 'hej' }) }); window.ResizeObserver = vi.fn().mockImplementation(() => ({ disconnect: vi.fn(), observe: vi.fn(), unobserve: vi.fn() })); ApexCharts.prototype.destroy = vi.fn(); describe('Table Range chart', () => { try { beforeAll(() => { global.ResizeObserver = class ResizeObserver { observe() {} unobserve() {} disconnect() {} }; }); afterEach(() => { vi.restoreAllMocks(); }); vi.mock('react-redux', () => ({ ...vi.importActual('react-redux'), useSelector: vi.fn(() => _MockSelector) })); vi.spyOn(LocalQuery, 'useQuery').mockReturnValue({ call: () => new Promise((resolve) => resolve({ status: 200, content: [ { name: 'plan1', partNumber: 'part-plan1', price: 1000.0, currency: 0, validFrom: '2024-09-27T22:21:27.782Z', validTo: '2024-10-27T22:21:27.782Z', status: 0, id: 'AgLD' } ] }) ), data: { status: 200 }, loading: false, refetch: () => new Promise((resolve) => resolve()) }); vi.spyOn(LocalCurrentId, 'useCurrentId').mockReturnValue(''); } catch (error) { expect(error).toBeInstanceOf(TypeError); expect(error).toHaveProperty('message', 'Cannot read properties of undefined'); } it('Checking SubscriptionThickBarChart', async () => { try { const { container, rerender, unmount } = render(<BaseThickBarChart buttons={[{ title: '30 days', value: 1 }]} />); await waitFor(() => rerender(<BaseThickBarChart buttons={[{ title: '30 days', value: 1 }]} />)); const chartSubscription = container?.querySelector('path[val=\"1730067687782\"]'); expect(chartSubscription).toBeInTheDocument(); unmount(); } catch (error) { expect(error).toBeInstanceOf(TypeError); console.log('error :>> ', error); // expect(error).toHaveProperty('message', 'Cannot read properties of undefined'); } }); });
解决思路
从错误栈来看,异常来自ApexCharts内部的动画初始化逻辑(initAnimations -> PathArray.morph),属于测试完成后异步触发的未捕获异常,以下是几种可行的处理方案:
1. 禁用ApexCharts动画
在测试时给图表组件传递animations: { enabled: false }的配置,从根源避免动画相关代码执行:
// 渲染组件时添加动画禁用配置 render(<BaseThickBarChart buttons={[{ title: '30 days', value: 1 }]} chartOptions={{ animations: { enabled: false } }} />);
如果BaseThickBarChart内部封装了配置,可在测试前全局覆盖ApexCharts默认配置:
beforeAll(() => { ApexCharts.defaultOptions = { ...ApexCharts.defaultOptions, animations: { enabled: false } }; });
2. 彻底Mock动画相关方法
当前Mock仅覆盖了exec和destroy,需补充Mock动画初始化逻辑,避免进入出错分支:
vi.mock('apexcharts', async () => { const actual = await vi.importActual('apexcharts'); return { ...actual, exec: vi.fn(() => Promise.resolve('uri')), initAnimations: vi.fn(), start: vi.fn() }; }); // 或直接Mock实例方法 ApexCharts.prototype.initAnimations = vi.fn(); ApexCharts.prototype.start = vi.fn();
3. 在Vitest中过滤特定异常
在测试文件顶部添加全局异常捕获,忽略目标异常:
beforeAll(() => { process.on('uncaughtException', (error) => { if (error.message.includes("Cannot read properties of undefined (reading '0')") && error.stack.includes('apexcharts.common.js')) { return; } throw error; }); });
也可在vitest.config.js中全局配置:
export default defineConfig({ test: { unhandledErrors: { ignore: [/Cannot read properties of undefined \(reading '0'\)/] } } });
4. 等待所有异步任务完成
启用假定时器,在测试末尾等待所有定时器执行完毕,确保动画异步代码处理完成:
beforeAll(() => { vi.useFakeTimers(); }); afterAll(() => { vi.useRealTimers(); }); it('Checking SubscriptionThickBarChart', async () => { const { container, rerender, unmount } = render(<BaseThickBarChart buttons={[{ title: '30 days', value: 1 }]} />); await waitFor(() => rerender(<BaseThickBarChart buttons={[{ title: '30 days', value: 1 }]} />)); const chartSubscription = container?.querySelector('path[val=\"1730067687782\"]'); expect(chartSubscription).toBeInTheDocument(); await vi.runAllTimers(); unmount(); });
内容的提问来源于stack exchange,提问作者x-magix
相关产品推荐
相关产品推荐

