Appearance
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.
Recommended tooling
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.
Update to the latest version of the SDK packages:
bashnpm 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@latestCreate a
jest.setup.tsfile in the root of your project:bashtouch jest.setup.tsCopy 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/errorpackage doesn't need to be mocked.In the
jest.config.jsfile, add the followingsetupFilesproperty:tsxsetupFiles: ["<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
Create a file with a
.tests.tsxsuffix or place the file in a folder named "tests":bash# This works: touch src/app.tests.tsx # This also works: touch src/tests/app.tsxImport the
renderfunction from React Testing Library:tsximport { render } from "@testing-library/react";Import the component to be tested, such as the
Appcomponent:tsximport { App } from "./app";Import the following providers to wrap around the component during testing:
tsximport { TestAppI18nProvider } from "@canva/app-i18n-kit"; import { TestAppUiProvider } from "@canva/app-ui-kit";For example:
tsxconst result = render( <TestAppI18nProvider> <TestAppUiProvider> <App /> </TestAppUiProvider> </TestAppI18nProvider> );To reduce boilerplate, we recommend creating the following helper function:
tsxfunction renderInTestProvider(node: React.ReactNode) { return render( <TestAppI18nProvider> <TestAppUiProvider>{node}</TestAppUiProvider> </TestAppI18nProvider> ); }This can then be used instead of the standard
renderfunction:tsxconst result = renderInTestProvider(<App />);Note: You only need to wrap the
TestAppI18nProviderprovider around localized components, but there's no downside to always including it.Use standard Jest syntax and matchers to test and interact with the component:
tsximport { 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:
Import the methods that will be called during the test:
tsximport { requestOpenExternalUrl } from "@canva/platform";Create mocked versions of the methods:
tsxconst 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.
If the method is asynchronous — and most of the SDK methods are — mock the resolved or rejected value:
tsxconst 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" }); });Use standard React Testing Library features, such as the
fireEventmethod, to interact with the component:tsximport { render, fireEvent } from "@testing-library/react"; const button = result.getByRole("button", { name: "Click here" }); await fireEvent.click(button);Use standard Jest matchers to test if (and how) the methods have been called:
tsxconst 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", }); });Before each test, reset all mocks:
tsxbeforeEach(() => { 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 testTo automatically re-run tests as the code changes, run the following command:
bash
npm run test:watchExample: 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 publicStep 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 developmentStep 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_HARNESSenvironment variable. - Check that the entry points for harness files are correctly configured.
- Check that your webpack configuration correctly handles the
Path issues
- Check that the import path in
harness/harness.tsxcorrectly points to your app's main component. - Check that the hot module replacement path matches your app's main component path.
- Check that the import path in
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
- Check that you have all required Canva SDK packages installed:
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.htmlAdditional resources
- Unit testing
- Canva Dev MCP server
- React documentation
- Webpack documentation