如何在Cypress中Mock NextJS的useSearchParams钩子?
在Cypress组件测试中Mock NextJS
useSearchParams 钩子的方案 我开发了一个依赖NextJS useSearchParams 钩子的Analytics组件,编写Cypress组件测试时,无法Mock该钩子导致测试报错。这个场景类似React Testing Library中Mock useSearchParams 的情况,但我没有使用React Router,现在需要找到在Cypress中Mock该钩子的可行方案。
组件代码
import { costCalculationApiUrl, solarAnalyticsApiHeader } from "@/constants/urls" import { fetcher } from "@/utils/fetcher" import { useSearchParams } from "next/navigation" import { useEffect } from "react" import { useDispatch, useSelector } from "react-redux" import useSWRMutation from "swr/mutation" import { RootState } from "@/redux/store" import { CardDescription, CardTitle } from "@/components/ui/card" import { Separator } from "@/components/ui/separator" import { Skeleton } from "@/components/ui/skeleton" import { CustomInstallationReportPreview } from "../../export-report/custom/components" import { Invoice } from "../../offer/components" import { Roof, updatePanelPlacementData } from "../../panel-installation/cutsom/state/slice" import { format2APIDate } from "../helpers" import { updateFinancialInsightsData } from "../state/slice" import { AnalyticsForm } from "./form" import { InvestementCard } from "./investement-card" import { AnalyticsStats } from "./stats" export const Analytics = () => { const searchParams = useSearchParams() const dispatch = useDispatch() const nominalPower = searchParams.get("wp") const selectedRoofs: Roof[] | undefined = useSelector((state: RootState) => state.customInstallation.selectedRoofs) const { feedInPrice, currentPrice, installationDate, maintanance, insurance, interest, creditPeriod, outsideCapital, inflation, } = useSelector((state: RootState) => state.financialInsights.formData) const { data: customInstallationData } = useSelector((state: RootState) => state.customInstallation) const url = `${costCalculationApiUrl}?feed_in_price=${feedInPrice}&used_price=${currentPrice}&installation_date=${format2APIDate(installationDate)}&maintenance_cost_kwp=${maintanance}&insurance_cost_kwp=${insurance}&interest_rate=${interest}&credit_period=${creditPeriod}&wp=${nominalPower}&outside_capital=${outsideCapital}&inflation=${inflation}&additional_grid_feed_in_annual=${customInstallationData?.additionalGridFeedAnnual}&additional_solar_consumption_annual_abs=${customInstallationData?.additionalSolarConsumptionAnnual}&additional_panels=${customInstallationData?.additionalPanels}` const { data, error, trigger, reset } = useSWRMutation(url, (url) => fetcher(url, solarAnalyticsApiHeader)) useEffect(() => { if (customInstallationData) { reset() trigger().then((data) => { if (data) { const amortizationTime = data["Amortization Time"] const amortizationDate = data["Amortization Date"] const financialSaving = data["Accumulated Discounted period Cashflows after 20 years"] const investment = data["Investment"] const timeSeries = data["Time-Series"] // Update the state for the report // Though we pass data as prop to the components but the report have to access the state to display the data dispatch( updateFinancialInsightsData({ amortizationDate, amortizationTime, financialSaving, investment, timeSeries, }) ) } }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [nominalPower, customInstallationData]) // This effect is for the report // When the user access directly to the Analytics tab, // The panel placement is empty by default and it should be filled by selected roofs // The selected roofs is filled by roofs from the API after the first render of panel installation page useEffect(() => { dispatch(updatePanelPlacementData(selectedRoofs)) // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedRoofs]) if (error) { // Message: Something went wrong return <p>Etwas ist schief gelaufen</p> } if (!data) { return ( <div className='flex flex-col justify-between gap-8 h-full px-8'> <Skeleton className='h-24 w-full' /> <div className='flex w-full gap-8'> <Skeleton className='h-96 w-1/4' /> <div className='flex flex-col w-3/4 h-fit gap-8'> <Skeleton className='w-full h-24' /> <Skeleton className='w-full h-48' /> <Skeleton className='w-full h-96' /> </div> </div> </div> ) } else { return ( <div className='flex flex-col px-8'> <CardTitle>Wirtschaftlichkeitsanalyse</CardTitle> <CardDescription className='mt-1 mb-0 text-base'> Prüfen Sie Ihre Annahmen und spielen Sie Szenarien durch. </CardDescription> <div className='flex items-center gap-2 absolute right-6'> <Invoice /> <CustomInstallationReportPreview /> </div> <Separator className='my-4' /> <div className='flex gap-16 mt-4 w-full items-stretch h-full overflow-hidden'> <div className='flex gap-8 w-full'> <div className='max-w-[480px] w-full'> <AnalyticsForm /> </div> <div className='flex flex-col flex-grow content-stretch w-full gap-8 overflow-auto'> <InvestementCard data={data} /> <Separator /> <AnalyticsStats data={data} /> </div> </div> </div> </div> ) } }
测试代码
import { costCalculationApiUrl } from "@/constants/urls" import { jest } from "@jest/globals" import { NextRouterMock } from "cypress/mocks/NextRouterMock" import { GlobalLayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime" import { Provider, useSelector } from "react-redux" import { RootState, store } from "@/redux/store" import { Analytics } from "../../components" import { format2APIDate } from "../../helpers" describe("", () => { it("should load data successfully without encountering a 422 error", () => { // Mount your component cy.mount( <Provider store={store}> <Analytics /> </Provider> ) }) })
报错截图

解决方案
1. 直接Mock useSearchParams 导出
在测试用例中直接替换next/navigation模块的useSearchParams方法,返回预设的参数对象:
// 在测试用例执行前添加Mock cy.stub(require('next/navigation'), 'useSearchParams').returns(new URLSearchParams('wp=5000'))
2. 通过Next.js上下文提供Mock数据
useSearchParams本质是从Next.js的GlobalLayoutRouterContext中获取数据,手动提供这个上下文并传入Mock的Router对象,更贴近真实运行环境:
import { GlobalLayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime" describe("Analytics组件测试", () => { it("应成功加载数据且无422错误", () => { // 构建包含组件所需参数的searchParams const mockSearchParams = new URLSearchParams('wp=5000') // 提供完整的Router上下文,避免其他导航钩子报错 cy.mount( <Provider store={store}> <GlobalLayoutRouterContext.Provider value={{ searchParams: mockSearchParams, push: cy.stub(), replace: cy.stub(), refresh: cy.stub(), prefetch: cy.stub(), }}> <Analytics /> </GlobalLayoutRouterContext.Provider> </Provider> ) // 验证组件未报错 cy.get('p').should('not.contain', 'Etwas ist schief gelaufen') }) })
3. 补充Mock API请求(可选)
如果组件依赖SWR发起API请求,需要拦截请求并返回Mock数据,避免真实网络请求干扰测试:
// 在组件mount前添加请求拦截 cy.intercept('GET', costCalculationApiUrl, { statusCode: 200, body: { "Amortization Time": 10, "Amortization Date": "2034-01-01", "Accumulated Discounted period Cashflows after 20 years": 50000, "Investment": 30000, "Time-Series": [] } }).as('getAnalyticsData')
内容的提问来源于stack exchange,提问作者Ala Eddine Menai
相关产品推荐
相关产品推荐

