2

I'm trying to test my component in cypress. the component uses the useRouter hook from next/navigation in a button to push back to another page.

import { useRouter } from "next/navigation";
import { VStack, Heading, Text, Button, Box, HStack } from "@chakra-ui/react";

const ForgotPassword = () => {
  const [usersEmails, setUsersEmails] = useState<String[]>([]);
  const [email, setEmail] = useState("");
  const [error, setError] = useState(false);


  const router = useRouter()

  useEffect(() => {
    const fetchEmails = async () => {
      const { error, data } = await supabase.from("users").select("email");

      if (error) {
        console.log(error);
      } else {
        const emailArray = data.map((user) => user.email);
        setUsersEmails(emailArray);
      }
    };

    fetchEmails();
  }, [supabase]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

  };

  return (
    <HStack h="100vh">
    <Box>

    <VStack w="full" maxW="md">
      <Heading as="h3" size="lg" color="#006bb2" alignSelf="flex-start">
        Forgot Password
      </Heading>
      <Text mb="15px">
        Enter your email and we&#34;ll send a link to reset your password
      </Text>
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          id="email"
          name="email"
          placeholder="[email protected]"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
        {error && (
          <Text mt="5px" color="red">
            We cannot find your email
          </Text>
        )}

        <button>
          Submit
        </button>
      </form>

      <Button
        leftIcon={<ArrowBackIcon />}
        color="gray.500"
        _hover={{ color: "#006bb2", cursor: "pointer" }}
        variant="link"
        onClick={() => router.push("/login")}
      >
        Back to Login
      </Button>
    </VStack>
    </Box>
    </HStack>

  );
};

export default ForgotPassword;

when i mount the component, i get the error from cypress

(uncaught exception)Error: invariant expected app router to be mounted

The following error originated from your application code, not from Cypress. invariant expected app router to be mounted

but if i comment out the "import useRouter", "const router = useRouter()" line and remove the "router.push("/login")", then the component mounts and cypress doesnt throw any error

i've tried the next/router & next/link solution from cypress docs

import React from 'react'
import ForgotPassword from './page'
import Router from 'next/router'


describe('Testing the forgot password component', () => {
  context('stubbing out `useRouter` hook', () => {
    let router
    
    beforeEach(() => {
      router = {
        push: cy.stub().as('router:push')
      }

      cy.stub(Router, 'useRouter').returns(router)
    })

    it('checks if user email exits in db', () => {
      cy.mount(<ForgotPassword />)

      cy.get('input[name="email"]').type("[email protected]");

      // cy.get('button').click()

      // cy.get('@routerBack').should((mock) => {
      //   expect(mock).to.have.been.calledOnce
      // })
    })
  })

})

cypress still throws the same "(uncaught exception)Error: invariant expected app router to be mounted" error and the component still doesn't mount.

i changed the line import Router from 'next/router' to import Router from 'next/navigation' and cypress error changes to

Trying to stub property 'useRouter' of undefined

Because this error occurred during a before each hook we are skipping the remaining tests in the current suite: stubbing out useRouter hook

3 Answers 3

4

The uncaught exception Error: invariant expected app router to be mounted can be resolved by wrapping the component in <AppRouterContext.Provider value={router}>.

That's the major fix, but there's also some secondary ones:

  • The 'next/router' is used in the test, but "next/navigation" in the component. These should be the same router in both places.

  • To stub useRouter from the Router object, you need to name the default import
    with: import * as Router from "next/navigation"
    not : import Router from "next/navigation"

    This solves the error Trying to stub property 'useRouter' of undefined


  • the alias used in the calledOnce assertion should be the same name as used in the stub.

Here is my test for the NextJs sample component

component

'use client'
 
import { useRouter } from 'next/navigation'
 
export default function Page() {
  const router = useRouter()
 
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

test

import React from 'react'
import { AppRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import * as Router from "next/navigation";
import Page from './example-client-component.js'

it('stubs the router', () => {
  const router = {
    push: cy.stub().as('router:push')
  }
  cy.stub(Router, 'useRouter').returns(router)

  cy.mount(
    <AppRouterContext.Provider value={router}>
      <Page />
    </AppRouterContext.Provider>
  )

  cy.get('button').click()

  cy.get('@router:push').should((mock) => {
    expect(mock).to.have.been.calledOnce
  })
})

test log

enter image description here

Sign up to request clarification or add additional context in comments.

Comments

0

I've extended previous solution to support also usePathname and useSearchParams hooks from next/navigation.

Custom router mock wrapper:

import { AppRouterContext, AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { PathnameContext, SearchParamsContext } from "next/dist/shared/lib/hooks-client-context.shared-runtime";
import { ReactNode, useState } from "react";

export const NextNavigationRouterMock = ({
  children,
  router
}: {
  children: ReactNode;
  router: Partial<AppRouterInstance>;
}) => {
  const [searchParams, setSearchParams] = useState<URLSearchParams>(new URLSearchParams());
  const routerProxy = new Proxy(router, {
    get(target: Record<string, () => void>, prop: string) {
      if (["push", "replace"].includes(prop)) {
        return function () {
          const [paramsString] = [...arguments];
           
          // update search params values
          setSearchParams(new URLSearchParams(paramsString?.split("/")?.[1]));
          
          // invoke the original method
          target[prop].apply(null, [...arguments] as []);
        };
      }

      return target[prop];
    }
  });

  return (
    <AppRouterContext.Provider value={routerProxy}>
      <SearchParamsContext.Provider value={searchParams}>
        <PathnameContext.Provider value={"/"}>{children}</PathnameContext.Provider>
      </SearchParamsContext.Provider>
    </AppRouterContext.Provider>
  );
};

Component:

'use client'
 
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
 
export const Page = () => {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  const onUpdateParams = () => {
    searchParams.set('params', '1');
  }
 
  const onNavigate = () => {
    router. (`${pathname}?${searchParams.toString()}`);
  }
 
  return (
   <>
    <button type="button" onClick={onUpdateParams}>
      Update params
    </button>
    <button type="button" onClick={onNavigate}>
     Navigate to page
    </button>
   </>
  )
}

Cypress test:

import React from 'react'
import { NextNavigationRouterMock } from "@cy/mocks";
import Page from './page'

describe("Navigation", () => {
  beforeEach(() => {
   const routerStub = {
       replace: cy.stub().as("router:replace")
   };

   cy.mount(
       <NextNavigationRouterMock value={routerStub}>
          <Page />
        </NextNavigationRouterMock>
   );
  });

  it('updates URL', () => {
     cy.get('button').click()
     cy.get("@router:replace").should("have.been.calledWith", "/?params=1");
   });
});

NextJS: 15.1 and React: 19.0

Comments

0

Typescript was complaining for me about router not having all of the necessary methods:

Type '{ push: Omit<SinonStub<any[], any>, "withArgs"> & SinonSpyAgent<SinonStub<any[], any>> & SinonStub<any[], any>; }' 
is missing the following properties from type 'AppRouterInstance': 
back, forward, refresh, replace, prefetchts (2739)

So I used Partial to prevent typescript from expecting the missing methods:

import React from 'react';

import {
  AppRouterContext,
  AppRouterInstance,
} from 'next/dist/shared/lib/app-router-context.shared-runtime';
import * as Router from 'next/navigation';

describe('<FullscreenButton />', () => {
  it('renders', () => {
    const router = {
      push: cy.stub().as('router:push'),
    }  as Partial<AppRouterInstance>
    cy.stub(Router, 'useRouter').returns(router)
  
    cy.mount(
      <AppRouterContext.Provider value={router as AppRouterInstance}>
        <Page />
      </AppRouterContext.Provider>
    )
  })
})

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.