I have a component that uses useSearchParams
hook from NextJS:
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>
)
}
}
I created a component testing but I could not figured out how to to mock the useSearchParams
hook:
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>
)
})
})
Cypress displays this issue:
Ths issue is similar to this but I'm not using React Router.
I solved the issue by wrapping the component with SearchPramsContext
and provide the URL params:
import { SearchParamsContext } from "next/dist/shared/lib/hooks-client-context.shared-runtime"
import { Provider } from "react-redux"
import { store } from "@/redux/store"
import { Analytics } from "../../components"
describe("", () => {
it("", () => {
const params = new URLSearchParams({ wp: "400" })
cy.mount(
<SearchParamsContext.Provider value={params}>
<Provider store={store}>
<Analytics />
</Provider>
</SearchParamsContext.Provider>
)
})
})