Testing apps
When an app is opened, an iframe is mounted on the page and Canva's APIs are injected into it.
The app can access those APIs at runtime via the window
object.
The problem is, this interferes with unit testing apps because, outside of Canva's environment, apps don't have access to the APIs. The only way to unit test them is to mock the APIs.
As of 2024-12-16, mocked versions of the APIs are included with the Apps SDK.
Recommended tooling
In the starter kit(opens in a new tab or window), we've included our recommended tooling and configuration for unit testing apps, including:
For the most part, testing apps is the same as testing any other React-based application, so we recommend checking out the official documentation for these tools to fully understand what they're capable of.
Setting up a test environment
If you cloned the starter kit or created an app with the CLI(opens in a new tab or window) after 2024-12-16, the test environment is already set up and you can skip this step. Otherwise, follow the instructions below to set everything up yourself.
Jest itself has existed as part of the starter kit for a while. If it's not available in your copy of the starter kit, it may be easier to clone of a fresh copy instead of setting up Jest from scratch.
-
Update to the latest version of the SDK packages:
npm install @canva/app-i18n-kit@latest @canva/app-ui-kit@latest @canva/asset@latest @canva/design@latest @canva/error@latest @canva/platform@latest @canva/user@latestBASH -
Create a
jest.setup.ts
file in the root of your project:touch jest.setup.tsBASH -
Copy the following code into the file:
// Import testing sub-packagesimport * as asset from "@canva/asset/test";import * as design from "@canva/design/test";import * as error from "@canva/error/test";import * as platform from "@canva/platform/test";import * as user from "@canva/user/test";// Initialize the test environmentsasset.initTestEnvironment();design.initTestEnvironment();error.initTestEnvironment();platform.initTestEnvironment();user.initTestEnvironment();// Once they're initialized, mock the SDKsjest.mock("@canva/asset");jest.mock("@canva/design");jest.mock("@canva/platform");jest.mock("@canva/user");TSXThe
@canva/error
package doesn't need to be mocked. -
In the
jest.config.js
file, add the followingsetupFiles
property:setupFiles: ["<rootDir>/jest.setup.ts"];TSXThis ensures the setup file is loaded — and the test environments are initialized — before the tests are run.
You can view the complete configuration file in the starter kit(opens in a new tab or window).
Testing an app's user interface
-
Create a file with a
.tests.tsx
suffix or place the file in a folder named "tests":# This works:touch src/app.tests.tsx# This also works:touch src/tests/app.tsxBASH -
Import the
render
function from React Testing Library:import { render } from "@testing-library/react";TSX -
Import the component to be tested, such as the
App
component:import { App } from "./app";TSX -
Import the following providers to wrap around the component during testing:
import { TestAppI18nProvider } from "@canva/app-i18n-kit";import { TestAppUiProvider } from "@canva/app-ui-kit";TSXFor example:
const result = render(<TestAppI18nProvider><TestAppUiProvider><App /></TestAppUiProvider></TestAppI18nProvider>);TSXTo reduce boilerplate, we recommend creating the following helper function:
function renderInTestProvider(node: React.ReactNode) {return render(<TestAppI18nProvider><TestAppUiProvider>{node}</TestAppUiProvider></TestAppI18nProvider>);}TSXThis can then be used instead of the standard
render
function:const result = renderInTestProvider(<App />);TSXYou only need to wrap the
TestAppI18nProvider
provider around localized components, but there's no downside to always including it. -
Use standard Jest syntax and matchers(opens in a new tab or window) to test and interact with the component:
import { TestAppI18nProvider } from "@canva/app-i18n-kit";import { TestAppUiProvider } from "@canva/app-ui-kit";import { render } from "@testing-library/react";import { App } from "./app";describe("App", () => {it("renders the app", async () => {const result = renderInTestProvider(<App />);expect(<App />).toBeInTheDocument();});});function renderInTestProvider(node: React.ReactNode) {return render(<TestAppI18nProvider><TestAppUiProvider>{node}</TestAppUiProvider></TestAppI18nProvider>);}TSX
Testing an app's behavior
You can use mocks to test the behavior of apps, which can be more informative that only testing the UI. For example, you can test what happens when a method is called with certain parameters or when it returns a certain value.
To use mocks in tests:
-
Import the methods that will be called during the test:
import { requestOpenExternalUrl } from "@canva/platform";TSX -
Create mocked versions of the methods:
const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);TSXThis isn't technically necessary, since the methods are already mocked in the setup file, but this approach allows us to benefit from the type-safety that TypeScript provides.
-
If the method is asynchronous — and most of the SDK methods are — mock the resolved or rejected value:
const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);// Mocking the resolved valueit("should open example URL", async () => {mockRequestOpenExternalUrl.mockResolvedValue({ status: "completed" });});// Mocking the rejected valueit("should not open example URL", async () => {mockRequestOpenExternalUrl.mockResolvedValue({ status: "aborted" });});TSX -
Use standard React Testing Library features, such as the
fireEvent
method, to interact with the component:import { render, fireEvent } from "@testing-library/react";const button = result.getByRole("button", { name: "Click here" });await fireEvent.click(button);TSX -
Use standard Jest matchers to test if (and how) the methods have been called:
const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);it("should open example URL", async () => {// ArrangemockRequestOpenExternalUrl.mockResolvedValue({ status: "completed" });const result = renderInTestProvider(<App />);// Actconst button = result.getByRole("button", { name: "Click here" });await fireEvent.click(button);// Assertexpect(requestOpenExternalUrl).toHaveBeenCalled();expect(requestOpenExternalUrl).toHaveBeenCalledTimes(1);expect(requestOpenExternalUrl).toHaveBeenCalledWith({url: "https://www.example.com",});});TSX -
Before each test, reset all mocks:
beforeEach(() => {jest.resetAllMocks();});TSXThis prevents the mocks from one test interfering with the mocks in another.
Running tests
To run the available tests, run the following command:
npm run test
To automatically re-run tests as the code changes, run the following command:
npm run test:watch
Example: Testing UI interactions
For a more complete example of unit testing apps, check out the unit testing example(opens in a new tab or window) in the starter kit.
src/app.tsx
import { Button, Rows } from "@canva/app-ui-kit";import { requestOpenExternalUrl } from "@canva/platform";import * as styles from "styles/components.css";export const App = () => {function handleClick() {requestOpenExternalUrl({url: "https://www.example.com",});}return (<div className={styles.scrollContainer}><Rows spacing="1u"><Button variant="primary" onClick={handleClick}>Click me</Button></Rows></div>);};
src/app.tests.tsx
import { TestAppI18nProvider } from "@canva/app-i18n-kit";import { TestAppUiProvider } from "@canva/app-ui-kit";import { render, fireEvent } from "@testing-library/react";import { requestOpenExternalUrl } from "@canva/platform";import { App } from "./app";describe("App", () => {const requestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);beforeEach(() => {jest.resetAllMocks();requestOpenExternalUrl.mockResolvedValue({ status: "completed" });});it("renders the app", async () => {const result = renderInTestProvider(<App />);expect(<App />).toBeInTheDocument();});it("should open example URL", async () => {// Arrangeconst result = renderInTestProvider(<App />);// Actconst button = result.getByRole("button", { name: "Click me" });await fireEvent.click(button);// Assertexpect(requestOpenExternalUrl).toHaveBeenCalled();expect(requestOpenExternalUrl).toHaveBeenCalledTimes(1);expect(requestOpenExternalUrl).toHaveBeenCalledWith({url: "https://www.example.com",});});});function renderInTestProvider(node: React.ReactNode) {return render(<TestAppI18nProvider><TestAppUiProvider>{node}</TestAppUiProvider></TestAppI18nProvider>);}