On September 25th, 2024, we released v2 of the Apps SDK. To learn what’s new and how to upgrade, see Migration FAQ and Migration guide.

Testing apps

How to unit test apps with Jest and React Testing Library.

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.

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.

  1. 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@latest
    BASH
  2. Create a jest.setup.ts file in the root of your project:

    touch jest.setup.ts
    BASH
  3. Copy the following code into the file:

    // Import testing sub-packages
    import * 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 environments
    asset.initTestEnvironment();
    design.initTestEnvironment();
    error.initTestEnvironment();
    platform.initTestEnvironment();
    user.initTestEnvironment();
    // Once they're initialized, mock the SDKs
    jest.mock("@canva/asset");
    jest.mock("@canva/design");
    jest.mock("@canva/platform");
    jest.mock("@canva/user");
    TSX

    The @canva/error package doesn't need to be mocked.

  4. In the jest.config.js file, add the following setupFiles property:

    setupFiles: ["<rootDir>/jest.setup.ts"];
    TSX

    This 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

  1. 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.tsx
    BASH
  2. Import the render function from React Testing Library:

    import { render } from "@testing-library/react";
    TSX
  3. Import the component to be tested, such as the App component:

    import { App } from "./app";
    TSX
  4. 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";
    TSX

    For example:

    const result = render(
    <TestAppI18nProvider>
    <TestAppUiProvider>
    <App />
    </TestAppUiProvider>
    </TestAppI18nProvider>
    );
    TSX

    To reduce boilerplate, we recommend creating the following helper function:

    function renderInTestProvider(node: React.ReactNode) {
    return render(
    <TestAppI18nProvider>
    <TestAppUiProvider>{node}</TestAppUiProvider>
    </TestAppI18nProvider>
    );
    }
    TSX

    This can then be used instead of the standard render function:

    const result = renderInTestProvider(<App />);
    TSX

    You only need to wrap the TestAppI18nProvider provider around localized components, but there's no downside to always including it.

  5. 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:

  1. Import the methods that will be called during the test:

    import { requestOpenExternalUrl } from "@canva/platform";
    TSX
  2. Create mocked versions of the methods:

    const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);
    TSX

    This 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.

  3. 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 value
    it("should open example URL", async () => {
    mockRequestOpenExternalUrl.mockResolvedValue({ status: "completed" });
    });
    // Mocking the rejected value
    it("should not open example URL", async () => {
    mockRequestOpenExternalUrl.mockResolvedValue({ status: "aborted" });
    });
    TSX
  4. 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
  5. 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 () => {
    // Arrange
    mockRequestOpenExternalUrl.mockResolvedValue({ status: "completed" });
    const result = renderInTestProvider(<App />);
    // Act
    const button = result.getByRole("button", { name: "Click here" });
    await fireEvent.click(button);
    // Assert
    expect(requestOpenExternalUrl).toHaveBeenCalled();
    expect(requestOpenExternalUrl).toHaveBeenCalledTimes(1);
    expect(requestOpenExternalUrl).toHaveBeenCalledWith({
    url: "https://www.example.com",
    });
    });
    TSX
  6. Before each test, reset all mocks:

    beforeEach(() => {
    jest.resetAllMocks();
    });
    TSX

    This 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
BASH

To automatically re-run tests as the code changes, run the following command:

npm run test:watch
BASH

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>
);
};
TSX

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 () => {
// Arrange
const result = renderInTestProvider(<App />);
// Act
const button = result.getByRole("button", { name: "Click me" });
await fireEvent.click(button);
// Assert
expect(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>
);
}
TSX