
Testing the Content of JSX Data in Cypress

I've recently been working on a new feature for my airline client, which involves extending the new search interface my team and I recently launched. With the previous search experience, the marketing team had been using Adobe Target to target specific search queries to display seasonal and location‑specific messages. Naturally, this was a feature the client was keen to include in the new search experience. Equally as naturally, I was not keen for them to use Target (which is ‑ after all ‑ only intended as an A/B testing tool and not a feature‑adder) to do this.
So, whilst we waited for the new headless CMS to be ready, we were presented with a set of markup which had been extracted from Target and was to be displayed strictly as provided. We ended up with a data structure where each object was an airport code (or series in the case of targeting specific journeys), with an accompanying title (a string) and description (as JSX ‑ directly from the HTML markup provided).
Here's a brief excerpt demonstrating the message for Antigua (whether the arrival or departure airport) and for the journey from Barbados to Edinburgh:
export const messages = { ANU: { title: 'Looking for flights to Antigua?', description: ( <> <p> Fly direct to Antigua with Virgin Atlantic during our seasonal service{' '} <strong>three times a week</strong>. </p> </> ), }, 'BGI-EDI': { title: 'We can see you searched for Edinburgh to Barbados flights.', description: ( <> <p> Unfortunately we recently made the tough choice to suspend our plans to operate these services. We're still serving this Caribbean favourite from Manchester and London Heathrow, if you'd like to fly from there instead. </p> </> ), },};I should mention here that some of the messages also include lists and other markup artefacts, which is why each description has been wrapped in a fragment (<></>), although it would make no difference whether some were multiple elements within fragments whilst others were single‑element items,
Development for this was relatively straightforward, and before too long, we had everything working:
Test Coverage
In this project environment, we use Cypress for test coverage. Testing that the message was displayed for specific searches was relatively straightforward; the complication with Cypress comes when you need ‑ as we did ‑ to test JSX, to ensure that the right message is being displayed for the right search.
In Cypress (or any end‑to‑end testing framework), testing for JSX matches presents a challenge because these frameworks only operate and test against the final, rendered HTML that your component or application produces, not the React component tree of the JSX itself. React describes with the UI of a component should look like; when rendered, JSX is transformed into React.createElement calls, which in turn produce JavaScript objects that represent the desired DOM structure (the "virtual DOM").
Whilst in our case, we are essentially only using JSX as an HTML wrapper anyway, this can become much more difficult if you're dealing with dynamic content or further component abstraction.
Suffice it to say: it isn't as simple as writing a test that says something like: cy.get(selectors.content).should('contain', JSX).
Whilst there's no right or wrong way of approaching this (and believe me ‑ I've tried a few in my time), the simplest method I've come up with to test JSX data against the rendered result in Cypress is to:
- Extract text from the JSX object;
- Loop over that text and check if each word within it appears in the rendered result.
Extracting Text from JSX
For this, I wrote a utility function which we can now use across our test suite. Here it is:
const extractTextFromJSX = (node: ReactNode): string => { if (typeof node === 'string') { return node; } else if (Array.isArray(node)) { return node.map(extractTextFromJSX).join(' '); } else if (isValidElement(node) && node.props.children) { return extractTextFromJSX(node.props.children); } return '';};Here, we're recursively extracting and concatenating text content from a JSX object, and returning the text as a string. This is done by looping through the object:
- If the node is a
stringthen we can simply return it. - If the node is an
array(which indicates that the component has multiple children), then it recursively called itself on each element within the array, joining the results together with spaces. - If the node is a valid React element and has children, then we recursively apply the function to
node.props.children. This way we can handle the descent into nested component structures and still extract text from those deeper levels of the tree. - Finally, we have a fallback: if our function gets to this point then we simply return an empty string to ensure that we always return something, even if no text content is found.
So, you can simply pass your JSX into this function and expect a string of space‑separated text to come out the other end. Bear in mind that you'll also need to import isValidElement and ReactNode from React in order to use this.
Testing JSX Against Rendered Content
Once you have the utility to convert your JSX into a string, comparing that against the rendered content becomes much more straightforward. You still can't do an equity check, but what you can do is loop over each word in the JSX output and check to see if the rendered version contains it.
Here's one I prepared earlier:
const expectedTextContent = extractTextFromJSX(description as JSX.Element);expectedTextContent.split(' ').forEach((word) => { cy.get(selectors.content).should('contain', word);});What this does is:
- Creates
expectedTextContent, which is the extracted text from the JSX (in this instance:description); - Uses
split()to separate this into an array of words; - Uses a
forEachto loop over each word, checking ifselectors.contentcontains it.
And that's about it! In this way we can take a JSX object into our Cypress tests, convert it to a string of text content, and then check that the rendered content contains the JSX content.
Caveats
As you might have already noticed, there are ‑ of course ‑ a couple of caveats with this approach (although I would argue this is still the most straightforward approach)...
Ordering
The way this test works, it will check each individual word that it has extracted from the JSX to see if it appears anywhere within the expected rendered element. This means that ordering is not at all catered for. the content 'lorem ipsum dolor sit amet' would still return as a pass if the rendered 'ipsum dolor lorem amet sit'.
There's no easy way to counter this one. If ordering is important (or likely to differ), then you could consider breaking the JSX down into smaller chunks (i.e., by paragraph) and check each of those individually. However, it would generally be fair to assume that the ordering of your JSX and that of the rendered output would match.
What this does do is test that every single word exists in both, so the coverage itself is very thorough.
Performance
On the topic of thoroughness, this is not a quick process and if you're dealing with very large pieces of JSX, then this could lead to slow tests. In our case, we found that each individual test took between around 31ms and around 110ms. Not grievously long periods of time by any means, but if you had a lot of tests to run then this can easily add up.

Equally, due to its recursive nature, the amount of time that the extractTextFromJSX function takes to actually extract the text will increase exponentially as the size and complexity of the JSX object increases.
Wrapping up
In our case (and in the cases I've come across), where it is necessary to add Cypress tests that ensure the content of our JSX matches the rendered output, the most straightforward approach I've come up with is:
- Convert the JSX content to a string of words by recursively extracting the text;
- Loop over each word in the extracted text and ensure that the word exists in the rendered output.
There are any number of other ways to approach this, but for us, this is how we approached it.
Related Articles

Angular Change Detection: How It Works and How to Optimise It. 
Validating Parentheses Input Using TypeScript. Validating Parentheses Input Using TypeScript

When to Use var or let or const. When to Use
varorletorconst
Exploring CSS Viewport Units Beyond vw and vh. Exploring CSS Viewport Units Beyond
vwandvh
Understanding Object Types with JavaScript's instanceof. Understanding Object Types with JavaScript's
instanceof
Preventing and Debugging Memory Leaks in React. Preventing and Debugging Memory Leaks in React

Understanding the JavaScript Event Loop. Understanding the JavaScript Event Loop

Flattening Arrays in JavaScript. Flattening Arrays in JavaScript

Caching Strategies in React. Caching Strategies in React

The Difference Between == and === in JavaScript. The Difference Between
==and===in JavaScript
What is Front‑End Development? What is Front‑End Development?

JavaScript's typeof Operator: Uses and Limitations. JavaScript's
typeofOperator: Uses and Limitations