Expo has first-class support for building full-stack websites with React, so I can leverage that to add Cypress/Playwright for E2E testing and add the Storybook for UI components.
What is @nx/expo? Nx is a build system that helps you maintain and scale monorepos, both locally and on CI. @nx/expo contains generators for managing Expo applications and libraries within an Nx workspace.
It should create the expo app and its Cypress e2e tests. I can run npx nx run <your-app-name>-e2e:e2e to run the Cypress e2e tests.
Add Cypress to an Existing Nx Expo App
Before I start, I need to make sure I can serve my Expo app for the web view. In this example, in my apps/cats/project.json, I got a serve target defined:
So I can run the command npx nx run cats:serve to serve the app on http://localhost:8081/.
I can also use @nx/expo inferred tasks without having a serve target in project.json. In .env file, set NX_ADD_PLUGINS=true; then run the command
npx nx add @nx/expo
Then I can still run the serve command npx nx run <your expo project>:serve without a serve target defined.
Then I can set up the cypress e2e project against the web app.
First, I need to install @nx/cypress:
npx nx add @nx/cypress
Second, I create a project.json for my Cypress project. In this example, I create a project.json at apps/cats-cypress/project.json, I name the project cats-cypress:
{"name":"cats-cypress"}
Third, run the below command to generate Cypress configuration:
It should create the expo app and its Playwright e2e tests.
Add Playwright to an Existing Nx Expo App
Similarly to Cypress steps, before I start, I need to make sure I can serve my Expo app for the web view. In this example, in my apps/cats/project.json, I got a serve target defined:
Second, I create a project.json for my Playwright project. In this example, I create a project.json at apps/cats-playwright/project.json, I name the project cats-playwright:
{"name":"cats-playwright"}
Third, run the below command to generate Cypress configuration:
npx nx g @nx/playwright:configuration --project cats-playwright --webServerCommand"nx run cats:serve"--webServerAddress http://localhost:8081
Since the serve command is npx nx run cats:serve, so the flag is --webServerCommand is nx run cats:serve. Also, the --webServerAddress is [http://localhost:8081](http://localhost:8081.).
It should generate playwright.config.ts and a sample test file:
For this example, I change the example test at apps/cats-playwright/e2e/example.spec.ts to:
import{test,expect}from'@playwright/test';test('has title',async ({page})=>{awaitpage.goto('/');// Expect h1 to contain a substring. expect(awaitpage.locator('h1').innerText()).toContain('Cat Facts');});
Run command npx nx run cats-playwright:e2e, and the tests should pass:
In this example, I should see in the console output:
npx nx g @nx/react:storybook-configuration cats
> NX Generating @nx/react:storybook-configuration
✔ Do you want to set up Storybook interaction tests? (Y/n) · true
✔ Automatically generate \*.stories.ts files for components declared in this project? (Y/n) · true
✔ Configure a static file server for the storybook instance? (Y/n) · true
UPDATE nx.json
UPDATE package.json
CREATE apps/cats/.storybook/main.ts
CREATE apps/cats/.storybook/preview.ts
CREATE apps/cats/tsconfig.storybook.json
UPDATE apps/cats/tsconfig.app.json
UPDATE apps/cats/tsconfig.json
UPDATE apps/cats/project.json
CREATE apps/cats/src/app/App.stories.tsx
CREATE apps/cats/src/app/bookmarks/bookmarks.stories.tsx
CREATE apps/cats/src/app/facts/facts.stories.tsx
That is it! In one single, it should generate the Storybook for me. Run the command to see Storybook in web view:
npx nx run <your-expo-app>:storybook
It interpolates my native UI components to web components and then creates the Storybook. How does it achieve that?
In this example, it generates a file apps/cats/.storybook/main.ts:
importtype{StorybookConfig}from'@storybook/react-webpack5';constconfig:StorybookConfig={stories:\['../src/lib/\*\*/\*.stories.@(js|jsx|ts|tsx|mdx)'\],addons:\['@storybook/addon-essentials','@storybook/addon-interactions','@nx/react/plugins/storybook',\],framework:{name:'@storybook/react-webpack5',options:{},},webpackFinal:async (config)=>{if (config.resolve){config.resolve.alias={...config.resolve.alias,'react-native$':'react-native-web',};config.resolve.extensions=\['.web.tsx','.web.ts','.web.jsx','.web.js',...(config.resolve.extensions??\[\]),\];}returnconfig;},};exportdefaultconfig;// To customize your webpack configuration you can use the webpackFinal field. // Check https://storybook.js.org/docs/react/builders/webpack#extending-storybooks-webpack-config // and https://nx.dev/recipes/storybook/custom-builder-configs
It uses Webpack to bundle and alias react-native to react-native-web.
When serving up my Storybook for the first time, I have some issues and problems preventing components from rendering. Below are the issues I run into and how I troubleshoot them.
Because I am using the library @react-navigation/native and I use its hooks like useNavigtion and useRoute inside my component, I have this error:
The easiest way is just to mock this library and create a decorator for it. In this example, I create a Navigation Decorator at apps/cats/.storybook/mocks/navigation-decorator.tsx:
Then I can add this decoration to my story file. In this example, I add it to apps/cats/src/app/bookmarks/bookmarks.stories.tsx:
importtype{Meta,StoryObj}from'@storybook/react';import{Bookmarks}from'./bookmarks';import{within}from'@storybook/testing-library';import{expect}from'@storybook/jest';import{NavigationDecorator}from'../../../.storybook/mocks/navigation-decorator';constmeta:Meta<typeofBookmarks>={component:Bookmarks,title:'Bookmarks',decorators:NavigationDecorator,// <---- add the decorator here };exportdefaultmeta;typeStory=StoryObj<typeofBookmarks>;exportconstPrimary={args:{},};exportconstHeading:Story={args:{},play:async ({canvasElement})=>{constcanvas=within(canvasElement);expect(canvas.getByText(/Welcome to Bookmarks!/gi)).toBeTruthy();},};
Error: Couldn’t find a route object.
This issue is similar to “Couldn’t find a navigation object”, it is also related to the library @react-navigation/native.
It happens because my component is using the useRoute hook and expecting certain routing parameters. I simply need to customize the mock NavigationDecorator for my component. For example, below is a component that is expecting an id from the route parameters:
Error: No QueryClient set, use QueryClientProvider to set one
This happens because I use the library @tanstack/react-query. I need to create a decorator for QueryClientProvider. At apps/cats/.storybook/mocks/query-client-decorator.tsx, I create this decorator:
In this example, my story at apps/cats/src/app/facts/facts.stories.tsx will become:
importtype{Meta,StoryObj}from'@storybook/react';import{Facts}from'./facts';import{within}from'@storybook/testing-library';import{expect}from'@storybook/jest';import{NavigationDecorator}from'../../../.storybook/mocks/navigation-decorator';import{QueryClientDecorator}from'../../../.storybook/mocks/query-client-decorator';constmeta:Meta<typeofFacts>={component:Facts,title:'Facts',decorators:\[NavigationDecorator,QueryClientDecorator\],// <---- add the QueryClientDecorator here };exportdefaultmeta;typeStory=StoryObj<typeofFacts>;exportconstPrimary={args:{},};exportconstHeading:Story={args:{},play:async ({canvasElement})=>{constcanvas=within(canvasElement);expect(canvas.getByText(/Welcome to Facts!/gi)).toBeTruthy();},};
Error: Unable to load react-native-vector-icons.
I got this error when I tried to compile the storybook:
You may need an additional loader to handle the result of these loaders.
|
| return (
> <Text selectable={false} {...props}>
| {glyph}
| {children}
@ ./node\_modules/react-native-vector-icons/MaterialCommunityIcons.js 6:0-50 9:16-29
@ ./node\_modules/react-native-paper/lib/module/components/MaterialCommunityIcon.js 8:27-94
@ ./node\_modules/react-native-paper/lib/module/core/PaperProvider.js 3:0-72 73:12-33
@ ./node\_modules/react-native-paper/lib/module/index.js 4:0-59 4:0-59 5:0-64 5:0-64
@ ./apps/cats/src/app/App.tsx 1:717-760 1:1882-1892
@ ./apps/cats/src/app/ lazy ^\\.\\/.\*$ namespace object ./App.tsx ./App
@ ./storybook-stories.js 1:383-427
@ ./storybook-config-entry.js 1:171-216 1:1789-1797 1:1871-1985 1:1926-1984 1:1972-1980
This happens because I use the library react-native-paper which depends on react-native-vector-icons.
In react-native-vector-icons, it .js files contain jsx code: <Text selectable={false} {…props}>. To solve this, I need to load the react-native-vector-icons library using babel-loader. In this example, I can add a rule to Storybook’s Webpack config:
The Storybook configuration at apps/cats/.storybook/main.ts will become:
importtype{StorybookConfig}from'@storybook/react-webpack5';constconfig:StorybookConfig={stories:\['../src/app/\*\*/\*.stories.@(js|jsx|ts|tsx|mdx)'\],staticDirs:\['./public'\],addons:\['@storybook/addon-essentials','@storybook/addon-interactions','@nx/react/plugins/storybook',\],framework:{name:'@storybook/react-webpack5',options:{},},webpackFinal:async (config)=>{if (config.resolve){config.resolve.alias={...config.resolve.alias,'react-native$':'react-native-web',};config.resolve.extensions=\['.web.tsx','.web.ts','.web.jsx','.web.js',...(config.resolve.extensions??\[\]),\];if (config.module?.rules){config.module.rules.push({test:/\\.(js|jsx)$/,include:/react-native-vector-icons/,loader:'babel-loader',options:{presets:\['@babel/preset-env',\['@babel/preset-react',{runtime:'automatic'}\],\],},});}}returnconfig;},};exportdefaultconfig;// To customize your webpack configuration you can use the webpackFinal field. // Check https://storybook.js.org/docs/react/builders/webpack#extending-storybooks-webpack-config // and https://nx.dev/recipes/storybook/custom-builder-configs
Now I can see react-native-vector-icons are being loaded correctly for the Storybook.
Now I can run npx nx run cats:storybook and it should render the storybook for me.