Maintaining a consistent UI in React apps is crucial for a seamless user experience. Visual testing helps detect unintended layout and style changes, preventing UI regressions. It ensures design accuracy, enhances reliability, and maintains brand consistency across devices.
Overview
React Visual Testing ensures that a React application’s UI remains consistent by detecting unintended visual changes. It helps catch design inconsistencies before they reach production.
Types of React Visual Testing:
- End-to-End Testing: Validates the entire UI workflow to catch layout shifts, broken styles, or missing elements across different environments.
- Component Testing: Focuses on individual UI components to ensure they render correctly in isolation and remain visually consistent.
How to Perform Visual Testing for React Apps
- Set up a testing framework: Choose a tool like Percy.
- Capture baseline snapshots: Take initial UI snapshots as a reference for future comparisons.
- Run visual tests: Execute tests across different devices and screen sizes to detect visual changes.
- Analyze and review differences: Compare new snapshots with baselines to identify unintended UI changes.
- Fix and approve changes: Update snapshots for intentional changes or fix regressions in the UI.
- Automate visual testing in CI/CD: Integrate visual tests into CI/CD pipelines to ensure UI consistency before deployment.
This article covers visual testing for react apps, its types, component testing, and end-to-end testing.
Understanding Visual Testing for Reach Apps
Visual testing for React apps ensures that UI components render correctly and remain visually consistent across updates. Unlike functional testing, which checks logic and interactions, visual testing captures snapshots of the UI and compares them to detect unintended changes.
This helps prevent layout shifts, broken styles, and regressions in design. It offers multiple benefits like:
- Prevents UI Bugs: Detects unexpected changes in design, layout, and responsiveness.
- Ensures Cross-Browser Consistency: Verifies that UI elements appear correctly across different browsers and devices.
- Detects Regression Issues: Helps maintain a stable design by catching unintended UI changes after code updates.
- Improves Developer Efficiency: Automates UI verification, reducing the need for manual visual inspections.
Read More: Strategies to Optimize Visual Testing
Types of React App Testing
In a React application, two levels of testing can be performed:
- End-to-End Testing is usually performed when the application is deployed and running with all its components rendered in the browser.
- Component Testing is performed during the development stage. It allows multiple tests with various combinations for a single component and helps improve application quality at an early stage.
React Component Visual Testing
For this article’s purpose, the Cypress Real World React app is used, which can be found here.
Prerequisite
- Refer to this documentation for Cypress and Percy’s setup
- For this article’s purpose, we are using the Cypress Real World React app, which can be found here.
- Please clone this repository to your local laptop for practice purposes.
The first step is to write a Cypress component test for the sign-in form, which looks like the following:
The component source code for this can be found here.
The Cypress component test for the Sign-in Form will look like this:
it("submits the username and password to the backend", () => { mount( <MemoryRouter> <SignInForm authService={authService} /> </MemoryRouter> ); cy.get("[data-test*=signin-username]").type("Katharina_Bernier"); cy.get("[data-test*=signin-password]").type("s3cret"); cy.get("[data-test*=signin-submit]").click(); cy.wait("@loginPost"); cy.get("[data-test*=signin-error]").should("not.exist"); });
Consider an example where the background color of the SIGN IN button needs to be validated.
The Cypress code to get the background color will look like below
cy.get(".MuiButton-containedPrimary").should( "have.css", "background-color", "rgb(63, 81, 181)" );
- But imagine writing this for several elements and covering for color and background color; this would become a time-consuming task.
- To speed up the time-consuming task and better validate, integrate Percy to capture the snapshot of this particular SIGN IN button and validate for any visual changes.
- Now use Percy’s command percySnapshot to capture a snapshot of this Sign in the form component.
The Cypress code appears as shown below, with further configuration details available here.
cy.percySnapshot("sign in form");
And the test will be as follows:
it("submits the username and password to the backend", () => { mount( <MemoryRouter> <SignInForm authService={authService} /> </MemoryRouter> ); cy.get("[data-test*=signin-username]").type("Katharina_Bernier"); cy.get("[data-test*=signin-password]").type("s3cret"); cy.get("[data-test*=signin-submit]").click(); cy.get("[data-test*=signin-error]").should("not.exist"); cy.percySnapshot("sign in form"); });
When the test runs, the first snapshot build is sent to Percy for approval. Once approved, it becomes the baseline image for future comparisons.
To see how the Percy Snapshot comparison works, change the background color of the SIGN IN button from secondary to primary and create a pull request.
Once the Github actions run the build, you can see the build failed as shown below.
The functional tests have passed, but the visual test performed with Percy in the component test has failed and requires review.
Click on the Details link against the failed Percy check to review.
- As you can see, the changed background color of the SIGN IN button is highlighted by Percy.
- You can now approve or Reject the build based on the requirement.
- The component-level Visual testing in React application consumes less time and gives early feedback on the appropriately used changes.
React End-to-End Visual Testing
End-to-End testing takes time as it covers the entire user journey in the application. While these tests focus on functionality, visual elements can still break. Using Percy for visual testing helps ensure the UI remains consistent throughout the user journey.
The Cypress test for the End-to-End flow will look like the following:
it.only("navigates to the new transaction form, selects a user and submits a transaction payment", function () { const payment = { amount: "35", description: "Sushi dinner 🍣", }; cy.getBySelLike("new-transaction").click(); cy.wait("@allUsers"); cy.getBySel("user-list-search-input").type(ctx.contact!.firstName, { force: true }); cy.wait("@usersSearch"); cy.visualSnapshot("User Search First Name Input"); cy.getBySelLike("user-list-item").contains(ctx.contact!.firstName).click({ force: true }); cy.visualSnapshot("User Search First Name List Item"); cy.getBySelLike("amount-input").type(payment.amount); cy.getBySelLike("description-input").type(payment.description); cy.visualSnapshot("Amount and Description Input"); cy.getBySelLike("submit-payment").click(); cy.wait(["@createTransaction", "@getUserProfile"]); cy.getBySel("alert-bar-success") .should("be.visible") .and("have.text", "Transaction Submitted!"); const updatedAccountBalance = Dinero({ amount: ctx.user!.balance - parseInt(payment.amount) * 100, }).toFormat(); if (isMobile()) { cy.getBySel("sidenav-toggle").click(); } cy.getBySelLike("user-balance").should("contain", updatedAccountBalance); cy.visualSnapshot("Updated User Balance"); if (isMobile()) { cy.get(".MuiBackdrop-root").click({ force: true }); } cy.getBySelLike("create-another-transaction").click(); cy.getBySel("app-name-logo").find("a").click(); cy.getBySelLike("personal-tab").click().should("have.class", "Mui-selected"); cy.wait("@personalTransactions"); cy.getBySel("transaction-list").first().should("contain", payment.description); cy.database("find", "users", { id: ctx.contact!.id }) .its("balance") .should("equal", ctx.contact!.balance + parseInt(payment.amount) * 100); cy.getBySel("alert-bar-success").should("not.exist"); cy.percySnapshot("Personal List Validate Transaction in List"); });
As you can see first, all functional checks are performed, followed by validation to ensure the visual aspects are correct.
- The above Cypress test can be found on this repo
- When you run the above test, you can see snapshots sent to Percy for the first time, and you need to Approve the build for making these images as baseline images.
If any change in the feature branch does not match the baseline image, the Percy test will fail, and Percy will highlight the changes, which you can Approve or Request for change.
Conclusion
Visual testing is essential for maintaining UI consistency in React applications. Component testing helps catch UI issues early, while end-to-end testing ensures a seamless user experience. Fixing bugs later in development is costly, so incorporating tests early improves product quality and ROI.
Using Percy for visual testing helps detect unintended UI changes efficiently. Adding more tests at the component level speeds up execution and ensures a stable, visually accurate application.