@canva/cli 0.0.1-beta.8 → 0.0.1-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canva/cli",
3
- "version": "0.0.1-beta.8",
3
+ "version": "0.0.1-beta.9",
4
4
  "description": "The official Canva CLI.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "Canva Pty Ltd.",
@@ -6,12 +6,12 @@
6
6
  "license": "SEE LICENSE IN LICENSE.md",
7
7
  "author": "Canva Pty Ltd.",
8
8
  "dependencies": {
9
- "@canva/app-ui-kit": "^4.3.0",
10
- "@canva/asset": "^2.0.0",
11
- "@canva/design": "^2.2.1",
12
- "@canva/error": "^2.0.0",
13
- "@canva/platform": "^2.0.0",
14
- "@canva/user": "^2.0.0",
9
+ "@canva/app-ui-kit": "^4.4.0",
10
+ "@canva/asset": "^2.1.0",
11
+ "@canva/design": "^2.3.0",
12
+ "@canva/error": "^2.1.0",
13
+ "@canva/platform": "^2.1.0",
14
+ "@canva/user": "^2.1.0",
15
15
  "cookie-parser": "1.4.7",
16
16
  "react": "18.3.1",
17
17
  "react-dom": "18.3.1"
@@ -25,7 +25,7 @@
25
25
  "@types/jest": "29.5.14",
26
26
  "@types/jsonwebtoken": "9.0.7",
27
27
  "@types/node": "20.10.0",
28
- "@types/node-fetch": "2.6.11",
28
+ "@types/node-fetch": "2.6.12",
29
29
  "@types/node-forge": "1.3.11",
30
30
  "@types/nodemon": "1.19.6",
31
31
  "@types/prompts": "2.4.9",
@@ -37,10 +37,10 @@
37
37
  "css-loader": "7.1.2",
38
38
  "css-modules-typescript-loader": "4.0.1",
39
39
  "cssnano": "7.0.6",
40
- "debug": "4.3.7",
41
- "dotenv": "16.4.5",
40
+ "debug": "4.4.0",
41
+ "dotenv": "16.4.7",
42
42
  "exponential-backoff": "3.1.1",
43
- "express": "4.21.1",
43
+ "express": "4.21.2",
44
44
  "express-basic-auth": "1.2.1",
45
45
  "jest": "29.7.0",
46
46
  "jsonwebtoken": "9.0.2",
@@ -50,7 +50,7 @@
50
50
  "node-forge": "1.3.1",
51
51
  "nodemon": "3.0.1",
52
52
  "postcss-loader": "8.1.1",
53
- "prettier": "3.3.3",
53
+ "prettier": "3.4.2",
54
54
  "prompts": "2.4.2",
55
55
  "style-loader": "4.0.0",
56
56
  "terser-webpack-plugin": "5.3.10",
@@ -59,7 +59,7 @@
59
59
  "ts-node": "10.9.2",
60
60
  "typescript": "5.5.4",
61
61
  "url-loader": "4.1.1",
62
- "webpack": "5.96.1",
62
+ "webpack": "5.97.1",
63
63
  "webpack-cli": "5.1.4",
64
64
  "webpack-dev-server": "5.1.0",
65
65
  "yargs": "17.7.2"
@@ -4,6 +4,7 @@ const TerserPlugin = require("terser-webpack-plugin");
4
4
  const { DefinePlugin, optimize } = require("webpack");
5
5
  const chalk = require("chalk");
6
6
  const { transform } = require("@formatjs/ts-transformer");
7
+ const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
7
8
 
8
9
  /**
9
10
  *
@@ -173,7 +174,8 @@ function buildConfig({
173
174
  }),
174
175
  // Apps can only submit a single JS file via the developer portal
175
176
  new optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
176
- ],
177
+ mode === "development" && new ReactRefreshWebpackPlugin(),
178
+ ].filter(Boolean),
177
179
  ...buildDevConfig(devConfig),
178
180
  };
179
181
  }
@@ -1,9 +1,11 @@
1
1
  import formatjs from "eslint-plugin-formatjs";
2
+ import eslintLocalI18nRules from "./eslint-local-i18n-rules/index.mjs";
2
3
 
3
4
  export default [
4
5
  {
5
6
  plugins: {
6
7
  formatjs,
8
+ "local-i18n-rules": eslintLocalI18nRules,
7
9
  },
8
10
  rules: {
9
11
  "formatjs/no-invalid-icu": "error",
@@ -33,6 +35,7 @@ export default [
33
35
  "formatjs/no-offset": "error",
34
36
  "formatjs/blocklist-elements": [2, ["selectordinal"]],
35
37
  "formatjs/no-complex-selectors": "error",
38
+ "local-i18n-rules/enforce-object-property-translation": ["warn"],
36
39
  },
37
40
  },
38
41
  ];
@@ -0,0 +1,181 @@
1
+ /**
2
+ * ESLint rule that identifies and flags untranslated user-facing strings in object properties.
3
+ *
4
+ * This rule helps maintain internationalization consistency by detecting untranslated
5
+ * strings in specific object properties (default: 'label'). It suggests using
6
+ * intl.formatMessage for proper translation.
7
+ *
8
+ * Note: The rule is currently implemented as a local rule, with plans to publish as
9
+ * an npm package to make it available to the broader development community.
10
+ *
11
+ * @example
12
+ * // ❌ Incorrect - Untranslated strings
13
+ * const options = [
14
+ * { value: "inbox", label: "Inbox" },
15
+ * { value: "starred", label: "Starred messages" },
16
+ * { value: "spam", label: "Spam folder" }
17
+ * ];
18
+ *
19
+ * // ✅ Correct - Using intl.formatMessage with descriptions
20
+ * const options = [
21
+ * {
22
+ * value: "inbox",
23
+ * label: intl.formatMessage({
24
+ * defaultMessage: "Inbox",
25
+ * description: "Label for main message inbox folder option"
26
+ * })
27
+ * },
28
+ * {
29
+ * value: "starred",
30
+ * label: intl.formatMessage({
31
+ * defaultMessage: "Starred messages",
32
+ * description: "Label for folder containing messages marked as important"
33
+ * })
34
+ * },
35
+ * {
36
+ * value: "spam",
37
+ * label: intl.formatMessage({
38
+ * defaultMessage: "Spam folder",
39
+ * description: "Label for folder containing filtered spam messages"
40
+ * })
41
+ * }
42
+ * ];
43
+ *
44
+ * @see https://www.canva.dev/docs/apps/localization/
45
+ */
46
+ export default {
47
+ rules: {
48
+ "enforce-object-property-translation": {
49
+ meta: {
50
+ type: "problem",
51
+ docs: {
52
+ description:
53
+ "Enforce translation of specific properties using intl.formatMessage",
54
+ category: "Possible Errors",
55
+ recommended: true,
56
+ },
57
+ fixable: "code",
58
+ schema: [
59
+ {
60
+ type: "object",
61
+ properties: {
62
+ properties: {
63
+ type: "array",
64
+ items: { type: "string" },
65
+ default: ["label"],
66
+ },
67
+ intlObjectName: {
68
+ type: "string",
69
+ default: "intl",
70
+ },
71
+ },
72
+ additionalProperties: false,
73
+ },
74
+ ],
75
+ messages: {
76
+ untranslatedProperty: `If "{{ originalMessage }}" is a user-facing string, you should translate it using "intl.formatMessage". See https://www.canva.dev/docs/apps/localization/.`,
77
+ },
78
+ },
79
+ create(context) {
80
+ const config = context.options[0] || {};
81
+ const propertiesToCheck = config.properties || ["label"];
82
+ const intlObjectName = config.intlObjectName || "intl";
83
+
84
+ function getTemplateLiteralString(node) {
85
+ const src = context.getSourceCode();
86
+ return src.getText(node);
87
+ }
88
+
89
+ // Extract string content from different node types
90
+ function extractStringContent(node) {
91
+ if (!node) return [];
92
+
93
+ switch (node.type) {
94
+ // label: "Foo"
95
+ case "Literal":
96
+ return typeof node.value === "string"
97
+ ? [{ node, value: node.value }]
98
+ : [];
99
+
100
+ // label: `Foo ${bar}`
101
+ case "TemplateLiteral":
102
+ return [{ node, value: getTemplateLiteralString(node) }];
103
+ // label: foo || "Bar"
104
+ case "LogicalExpression": {
105
+ if (node.operator === "||") {
106
+ return [
107
+ ...extractStringContent(node.left),
108
+ ...extractStringContent(node.right),
109
+ ];
110
+ }
111
+ return [];
112
+ }
113
+ // label: "Foo" + "Bar" + "Baz"
114
+ case "BinaryExpression":
115
+ if (node.operator === "+") {
116
+ return [
117
+ ...extractStringContent(node.left),
118
+ ...extractStringContent(node.right),
119
+ ];
120
+ }
121
+ return [];
122
+ // label: foo ? "Foo" : "Bar"
123
+ case "ConditionalExpression":
124
+ return [
125
+ ...extractStringContent(node.consequent),
126
+ ...extractStringContent(node.alternate),
127
+ ];
128
+
129
+ default:
130
+ return [];
131
+ }
132
+ }
133
+
134
+ function isTranslated(node) {
135
+ return (
136
+ node.parent.type === "CallExpression" &&
137
+ node.parent.callee.type === "MemberExpression" &&
138
+ node.parent.callee.object.name === intlObjectName &&
139
+ node.parent.callee.property.name === "formatMessage"
140
+ );
141
+ }
142
+
143
+ return {
144
+ Property(node) {
145
+ const keyName = node.key.name || node.key.value;
146
+ if (propertiesToCheck.includes(keyName)) {
147
+ const results = extractStringContent(node.value);
148
+ if (!results) return;
149
+ results.forEach((result) => {
150
+ const { node: stringNode, value: stringValue } = result;
151
+
152
+ if (!isTranslated(stringNode)) {
153
+ context.report({
154
+ node: stringNode,
155
+ messageId: "untranslatedProperty",
156
+ data: {
157
+ property: keyName,
158
+ originalMessage:
159
+ stringValue.length > 40
160
+ ? stringValue.split(" ").slice(0, 4).join(" ") + "..."
161
+ : stringValue,
162
+ intlObjectName,
163
+ },
164
+ fix(fixer) {
165
+ const newText = `${intlObjectName}.formatMessage({
166
+ defaultMessage: ${JSON.stringify(stringValue)},
167
+ // TODO: Provide a meaningful description for translators
168
+ description: ""
169
+ })`;
170
+ return fixer.replaceText(stringNode, newText);
171
+ },
172
+ });
173
+ }
174
+ });
175
+ }
176
+ },
177
+ };
178
+ },
179
+ },
180
+ },
181
+ };
@@ -1,8 +1,35 @@
1
+ import { pathsToModuleNameMapper } from "ts-jest";
2
+ import tsconfig from "./tsconfig.json" assert { type: "json" };
3
+
4
+ const { compilerOptions } = tsconfig;
5
+
1
6
  /** @type {import('ts-jest').JestConfigWithTsJest} */
2
7
 
3
8
  export default {
4
9
  preset: "ts-jest",
5
- testEnvironment: "node",
6
- testRegex: "(/tests/.*|(\\.|/)(tests))\\.ts?$",
10
+ testEnvironment: "jsdom",
11
+ testRegex: "(/tests/.*|(\\.|/)(tests))\\.tsx?$",
7
12
  modulePathIgnorePatterns: ["./internal/", "./node_modules/"],
13
+ modulePaths: [compilerOptions.baseUrl],
14
+ moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths),
15
+ transform: {
16
+ ".+\\.(css)$": "jest-css-modules-transform",
17
+ "^.+\\.tsx?$": [
18
+ "ts-jest",
19
+ {
20
+ astTransformers: {
21
+ before: [
22
+ {
23
+ path: "@formatjs/ts-transformer/ts-jest-integration",
24
+ options: {
25
+ overrideIdFn: "[sha512:contenthash:base64:6]",
26
+ ast: true,
27
+ },
28
+ },
29
+ ],
30
+ },
31
+ },
32
+ ],
33
+ },
34
+ setupFiles: ["<rootDir>/jest.setup.ts"],
8
35
  };
@@ -0,0 +1,19 @@
1
+ // Import testing sub-packages
2
+ import * as asset from "@canva/asset/test";
3
+ import * as design from "@canva/design/test";
4
+ import * as error from "@canva/error/test";
5
+ import * as platform from "@canva/platform/test";
6
+ import * as user from "@canva/user/test";
7
+
8
+ // Initialize the test environments
9
+ asset.initTestEnvironment();
10
+ design.initTestEnvironment();
11
+ error.initTestEnvironment();
12
+ platform.initTestEnvironment();
13
+ user.initTestEnvironment();
14
+
15
+ // Once they're initialized, mock the SDKs
16
+ jest.mock("@canva/asset");
17
+ jest.mock("@canva/design");
18
+ jest.mock("@canva/platform");
19
+ jest.mock("@canva/user");
@@ -18,12 +18,13 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@canva/app-components": "^1.0.0-beta.29",
21
- "@canva/app-i18n-kit": "^1.0.1",
22
- "@canva/app-ui-kit": "^4.3.0",
23
- "@canva/asset": "^2.0.0",
24
- "@canva/design": "^2.2.1",
25
- "@canva/platform": "^2.0.0",
26
- "@canva/user": "^2.0.0",
21
+ "@canva/app-i18n-kit": "^1.0.2",
22
+ "@canva/app-ui-kit": "^4.4.0",
23
+ "@canva/asset": "^2.1.0",
24
+ "@canva/design": "^2.3.0",
25
+ "@canva/error": "^2.1.0",
26
+ "@canva/platform": "^2.1.0",
27
+ "@canva/user": "^2.1.0",
27
28
  "cookie-parser": "1.4.7",
28
29
  "cors": "2.8.5",
29
30
  "react": "18.3.1",
@@ -31,12 +32,14 @@
31
32
  "react-intl": "6.8.7"
32
33
  },
33
34
  "devDependencies": {
34
- "@eslint/eslintrc": "3.1.0",
35
- "@eslint/js": "9.14.0",
36
- "@formatjs/cli": "6.3.8",
37
- "@formatjs/ts-transformer": "3.13.22",
35
+ "@eslint/eslintrc": "3.2.0",
36
+ "@eslint/js": "9.16.0",
37
+ "@formatjs/cli": "6.3.14",
38
+ "@formatjs/ts-transformer": "3.13.26",
38
39
  "@ngrok/ngrok": "1.4.1",
40
+ "@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
39
41
  "@svgr/webpack": "8.1.0",
42
+ "@testing-library/react": "16.1.0",
40
43
  "@types/cors": "2.8.17",
41
44
  "@types/debug": "4.1.12",
42
45
  "@types/express": "4.17.21",
@@ -44,30 +47,32 @@
44
47
  "@types/jest": "29.5.14",
45
48
  "@types/jsonwebtoken": "9.0.7",
46
49
  "@types/node": "20.10.0",
47
- "@types/node-fetch": "2.6.11",
50
+ "@types/node-fetch": "2.6.12",
48
51
  "@types/node-forge": "1.3.11",
49
52
  "@types/nodemon": "1.19.6",
50
53
  "@types/prompts": "2.4.9",
51
54
  "@types/react": "18.3.12",
52
55
  "@types/react-dom": "18.3.1",
53
56
  "@types/webpack-env": "1.18.5",
54
- "@typescript-eslint/eslint-plugin": "8.13.0",
55
- "@typescript-eslint/parser": "8.13.0",
57
+ "@typescript-eslint/eslint-plugin": "8.18.0",
58
+ "@typescript-eslint/parser": "8.18.0",
56
59
  "chalk": "4.1.2",
57
60
  "cli-table3": "0.6.5",
58
61
  "css-loader": "7.1.2",
59
62
  "css-modules-typescript-loader": "4.0.1",
60
63
  "cssnano": "7.0.6",
61
- "debug": "4.3.7",
62
- "dotenv": "16.4.5",
63
- "eslint": "9.14.0",
64
- "eslint-plugin-formatjs": "5.2.2",
64
+ "debug": "4.4.0",
65
+ "dotenv": "16.4.7",
66
+ "eslint": "9.16.0",
67
+ "eslint-plugin-formatjs": "5.2.8",
65
68
  "eslint-plugin-jest": "28.9.0",
66
69
  "eslint-plugin-react": "7.37.2",
67
70
  "exponential-backoff": "3.1.1",
68
- "express": "4.21.1",
71
+ "express": "4.21.2",
69
72
  "express-basic-auth": "1.2.1",
70
73
  "jest": "29.7.0",
74
+ "jest-css-modules-transform": "4.4.2",
75
+ "jest-environment-jsdom": "29.7.0",
71
76
  "jsonwebtoken": "9.0.2",
72
77
  "jwks-rsa": "3.1.0",
73
78
  "mini-css-extract-plugin": "2.9.2",
@@ -75,8 +80,9 @@
75
80
  "node-forge": "1.3.1",
76
81
  "nodemon": "3.0.1",
77
82
  "postcss-loader": "8.1.1",
78
- "prettier": "3.3.3",
83
+ "prettier": "3.4.2",
79
84
  "prompts": "2.4.2",
85
+ "react-refresh": "0.16.0",
80
86
  "style-loader": "4.0.0",
81
87
  "terser-webpack-plugin": "5.3.10",
82
88
  "ts-jest": "29.2.5",
@@ -84,7 +90,7 @@
84
90
  "ts-node": "10.9.2",
85
91
  "typescript": "5.5.4",
86
92
  "url-loader": "4.1.1",
87
- "webpack": "5.96.1",
93
+ "webpack": "5.97.1",
88
94
  "webpack-cli": "5.1.4",
89
95
  "webpack-dev-server": "5.1.0",
90
96
  "yargs": "17.7.2"
@@ -4,6 +4,7 @@ const TerserPlugin = require("terser-webpack-plugin");
4
4
  const { DefinePlugin, optimize } = require("webpack");
5
5
  const chalk = require("chalk");
6
6
  const { transform } = require("@formatjs/ts-transformer");
7
+ const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
7
8
 
8
9
  /**
9
10
  *
@@ -173,7 +174,8 @@ function buildConfig({
173
174
  }),
174
175
  // Apps can only submit a single JS file via the developer portal
175
176
  new optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
176
- ],
177
+ mode === "development" && new ReactRefreshWebpackPlugin(),
178
+ ].filter(Boolean),
177
179
  ...buildDevConfig(devConfig),
178
180
  };
179
181
  }
@@ -17,16 +17,17 @@
17
17
  "postinstall": "ts-node ./scripts/copy-env.ts"
18
18
  },
19
19
  "dependencies": {
20
- "@canva/app-i18n-kit": "^1.0.1",
21
- "@canva/app-ui-kit": "^4.3.0",
22
- "@canva/asset": "^2.0.0",
23
- "@canva/design": "^2.2.1",
24
- "@canva/platform": "^2.0.0",
25
- "@canva/user": "^2.0.0",
20
+ "@canva/app-i18n-kit": "^1.0.2",
21
+ "@canva/app-ui-kit": "^4.4.0",
22
+ "@canva/asset": "^2.1.0",
23
+ "@canva/design": "^2.3.0",
24
+ "@canva/error": "^2.1.0",
25
+ "@canva/platform": "^2.1.0",
26
+ "@canva/user": "^2.1.0",
26
27
  "cookie-parser": "1.4.7",
27
28
  "cors": "2.8.5",
28
- "html-react-parser": "5.1.18",
29
- "obscenity": "0.4.0",
29
+ "html-react-parser": "5.2.0",
30
+ "obscenity": "0.4.1",
30
31
  "react": "18.3.1",
31
32
  "react-dom": "18.3.1",
32
33
  "react-error-boundary": "4.1.2",
@@ -34,42 +35,46 @@
34
35
  "react-router-dom": "6.28.0"
35
36
  },
36
37
  "devDependencies": {
37
- "@eslint/eslintrc": "3.1.0",
38
- "@eslint/js": "9.14.0",
39
- "@formatjs/cli": "6.3.8",
40
- "@formatjs/ts-transformer": "3.13.22",
38
+ "@eslint/eslintrc": "3.2.0",
39
+ "@eslint/js": "9.16.0",
40
+ "@formatjs/cli": "6.3.14",
41
+ "@formatjs/ts-transformer": "3.13.26",
41
42
  "@ngrok/ngrok": "1.4.1",
43
+ "@pmmmwh/react-refresh-webpack-plugin": "0.5.15",
42
44
  "@svgr/webpack": "8.1.0",
45
+ "@testing-library/react": "16.1.0",
43
46
  "@types/debug": "4.1.12",
44
47
  "@types/express": "4.17.21",
45
48
  "@types/express-serve-static-core": "4.19.6",
46
49
  "@types/jest": "29.5.14",
47
50
  "@types/jsonwebtoken": "9.0.7",
48
51
  "@types/node": "20.10.0",
49
- "@types/node-fetch": "2.6.11",
52
+ "@types/node-fetch": "2.6.12",
50
53
  "@types/node-forge": "1.3.11",
51
54
  "@types/nodemon": "1.19.6",
52
55
  "@types/prompts": "2.4.9",
53
56
  "@types/react": "18.3.12",
54
57
  "@types/react-dom": "18.3.1",
55
58
  "@types/webpack-env": "1.18.5",
56
- "@typescript-eslint/eslint-plugin": "8.13.0",
57
- "@typescript-eslint/parser": "8.13.0",
59
+ "@typescript-eslint/eslint-plugin": "8.18.0",
60
+ "@typescript-eslint/parser": "8.18.0",
58
61
  "chalk": "4.1.2",
59
62
  "cli-table3": "0.6.5",
60
63
  "css-loader": "7.1.2",
61
64
  "css-modules-typescript-loader": "4.0.1",
62
65
  "cssnano": "7.0.6",
63
- "debug": "4.3.7",
64
- "dotenv": "16.4.5",
65
- "eslint": "9.14.0",
66
- "eslint-plugin-formatjs": "5.2.2",
66
+ "debug": "4.4.0",
67
+ "dotenv": "16.4.7",
68
+ "eslint": "9.16.0",
69
+ "eslint-plugin-formatjs": "5.2.8",
67
70
  "eslint-plugin-jest": "28.9.0",
68
71
  "eslint-plugin-react": "7.37.2",
69
72
  "exponential-backoff": "3.1.1",
70
- "express": "4.21.1",
73
+ "express": "4.21.2",
71
74
  "express-basic-auth": "1.2.1",
72
75
  "jest": "29.7.0",
76
+ "jest-css-modules-transform": "4.4.2",
77
+ "jest-environment-jsdom": "29.7.0",
73
78
  "jsonwebtoken": "9.0.2",
74
79
  "jwks-rsa": "3.1.0",
75
80
  "mini-css-extract-plugin": "2.9.2",
@@ -77,8 +82,9 @@
77
82
  "node-forge": "1.3.1",
78
83
  "nodemon": "3.0.1",
79
84
  "postcss-loader": "8.1.1",
80
- "prettier": "3.3.3",
85
+ "prettier": "3.4.2",
81
86
  "prompts": "2.4.2",
87
+ "react-refresh": "0.16.0",
82
88
  "style-loader": "4.0.0",
83
89
  "terser-webpack-plugin": "5.3.10",
84
90
  "ts-jest": "29.2.5",
@@ -86,7 +92,7 @@
86
92
  "ts-node": "10.9.2",
87
93
  "typescript": "5.5.4",
88
94
  "url-loader": "4.1.1",
89
- "webpack": "5.96.1",
95
+ "webpack": "5.97.1",
90
96
  "webpack-cli": "5.1.4",
91
97
  "webpack-dev-server": "5.1.0",
92
98
  "yargs": "17.7.2"
@@ -1,13 +1,19 @@
1
- import { Outlet } from "react-router-dom";
2
- import { Rows } from "@canva/app-ui-kit";
3
- import { Footer } from "./components";
4
- import * as styles from "styles/components.css";
1
+ import { createHashRouter, RouterProvider } from "react-router-dom";
2
+ import { ContextProvider } from "./context";
3
+ import { routes } from "./routes";
4
+ import { AppI18nProvider } from "@canva/app-i18n-kit";
5
+ import { AppUiProvider } from "@canva/app-ui-kit";
6
+ import { ErrorBoundary } from "react-error-boundary";
7
+ import { ErrorPage } from "./pages";
5
8
 
6
9
  export const App = () => (
7
- <div className={styles.scrollContainer}>
8
- <Rows spacing="3u">
9
- <Outlet />
10
- <Footer />
11
- </Rows>
12
- </div>
10
+ <AppI18nProvider>
11
+ <AppUiProvider>
12
+ <ErrorBoundary fallback={<ErrorPage />}>
13
+ <ContextProvider>
14
+ <RouterProvider router={createHashRouter(routes)} />
15
+ </ContextProvider>
16
+ </ErrorBoundary>
17
+ </AppUiProvider>
18
+ </AppI18nProvider>
13
19
  );
@@ -0,0 +1,43 @@
1
+ /* eslint-disable formatjs/no-literal-string-in-jsx */
2
+ import { TestAppUiProvider } from "@canva/app-ui-kit";
3
+ import { TestAppI18nProvider } from "@canva/app-i18n-kit";
4
+ import type { RenderResult } from "@testing-library/react";
5
+ import { fireEvent, render } from "@testing-library/react";
6
+ import React from "react";
7
+ import { RemainingCredits } from "../remaining_credits";
8
+ import { requestOpenExternalUrl } from "@canva/platform";
9
+
10
+ function renderInTestProvider(node: React.ReactNode): RenderResult {
11
+ return render(
12
+ // In a test environment, you should wrap your apps in `TestAppI18nProvider` and `TestAppUiProvider`, rather than `AppI18nProvider` and `AppUiProvider`
13
+ <TestAppI18nProvider>
14
+ <TestAppUiProvider>{node}</TestAppUiProvider>,
15
+ </TestAppI18nProvider>,
16
+ );
17
+ }
18
+
19
+ // This test demonstrates how to test code that uses functions from the Canva Apps SDK
20
+ // For more information on testing with the Canva Apps SDK, see https://www.canva.dev/docs/apps/testing/
21
+ describe("Remaining Credit Tests", () => {
22
+ const mockRequestOpenExternalUrl = jest.mocked(requestOpenExternalUrl);
23
+
24
+ beforeEach(() => {
25
+ jest.resetAllMocks();
26
+ });
27
+
28
+ it("should call requestOpenExternalUrl when the link is clicked", () => {
29
+ // assert that the mock is in the expected clean state
30
+ expect(mockRequestOpenExternalUrl).not.toHaveBeenCalled();
31
+
32
+ const result = renderInTestProvider(<RemainingCredits />);
33
+
34
+ // get a reference to the link to purchase more credits
35
+ const purchaseMoreLink = result.getByRole("button");
36
+
37
+ // programmatically simulate clicking the button
38
+ fireEvent.click(purchaseMoreLink);
39
+
40
+ // we expect that requestOpenExternalUrl has been called
41
+ expect(mockRequestOpenExternalUrl).toHaveBeenCalled();
42
+ });
43
+ });
@@ -0,0 +1,13 @@
1
+ import { Outlet } from "react-router-dom";
2
+ import { Rows } from "@canva/app-ui-kit";
3
+ import { Footer } from "./components";
4
+ import * as styles from "styles/components.css";
5
+
6
+ export const Home = () => (
7
+ <div className={styles.scrollContainer}>
8
+ <Rows spacing="3u">
9
+ <Outlet />
10
+ <Footer />
11
+ </Rows>
12
+ </div>
13
+ );