In the previous article, we talked about how Nx comes with first-class support for setting up Storybook. Nx also automatically generates Cypress e2e tests for the various Storybook stories, which is exactly what we're going to explore in this article.
Adding automated tests for our personal blog platform is probably overkill and most people wouldn't probably do. One of the key benefits of Nx is that it automatically integrates a variety of tools. So far in the series, we've seen
- automated setup of Next.js apps with TypeScript support
- allowing to split your application logic into separate libraries and seamlessly integrate them into an application, in our case a Next.js based one
- integrating Storybook with React components and TypeScript
By having Nx generate these configs for you, you don't have to deal with the complexity of setting up all these tools. Also, it will lower the entry barrier and friction for developers to start using them. Such as the automated Cypress setup. I probably wouldn't write Cypress tests for my personal blog platform, but given the hard task of setting everything up is already done, what's left is really only to write some high-level tests.
Writing Cypress e2e tests for your Next.js application
Right at the very beginning of this series when we generated our Nx workspace with the Next.js preset, you might have noticed that we also got a apps/site-e2e
folder setup automatically.
Nx also generated a default Cypress spec file:
// apps/site-e2e/src/integration/app.spec.ts
import { getGreeting } from '../support/app.po';
describe('site', () => {
beforeEach(() => cy.visit('/'));
it('should display welcome message', () => {
// Custom command example, see `../support/commands.ts` file
cy.login('my-email@something.com', 'myPassword');
// Function helper example, see `../support/app.po.ts` file
getGreeting().contains('Welcome to site!');
});
});
You can run the Next.js app Cypress tests very much the same way as we did for our Storybook Cypress tests:
npx nx e2e site-e2e
Obviously, they might not pass successfully right now since we've modified the initially generated application. Let's fix them and to make an example, let's test whether our markdown rendered article we've covered in a previous post, renders properly at /articles/dynamic-routing
.
What we want to test is
- When navigating to
/articles/dynamic-routing
, the page loads properly. - The
h1
contains the expected title of the article. - the embedded Youtube component we talked about in the article about component hydration with MDX renders properly.
We can launch Cypress in "watch mode" such that we can see the test running as we make adjustments.
npx nx e2e site-e2e --watch
Let's modify the existing apps/site-e2e/src/integration/app.spec.ts
file to implement the Cypress test.
Note, you might want to create a dedicated spec file for testing the article loading, while the
app.spec.ts
might be more suitable for loading more high-level things about the web app. Like whether the navbar loads etc. But for the purpose of demoing the Cypress integration quickly, it works 🙂.
Here's the modified test:
// apps/site-e2e/src/integration/app.spec.ts
describe('site', () => {
beforeEach(() => {
// navigate to an example article
cy.visit('/articles/dynamic-routing');
});
it('should render the title of the article', () => {
cy.get('h1').should('contain', 'Dynamic Routing and Static Generation');
});
it('should properly render the Youtube component', () => {
cy.get('iframe').should('be.visible');
});
});
If you have a look at the Cypress runner, you should see it pass properly.
Writing Cypress e2e tests for our previously created Storybook stories
Similarly to the Next.js app based e2e tests, Nx also generated e2e tests specifically for our Storybook setup, which we generated in the previous article. All those tests reside in the apps/storybook-e2e/ui-e2e
folder. The reason why they are in a separate "storybook-e2e" folder is because I specifically passed that as the Cypress directory when generating the Storybook setup.
The default Cypress spec generated by Nx is the following:
// apps/storybook-e2e/ui-e2e/src/integration/topic-button/topic-button.spec.ts
describe('shared-ui: TopicButton component', () => {
beforeEach(() => cy.visit('/iframe.html?id=topicbutton--primary'));
it('should render the component', () => {
cy.get('h1').should('contain', 'Welcome to TopicButton!');
});
});
There are a couple of things to notice here in terms of the testing strategy. What Nx leverages here when generating the Storybook tests, is Storybook's interaction testing functionality. That feature allows to directly target the story rendering via a URL:
cy.visit('/iframe.html?id=topicbutton--primary')
Furthermore, we can control the different component props variation by leveraging the possibility to also pass the Story args via the URL:
cy.visit('/iframe.html?id=topicbutton--primary&args=topicName:Next.js;');
Having that knowledge we can easily develop our Cypress test.
Launch Storybook Cypress e2e tests
npx nx e2e storybook-e2e-ui-e2e --watch
By passing the --watch
flag, we can interact with the Cypress runner which is handy during development. Without the flag, the e2e tests will run in headless mode which is suitable for CI.
When you launch this command, behind the scenes, Nx serves our Storybook for the shared/ui
library, followed by launching Cypress and making sure it points to the local Storybook server.
Obviously running the Cypress e2e now wouldn't really work as we've changed the implementation of our React component meanwhile. So let's fix that.
Implementing the Cypress test for our Storybook story
We want to have two different test cases for our simple Topic Button component:
- make sure it renders the passed
topicName
properly - make sure it passes the topic name to the event handler when clicking on the Topic button component
Test case 1
In order to have "hook points" that can be grabbed during the Cypress test run, it is good practice to use data-testid
attributes on the DOM elements which we want to use in our test implementation. Thus, we need to change our topic-button.tsx
and add one to the rendering element of our topicName
as well as to the entire topic button div:
// libs/shared/ui/src/lib/topic-button/topic-button.tsx
...
export function TopicButton(props: TopicButtonProps) {
...
return (
<div
...
data-testid="topicButton"
>
<img src={icon} alt="" className="w-12" />
<div className="p-5">
<h2 className="font-bold text-4xl" data-testid="topicName">
{props.topicName}
</h2>
</div>
</div>
);
}
Then, in our test case, we use set the Story args via the URL, in this case passing first topicName:Next.js
, and then we verify whether the [data-testid=topicName]
element contains the correct name. And to be sure, we also change it to React
and assert those changes are reflected in the rendering
// apps/storybook-e2e/ui-e2e/src/integration/topic-button/topic-button.spec.ts
describe('shared-ui: TopicButton component', () => {
it('should render the topic name', () => {
cy.visit('/iframe.html?id=topicbutton--primary&args=topicName:Next.js;');
cy.get('[data-testid=topicName]').should('contain', 'Next.js');
cy.visit('/iframe.html?id=topicbutton--primary&args=topicName:React;');
cy.get('[data-testid=topicName]').should('contain', 'React');
});
});
Test case 2
Back when implementing our topic-button.stories.tsx
we added a feature to the story that registers to the TopicButton
's onClick
event and renders the result directly below the button. This makes it particularly easy to test it in our Cypress test. To make it easy to grab the according DOM element in our Cypress test, we add another data-testid="click-result"
to that element.
// libs/shared/ui/src/lib/topic-button/topic-button.stories.tsx
...
const Template: Story<TopicButtonProps> = (args) => {
const [clickedTopic, setClickedTopic] = useState<string | null>(null);
return (
<div className="bg-gray-100 p-20">
<TopicButton
{...args}
onClick={(topicName) => setClickedTopic(topicName)}
/>
{clickedTopic && (
<div data-testid="click-result">
Button has been clicked: {clickedTopic}
</div>
)}
</div>
);
};
export const Primary = Template.bind({});
Primary.args = {
topicName: 'Next.js',
};
In the topic-button.spec.ts
we add another test case, set the topicName
to React
, click the topic button component and verify the output matches our expectations:
// apps/storybook-e2e/ui-e2e/src/integration/topic-button/topic-button.spec.ts
describe('shared-ui: TopicButton component', () => {
it('should render the topic name', () => {
...
});
it('clicking the icon should properly pass the name of the topic to the event handler', () => {
cy.visit('/iframe.html?id=topicbutton--primary&args=topicName:React;');
cy.get('[data-testid=topicButton]').click();
cy.get('[data-testid=click-result]').should('contain', 'React');
});
});
Running Cypress tests
Finally we can run the Cypress tests again
npx nx e2e storybook-e2e-ui-e2e --watch
Conclusion
In this article we learned
- How Nx is able to automatically generates a Cypress e2e test for our Nx apps as well as our Storybook stories
- How the Cypress setup works
- How to implement a simple Cypress test for our Next.js app
- How to implement the Cypress e2e test for our Topic button story
See also:
- https://storybook.js.org/docs/react/workflows/interaction-testing
- https://storybook.js.org/docs/react/writing-stories/args#setting-args-through-the-url
- https://docs.cypress.io/guides/references/best-practices#Selecting-Elements
GitHub repository
All the sources for this article can be found in this GitHub repository's branch: https://github.com/juristr/blog-series-nextjs-nx/tree/08-storybook-cypress-tests
Learn more
🧠 Nx Docs
👩💻 Nx GitHub
💬 Nrwl Community Slack
📹 Nrwl Youtube Channel
🥚 Free Egghead course
🧐 Need help with Angular, React, Monorepos, Lerna or Nx? Talk to us 😃
Also, if you liked this, click the ❤️ and make sure to follow Juri and Nx on Twitter for more!