Skip to content

Testing Apps

Testing Overview

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, we've included our recommended tooling and configuration for unit testing apps, including:

  • Jest
  • React Testing Library

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

Warning: 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:

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

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

    tsx
    // 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 intents from "@canva/intents/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();
    intents.initTestEnvironment();
    platform.initTestEnvironment();
    user.initTestEnvironment();
    
    // Once they're initialized, mock the SDKs
    jest.mock("@canva/asset");
    jest.mock("@canva/design");
    jest.mock("@canva/intents");
    jest.mock("@canva/platform");
    jest.mock("@canva/user");

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

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

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

    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.

Testing an app's user interface

  1. Create a file with a .tests.tsx suffix or place the file in a folder named "tests":

    bash
    # This works:
    touch src/app.tests.tsx
    
    # This also works:
    touch src/tests/app.tsx
  2. Import the render function from React Testing Library:

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

    tsx
    import { App } from "./app";
  4. Import the following providers to wrap around the component during testing:

    tsx
    import { TestAppI18nProvider } from "@canva/app-i18n-kit";
    import { TestAppUiProvider } from "@canva/app-ui-kit";

    For example:

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

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

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

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

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

    Note: 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 to test and interact with the component:

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

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:

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

    tsx
    const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);

    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:

    tsx
    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" });
    });
  4. Use standard React Testing Library features, such as the fireEvent method, to interact with the component:

    tsx
    import { render, fireEvent } from "@testing-library/react";
    const button = result.getByRole("button", { name: "Click here" });
    await fireEvent.click(button);
  5. Use standard Jest matchers to test if (and how) the methods have been called:

    tsx
    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",
      });
    });
  6. Before each test, reset all mocks:

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

    This prevents the mocks from one test interfering with the mocks in another.

Running tests

To run the available tests, run the following command:

bash
npm run test

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

bash
npm run test:watch

Example: Testing UI interactions

Note: For a more complete example of unit testing apps, check out the unit testing example in the starter kit.

src/app.tsx

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

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

Test Harness

A step by step guide setting up the Apps SDK test harness.

The Canva Apps SDK test harness lets you run and test your Canva app outside of the Canva environment.

This can be especially useful as a visual assist tool for AI agents, providing them with a clean browser environment to observe and debug your app's UI without the Canva interface as a distraction. This isolation helps AI agents better understand and suggest improvements to your app's interface.

NOTE: The Canva Apps SDK harness is provided as an experimental resource to support development.

Step 1: Create required directories

First, create the necessary directories in your project root, if they don't already exist:

bash
mkdir -p harness public

Step 2: Create the harness HTML file

Create a file at public/harness.html with the following content:

html
<!doctype html>
<html dir="ltr" lang="en" class="cc24 theme dark">
  <head>
    <title>Canva Apps SDK Test Harness</title>
    <script src="/init.js" defer></script>
    <script src="/harness.js" defer></script>
  </head>
  <body
    style="
      width: 360px;
      height: 770px;
      padding: 10px;
      border: 1px solid red;
      overflow-y: auto;
      overflow-x: hidden;
    "
  >
    <div id="root" style="width: 100%; height: 100%"></div>
  </body>
</html>

Step 3: Create the harness initialization file

Create a file at harness/init.ts with the following content:

typescript
import * as design from "@canva/design/test";
import * as asset from "@canva/asset/test";
import * as user from "@canva/user/test";
import * as error from "@canva/error/test";
import * as intents from "@canva/intents/test";
import * as platform from "@canva/platform/test";

design.initTestEnvironment();
asset.initTestEnvironment();
error.initTestEnvironment();
user.initTestEnvironment();
intents.initTestEnvironment();
platform.initTestEnvironment();

Step 4: Create the harness app file

Create a file at harness/harness.tsx with the following content (replace src/app.tsx with the path to your app's main component):

typescript
import React from "react";
import { TestAppUiProvider } from "@canva/app-ui-kit";
import { createRoot } from "react-dom/client";
import { App } from "../src/app"; // Change this path to your app's main component
import "@canva/app-ui-kit/styles.css";
import { TestAppI18nProvider } from "@canva/app-i18n-kit";

const root = createRoot(document.getElementById("root") as Element);

function render() {
  root.render(
    <TestAppI18nProvider>
      <TestAppUiProvider>
        <App />
      </TestAppUiProvider>
    </TestAppI18nProvider>,
  );
}

render();

if (module.hot) {
  module.hot.accept("../src/app.tsx", render); // Change this path to your app's main component
}

Step 5: Update your webpack configuration

NOTE: If you have created your app from the Canva CLI (using canva apps create, since version 0.0.1-beta.28), you might already have IN_HARNESS configured.

Modify your webpack.config.ts to support the harness environment. The following diff shows the required changes for your webpack configuration.

diff
  export function buildConfig({
    devConfig,
    appEntry = path.join(process.cwd(), "src", "index.tsx"),
    backendHost = process.env.CANVA_BACKEND_HOST,
+   inHarness = process.env.IN_HARNESS?.toLowerCase() === "true",
  }: {
    devConfig?: DevConfig;
    appEntry?: string;
    backendHost?: string;
+   inHarness?: boolean;
  } = {}): Configuration & DevServerConfiguration {
    // ...
    return {
      mode,
      context: path.resolve(process.cwd(), "./"),
-     entry: {
-       app: appEntry,
-     },
+     entry: inHarness
+       ? {
+           harness: path.join(process.cwd(), "harness", "harness.tsx"),
+           init: path.join(process.cwd(), "harness", "init.ts"),
+         }
+       : {
+           app: appEntry,
+         },
      target: "web",
      resolve: {
        alias: {
          styles: path.resolve(process.cwd(), "styles"),
          src: path.resolve(process.cwd(), "src"),
        },
        extensions: [".ts", ".tsx", ".js", ".css", ".svg", ".woff", ".woff2"],
      },
      infrastructureLogging: {
-       level: "none",
+       level: inHarness ? "info" : "none",
      },
      module: {
        rules: [
           // ...
        ],
      },
      ...buildDevConfig(devConfig),
    };
  }

Step 6: Run the test harness

Start the webpack dev server with the test harness environment variable:

bash
IN_HARNESS=true npx webpack serve --config webpack.config.ts --mode development

Step 7: View your app in the test harness

Open your browser and navigate to http://localhost:8080/harness.html

You should now see your app running in the test harness environment.

Troubleshooting

  • Webpack configuration issues

    • Check that your webpack configuration correctly handles the IN_HARNESS environment variable.
    • Check that the entry points for harness files are correctly configured.
  • Path issues

    • Check that the import path in harness/harness.tsx correctly points to your app's main component.
    • Check that the hot module replacement path matches your app's main component path.
  • Dependencies

    • Check that you have all required Canva SDK packages installed:
      • @canva/app-ui-kit
      • @canva/app-i18n-kit
      • @canva/design
      • @canva/asset
      • @canva/user
      • @canva/platform

Cleanup

When you're done testing, you can stop the webpack dev server and remove the harness artifacts. Alternatively, you can add the harness artifacts to your .gitignore file.

bash
# Stop the webpack dev server (Ctrl+C)
# Then remove the harness directories
rm -rf harness public/harness.html

Additional resources

  • Unit testing
  • Canva Dev MCP server
  • React documentation
  • Webpack documentation

Canva Developer Documentation SOP Site