Terrabythia
Terrabythia

Reputation: 2161

Cypress and NextJS using React Server Actions

I'm trying to intercept the client side request that's done by React Server Components for my tests in Cypress.

I thought it should be as simple as looking at and copying the request response (including headers) in the network tab and copy that to Cypress' intercept function, but I keep getting the error Connection closed and the request result in my front end code logs undefined.

Any ideas?

My current intercept implementation in my Cypress test:

cy.intercept("POST", "/account/register", {
  statusCode: 200,
  headers: {
    "x-action-revalidated": "[[],0,0]",
    "content-type": "text/x-component",
  },
  body: `0:["$@1",["development",null]]\n1:{"result":"success","status":200,"data":{"id":"91ffe221-ace9-40a1-98d5-2851cb071cbd","name":"Test User","email":"[email protected]","createdAt":"$D2024-05-26T16:32:11.280Z","updatedAt":"$D2024-05-26T14:32:11.288Z"}}`,
}).as("registerRequest");

The error message in my console:

index-3fe21bb9.js:133640 Error: The following error originated from your application code, not from Cypress. It was caused by an unhandled promise rejection.

  > Connection closed.

When Cypress detects uncaught errors originating from your application it will automatically fail the current test.

This behavior is configurable, and you can choose to turn this off by listening to the `uncaught:exception` event.
    at close (webpack-internal:///app-pages-browser/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2131:31)
    at progress (webpack-internal:///app-pages-browser/./node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js:2148:7)
From previous event:
    at Promise.longStackTracesCaptureStackTrace [as _captureStackTrace] (http://localhost:3000/__cypress/runner/cypress_runner.js:3486:19)
    at Promise._then (http://localhost:3000/__cypress/runner/cypress_runner.js:1239:17)
    at Promise._passThrough (http://localhost:3000/__cypress/runner/cypress_runner.js:4110:17)
    at Promise.lastly.Promise.finally (http://localhost:3000/__cypress/runner/cypress_runner.js:4119:17)
    at Object.onRunnableRun (http://localhost:3000/__cypress/runner/cypress_runner.js:162793:53)
    at $Cypress.action (http://localhost:3000/__cypress/runner/cypress_runner.js:41042:28)
    at Runnable.run (http://localhost:3000/__cypress/runner/cypress_runner.js:145381:13)
    at Runner.runTest (http://localhost:3000/__cypress/runner/cypress_runner.js:155323:10)
    at http://localhost:3000/__cypress/runner/cypress_runner.js:155449:12
    at next (http://localhost:3000/__cypress/runner/cypress_runner.js:155232:14)
    at http://localhost:3000/__cypress/runner/cypress_runner.js:155242:7
    at next (http://localhost:3000/__cypress/runner/cypress_runner.js:155144:14)
    at http://localhost:3000/__cypress/runner/cypress_runner.js:155210:5
    at timeslice (http://localhost:3000/__cypress/runner/cypress_runner.js:145721:27)

Upvotes: 3

Views: 1059

Answers (2)

Mohsen Mahoski
Mohsen Mahoski

Reputation: 492

I handle server actions by updating parameters in my URL. Each server action should have a unique identifier. When an action is called, it should be added to the URL as a parameter and removed once the server action is completed (whether it succeeds or fails). This ensures that each server action is called with a unique identifier, making it easier to intercept. This approach will add extra logic to your server actions and application. Here is an example of the code:

React component:

"use client"

import React, { useState } from 'react'

import { getData } from '@/actions/getData.action';

const addParamToUrl = (param: string, value: string) => {
  const url = new URL(window.location.href);
  if (value) {
    url.searchParams.set(param, value);
  } else {
    url.searchParams.delete(param);
  }
  window.history.pushState({}, '', url);
}

const Button = () => {
  
   const [loading, setLoading] = useState(false);
   const [data, setData] = useState<string | null>(null);
   const [error, setError] = useState<string | null>(null);

  const handleGetData = async (identifier: string, wrong?: boolean) => {
    setLoading(true);
    
    addParamToUrl("identifier", identifier);
    const data = await getData(identifier, wrong);
    addParamToUrl("identifier", "");

   if(data?.error) {    
    setError("Error fetching data");
   } else {
    setData(data?.data);
   }
    setLoading(false);
  }
  
 
  return (
    <>
        <button 
          className='bg-green-500 cursor-pointer p-5 rounded-md' 
          data-cy="success"
          onClick={() => handleGetData("successIdentifier")}
        >
          Success message
        </button>
        <button 
          className='bg-red-500 cursor-pointer p-5 rounded-md' 
          data-cy="error"
          onClick={() => handleGetData("errorIdentifier", true)}
        >
          Error message
        </button>
        <div className='flex items-center justify-center flex-col'>
            {JSON.stringify(data)}
            {loading && <div>Loading...</div>}
            {error && <div className='text-red-500'>{error}</div>}
        </div>
    </>
  )
}

export default Button

Cypress test:

describe("template spec", () => {
  it("Click Success and r", async() => {
    cy.visit("http://localhost:3000");
    cy.intercept("POST", "http://localhost:3000/?identifier=successIdentifier")
.as("successHandler");
    cy.intercept("POST", "http://localhost:3000/?identifier=errorIdentifier")
.as("errorHandler");

    cy.get('[data-cy="success"]').click();
    cy.get('[data-cy="error"]').click();
    
    cy.wait("@successHandler").then(() => {
      cy.log("successHandler intercepted");
    });
    
    cy.wait("@errorHandler").then(() => {
      cy.log("errorHandler intercepted");
    });
  });
});

Server action:

'use server'
 
const delay = (time: number): Promise<void> => {
  return new Promise((resolve) => {
       setTimeout(() => {
        resolve();
       }, time);
  });
}

export async function getData(identifier: string, wrong?: boolean) {
   try {
    await delay(Math.random() * 10000);
    if(wrong){
        throw new Error("Error fetching data");
    }
    const res = await fetch(`https://jsonplaceholder.typicode.com/todos/1`)
    const data = await res.json();
    console.log(identifier);
    return {
        data
    }
   } catch {
     return {
        error: "Error fetching data",
     }
   }
}

Upvotes: 0

Terrabythia
Terrabythia

Reputation: 2161

I've figured it out. There are two things to note when intercepting and returning a custom response for a React Server Action request:

  • Make sure the header Content-Type: "text/x-component" is present on the response
  • Make sure the response body has a new line on the end of the content

The second one is what I was missing.

This example interception does work:

cy.intercept("POST", "/account/register", {
  headers: {
    "content-type": "text/x-component",
  },
  body: `0:["$@1",["development",null]]\n1:{"result":"success","status":200,"data":{"id":"982310j2","name":"Test User","email":"[email protected]","createdAt":"$D2024-05-26T16:32:11.280Z","updatedAt":"$D2024-05-26T14:32:11.288Z"}}\n`,
}).as("registerRequest");

I published a repository example cypress test using React Server Actions, so if anyone still has problems with mocking the response of a server action in cypress, they can look at my implementation here: https://github.com/terrabythia/cypress-server-action-test-example

Upvotes: 3

Related Questions