From 6bedfaaad92fba278a7b3778b345a30e4394076e Mon Sep 17 00:00:00 2001 From: Filip Satek Date: Thu, 15 Aug 2024 16:05:11 +0200 Subject: [PATCH] feat: add @ima/testing-library --- package-lock.json | 204 ++++++++++-------- package.json | 1 + packages/core/src/boot.ts | 2 +- packages/react-page-renderer/package.json | 2 +- packages/server/types.d.ts | 2 + packages/testing-library/.npmignore | 4 + packages/testing-library/LICENSE | 21 ++ packages/testing-library/README.md | 106 +++++++++ packages/testing-library/jest.config.js | 6 + packages/testing-library/package.json | 48 +++++ packages/testing-library/setupJest.js | 0 .../testing-library/src/app/config/bind.ts | 53 +++++ .../src/app/config/settings.ts | 45 ++++ packages/testing-library/src/app/main.ts | 15 ++ packages/testing-library/src/configuration.ts | 57 +++++ packages/testing-library/src/helpers.ts | 11 + packages/testing-library/src/index.ts | 2 + packages/testing-library/src/jest-preset.ts | 76 +++++++ .../src/jestSetupFileAfterEnv.ts | 6 + packages/testing-library/src/localization.ts | 80 +++++++ packages/testing-library/src/rtl.tsx | 96 +++++++++ packages/testing-library/tsconfig.json | 17 ++ 22 files changed, 759 insertions(+), 95 deletions(-) create mode 100644 packages/testing-library/.npmignore create mode 100644 packages/testing-library/LICENSE create mode 100644 packages/testing-library/README.md create mode 100644 packages/testing-library/jest.config.js create mode 100644 packages/testing-library/package.json create mode 100644 packages/testing-library/setupJest.js create mode 100644 packages/testing-library/src/app/config/bind.ts create mode 100644 packages/testing-library/src/app/config/settings.ts create mode 100644 packages/testing-library/src/app/main.ts create mode 100644 packages/testing-library/src/configuration.ts create mode 100644 packages/testing-library/src/helpers.ts create mode 100644 packages/testing-library/src/index.ts create mode 100644 packages/testing-library/src/jest-preset.ts create mode 100644 packages/testing-library/src/jestSetupFileAfterEnv.ts create mode 100644 packages/testing-library/src/localization.ts create mode 100644 packages/testing-library/src/rtl.tsx create mode 100644 packages/testing-library/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 755e5950f2..74fb4d0211 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "./packages/devtools", "./packages/error-overlay", "./packages/react-page-renderer", + "./packages/testing-library", "./packages/storybook-integration", "./website" ], @@ -100,6 +101,13 @@ "node": ">=0.10.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "license": "MIT", + "peer": true + }, "node_modules/@algolia/autocomplete-core": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz", @@ -4938,6 +4946,10 @@ "resolved": "packages/storybook-integration", "link": true }, + "node_modules/@ima/testing-library": { + "resolved": "packages/testing-library", + "link": true + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -7758,29 +7770,30 @@ } }, "node_modules/@testing-library/dom": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", - "integrity": "sha512-0DGPd9AR3+iDTjGoMpxIkAsUihHZ3Ai6CneU6bRRrffXMgzCdlNk43jTrD2/5LT6CBb3MWTP8v510JzYtahD2w==", - "dev": true, + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", + "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/@testing-library/dom/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -7792,7 +7805,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -7802,22 +7815,74 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz", + "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "license": "MIT", + "peer": true + }, "node_modules/@testing-library/react": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", - "integrity": "sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg==", - "dev": true, + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", + "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^9.0.0", - "@types/react-dom": "^18.0.0" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, "node_modules/@tootallnate/once": { @@ -7841,7 +7906,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", - "dev": true + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.1", @@ -8293,7 +8358,7 @@ "version": "18.2.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -9780,12 +9845,13 @@ } }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/arr-diff": { @@ -12914,6 +12980,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "license": "MIT", + "peer": true + }, "node_modules/cssdb": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.7.2.tgz", @@ -13845,35 +13918,6 @@ } } }, - "node_modules/deep-equal": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.2.tgz", - "integrity": "sha512-xjVyBf0w5vH0I42jdAZzOKVldmPgSulmiyPRywoyq7HXC9qdgo17kxJE+rdnif5Tz6+pIrpJI8dCpMNLIGkUiA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.1", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.0", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -14019,7 +14063,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -14183,7 +14226,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true + "peer": true }, "node_modules/dom-converter": { "version": "0.2.0", @@ -14674,26 +14717,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-iterator-helpers": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.14.tgz", @@ -21634,7 +21657,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -22087,7 +22110,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, "engines": { "node": ">=4" } @@ -26154,8 +26176,7 @@ "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, "node_modules/react-json-tree": { "version": "0.18.0", @@ -26545,7 +26566,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -29170,18 +29190,6 @@ "graceful-fs": "^4.1.3" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", @@ -29424,7 +29432,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, "dependencies": { "min-indent": "^1.0.0" }, @@ -34023,7 +34030,7 @@ }, "devDependencies": { "@cfaester/enzyme-adapter-react-18": "^0.7.0", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.0.0", "@types/react": "^18.0.33", "@types/react-dom": "^18.0.6", "@types/webpack-env": "^1.16.3", @@ -34081,6 +34088,17 @@ "webpack": ">=5.x" } }, + "packages/testing-library": { + "version": "19.6.0", + "license": "MIT", + "peerDependencies": { + "@ima/core": ">=19.0.0", + "@ima/react-page-renderer": ">=19.0.0", + "@testing-library/dom": ">=10.0.0", + "@testing-library/jest-dom": ">=6.0.0", + "@testing-library/react": ">=16.0.0" + } + }, "website": { "name": "@ima/docs", "version": "0.0.0", diff --git a/package.json b/package.json index ce9249abbf..e1a3fce2ba 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "./packages/devtools", "./packages/error-overlay", "./packages/react-page-renderer", + "./packages/testing-library", "./packages/storybook-integration", "./website" ], diff --git a/packages/core/src/boot.ts b/packages/core/src/boot.ts index eefb797d73..070b456f40 100644 --- a/packages/core/src/boot.ts +++ b/packages/core/src/boot.ts @@ -55,7 +55,7 @@ export interface Resources { */ export interface Environment { [key: string]: unknown; - $Debug: GlobalImaObject['$Version']; + $Debug: GlobalImaObject['$Debug']; $Language: Record; $Version: GlobalImaObject['$Version']; $App: GlobalImaObject['$App']; diff --git a/packages/react-page-renderer/package.json b/packages/react-page-renderer/package.json index 33b2d19536..d7150ba79b 100644 --- a/packages/react-page-renderer/package.json +++ b/packages/react-page-renderer/package.json @@ -65,7 +65,7 @@ }, "devDependencies": { "@cfaester/enzyme-adapter-react-18": "^0.7.0", - "@testing-library/react": "^14.0.0", + "@testing-library/react": "^16.0.0", "@types/react": "^18.0.33", "@types/react-dom": "^18.0.6", "@types/webpack-env": "^1.16.3", diff --git a/packages/server/types.d.ts b/packages/server/types.d.ts index 8919b4409a..8a92aba009 100644 --- a/packages/server/types.d.ts +++ b/packages/server/types.d.ts @@ -23,6 +23,8 @@ declare module '@ima/server' { } export function createIMAServer(params: { + applicationFolder?: string; + processEnvironment?: (environment: Environment) => Environment; environment?: Environment; logger?: any; emitter?: Emitter; diff --git a/packages/testing-library/.npmignore b/packages/testing-library/.npmignore new file mode 100644 index 0000000000..00d1f7e29d --- /dev/null +++ b/packages/testing-library/.npmignore @@ -0,0 +1,4 @@ +* +!dist/**/* +!package.json +!jest-preset.js diff --git a/packages/testing-library/LICENSE b/packages/testing-library/LICENSE new file mode 100644 index 0000000000..a613dddbf1 --- /dev/null +++ b/packages/testing-library/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 Seznam.cz a.s. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/testing-library/README.md b/packages/testing-library/README.md new file mode 100644 index 0000000000..6d54ce7bdd --- /dev/null +++ b/packages/testing-library/README.md @@ -0,0 +1,106 @@ +

+ +

+ +

@ima/testing-library

+

Testing library for IMA.js applications. +

+ +--- + +## IMA Testing Library + +The `@ima/testing-library` contains utilities for testing IMA.js applications. It provides integration with [Jest](https://jestjs.io), [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) (RTL for short) amd [Testing Library Jest DOM](https://testing-library.com/docs/ecosystem-jest-dom). + +## Installation + +Install the new dependencies. Note that RTL dependencies are only peer dependencies and you should specify them in your project. + +```bash +npm install -D @ima/testing-library @testing-library/dom @testing-library/jest-dom @testing-library/react +``` + +Configure jest preset in your jest config file. + +```json +{ + "preset": "@ima/testing-library" +} +``` + +Everything should start working out of the box for a typical IMA.js application. If you are trying to setup this library in a monorepo or an npm package, you might have to do some tweaks with the configuration. In this case, you need the jest config file to be in non-json format. + +```javascript +const { setImaTestingLibraryConfig, FALLBACK_APP_MAIN_PATH } = require('@ima/testing-library'); + +setImaTestingLibraryConfig({ + // your custom config + appMainPath: FALLBACK_APP_MAIN_PATH, // There is a default app main file as part of the package, it contains only the minimal setup and it might be enough for you if you don't have any real app main file. + imaConfigPath: 'path/to/your/ima.config.js', + applicationFolder: '/path/to/folder/containing/server/folder', +}); + +module.exports = { + preset: '@ima/testing-library' +}; +``` + +## Usage + +IMA Testing Library is re-exporting everything from `@testing-library/react`. It provides the default context wrapper for the `render` method. Thanks to this, the default example from the React Testing Library documentation will work out of the box. You just need to import the `render` method from the `@ima/testing-library` package. + +```javascript +import { render } from '@ima/testing-library'; + +test('renders learn react link', () => { + const { getByText } = render(); + const linkElement = getByText(/learn react/i); + + expect(linkElement).toBeInTheDocument(); +}); +``` + +You might need to specify custom additions to the context, or mock some parts of the IMA application. You can do this by providing a custom context wrapper and using the `@ima/testing-library` specific utilities. + +```javascript +import { render, getContextWrapper, getContextValue, initImaApp } from '@ima/testing-library'; + +test('renders learn react link with custom context wrapper', () => { + const ContextWrapper = getContextWrapper(); + const { getByText } = render(, { + wrapper: , + }); + const linkElement = getByText(/learn react/i); + + expect(linkElement).toBeInTheDocument(); +}); + +test('renders learn react link with custom context value', () => { + const contextValue = getContextValue(); + + contextValue.$Utils.$Foo = jest.fn(() => 'bar'); + + const ContextWrapper = getContextWrapper(contextValue); + const { getByText } = render(, { + wrapper: ContextWrapper, + }); + const linkElement = getByText(/learn react/i); + + expect(linkElement).toBeInTheDocument(); +}); + +test('renders learn react link with custom app configuration', () => { + const app = initImaApp(); + + app.oc.get('$Utils').$Foo = jest.fn(() => 'bar'); + + const contextValue = getContextValue(app); + const ContextWrapper = getContextWrapper(contextValue); + const { getByText } = render(, { + wrapper: ContextWrapper, + }); + const linkElement = getByText(/learn react/i); + + expect(linkElement).toBeInTheDocument(); +}); +``` diff --git a/packages/testing-library/jest.config.js b/packages/testing-library/jest.config.js new file mode 100644 index 0000000000..9f891301ff --- /dev/null +++ b/packages/testing-library/jest.config.js @@ -0,0 +1,6 @@ +const defaultConfig = require('../../jest.config.base.js'); + +module.exports = { + ...defaultConfig, + testRegex: '(/__tests__/).*Spec\\.[jt]s$', +}; diff --git a/packages/testing-library/package.json b/packages/testing-library/package.json new file mode 100644 index 0000000000..b9933fc41b --- /dev/null +++ b/packages/testing-library/package.json @@ -0,0 +1,48 @@ +{ + "name": "@ima/testing-library", + "version": "19.6.0", + "description": "Testing library for IMA.js applications.", + "keywords": [ + "IMA.js", + "react", + "testing", + "library" + ], + "bugs": { + "url": "https://github.com/seznam/ima/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/seznam/ima.git", + "directory": "packages/testing-library" + }, + "license": "MIT", + "author": "Filip Satek ", + "scripts": { + "dev": "node ../plugin-cli/dist/bin/ima-plugin.js dev", + "build": "node ../plugin-cli/dist/bin/ima-plugin.js build", + "link": "node ../plugin-cli/dist/bin/ima-plugin.js link", + "lint": "eslint './**/*.{js,jsx,ts,tsx}'", + "test": "jest -c jest.config.js" + }, + "exports": { + ".": { + "types": "./dist/esm/index.d.ts", + "import": "./dist/esm/index.js", + "default": "./dist/cjs/index.js" + }, + "./jest-preset": "./dist/cjs/jest-preset.js", + "./jestSetupFileAfterEnv": "./dist/cjs/jestSetupFileAfterEnv.js" + }, + "peerDependencies": { + "@ima/core": ">=19.0.0", + "@ima/react-page-renderer": ">=19.0.0", + "@testing-library/dom": ">=10.0.0", + "@testing-library/jest-dom": ">=6.0.0", + "@testing-library/react": ">=16.0.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/testing-library/setupJest.js b/packages/testing-library/setupJest.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/testing-library/src/app/config/bind.ts b/packages/testing-library/src/app/config/bind.ts new file mode 100644 index 0000000000..03f2166b66 --- /dev/null +++ b/packages/testing-library/src/app/config/bind.ts @@ -0,0 +1,53 @@ +import { + ComponentUtils, + InitBindFunction, + PageRenderer, + Window, +} from '@ima/core'; +import { + defaultCssClasses as cssClassNameProcessor, + PageRendererFactory, + ServerPageRenderer, +} from '@ima/react-page-renderer'; +import { ClientPageRenderer } from '@ima/react-page-renderer/renderer/ClientPageRenderer'; + +declare module '@ima/core' { + interface OCAliasMap { + $CssClasses: () => typeof cssClassNameProcessor; + $PageRendererFactory: PageRendererFactory; + } +} + +export const initBindApp: InitBindFunction = (ns, oc) => { + // UI components + oc.bind('$CssClasses', function () { + return cssClassNameProcessor; + }); + + // You can set own Component utils here + oc.get(ComponentUtils).register({ + $CssClasses: '$CssClasses', + }); + + oc.inject(PageRendererFactory, [ComponentUtils]); + oc.bind('$PageRendererFactory', PageRendererFactory); + + if (oc.get(Window).isClient()) { + oc.provide(PageRenderer, ClientPageRenderer, [ + PageRendererFactory, + '$Helper', + '$Dispatcher', + '$Settings', + Window, + ]); + } else { + oc.provide(PageRenderer, ServerPageRenderer, [ + PageRendererFactory, + '$Helper', + '$Dispatcher', + '$Settings', + ]); + } + + oc.bind('$PageRenderer', PageRenderer); +}; diff --git a/packages/testing-library/src/app/config/settings.ts b/packages/testing-library/src/app/config/settings.ts new file mode 100644 index 0000000000..03e6b7e572 --- /dev/null +++ b/packages/testing-library/src/app/config/settings.ts @@ -0,0 +1,45 @@ +import { InitSettingsFunction } from '@ima/core'; + +export const initSettings: InitSettingsFunction = (ns, oc, config) => { + return { + prod: { + $Version: config.$Version, + $Http: { + defaultRequestOptions: { + timeout: 7000, // Request timeout + repeatRequest: 0, // Count of automatic repeated request after failing request. + ttl: 60000, // Default time to live for cached request in ms. + fetchOptions: { + mode: 'cors', + headers: { + // Set default request headers + Accept: 'application/json', + 'Accept-Language': config.$Language, + }, + }, + cache: false, // if value exists in cache then returned it else make request to remote server. + }, + cacheOptions: { + prefix: 'http.', // Cache key prefix for response bodies (already parsed as JSON) of completed HTTP requests. + }, + }, + $Router: { + /** + * Middleware execution timeout, see https://imajs.io/basic-features/routing/middlewares#execution-timeout + * for more information. + */ + middlewareTimeout: 30000, + }, + $Cache: { + enabled: false, //Turn on/off cache for all application. + ttl: 60000, // Default time to live for cached value in ms. + }, + $Page: { + $Render: { + documentView: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function + masterElementId: 'page', + }, + }, + }, + }; +}; diff --git a/packages/testing-library/src/app/main.ts b/packages/testing-library/src/app/main.ts new file mode 100644 index 0000000000..f3387a4c9a --- /dev/null +++ b/packages/testing-library/src/app/main.ts @@ -0,0 +1,15 @@ +import * as ima from '@ima/core'; + +import { initBindApp } from './config/bind'; +import { initSettings } from './config/settings'; + +const getInitialAppConfigFunctions = () => { + return { + initBindApp, + initRoutes: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function + initServicesApp: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function + initSettings, + }; +}; + +export { getInitialAppConfigFunctions, ima }; diff --git a/packages/testing-library/src/configuration.ts b/packages/testing-library/src/configuration.ts new file mode 100644 index 0000000000..8df5a9c584 --- /dev/null +++ b/packages/testing-library/src/configuration.ts @@ -0,0 +1,57 @@ +import type { Environment } from '@ima/core'; + +export interface Configuration { + /** + * The path to the main application file. This file should be exporting getInitialAppConfigFunctions and ima keys. + */ + appMainPath: string; + /** + * The path to the IMA configuration file. This can be only configured once before first `initImaApp` call and cannot be reconfigured later. + */ + imaConfigPath: string; + /** + * The protocol of the application. This can be only configured in the jest config file and cannot be reconfigured later. + */ + protocol: string; + /** + * The host of the application. This can be only configured in the jest config file and cannot be reconfigured later. + */ + host: string; + /** + * The locale of the application. This will affect the language of the application. + */ + locale: string; + /** + * The process environment configuration. This allows you to change the environment configuration that will be available in jsdom. + * This can be only configured in the jest config file and cannot be reconfigured later. + */ + processEnvironment: (env: Environment) => Environment; + /** + * The path to the application folder. This can be only configured in the jest config file and cannot be reconfigured later. + */ + applicationFolder: string | undefined; +} + +const configuration: Configuration = { + appMainPath: 'app/main.js', + imaConfigPath: 'ima.config.js', + protocol: 'https:', + host: 'imajs.io', + locale: 'en', + processEnvironment: env => env, + applicationFolder: undefined, +}; + +/** + * Get the current configuration. + */ +export function getImaTestingLibraryConfig() { + return configuration; +} + +/** + * Modify the current configuration. + */ +export function setImaTestingLibraryConfig(config: Partial) { + Object.assign(configuration, config); +} diff --git a/packages/testing-library/src/helpers.ts b/packages/testing-library/src/helpers.ts new file mode 100644 index 0000000000..40f856acec --- /dev/null +++ b/packages/testing-library/src/helpers.ts @@ -0,0 +1,11 @@ +import path from 'path'; + +/** + * Requires specified file from projectPath + * + * @param {string} projectPath relative project path to a file + * @returns {*} File exports + */ +export function requireFromProject(projectPath: string) { + return require(path.resolve(projectPath)); +} diff --git a/packages/testing-library/src/index.ts b/packages/testing-library/src/index.ts new file mode 100644 index 0000000000..3c364ab191 --- /dev/null +++ b/packages/testing-library/src/index.ts @@ -0,0 +1,2 @@ +export * from './rtl'; +export * from './configuration'; diff --git a/packages/testing-library/src/jest-preset.ts b/packages/testing-library/src/jest-preset.ts new file mode 100644 index 0000000000..98732d6f73 --- /dev/null +++ b/packages/testing-library/src/jest-preset.ts @@ -0,0 +1,76 @@ +import { createIMAServer } from '@ima/server'; +import type { Config } from 'jest'; + +import { getImaTestingLibraryConfig } from './configuration'; + +const imaTestingLibraryConfig = getImaTestingLibraryConfig(); + +/** + * Get response content from @ima/server. + */ +async function _getIMAResponseContent(): Promise { + // Mock devUtils to override manifest loading + const devUtils = { + manifestRequire: () => ({}), + }; + + // Prepare serverApp with environment override + const { serverApp } = await createIMAServer({ + devUtils, + applicationFolder: imaTestingLibraryConfig.applicationFolder, + processEnvironment: currentEnvironment => + imaTestingLibraryConfig.processEnvironment({ + ...currentEnvironment, + $Server: { + ...currentEnvironment.$Server, + concurrency: 0, + serveSPA: { + allow: true, + }, + }, + $Debug: true, + }), + }); + + // Generate request response + const response = await serverApp.requestHandler( + { + get: () => '', + headers: () => '', + originalUrl: imaTestingLibraryConfig.host, + protocol: imaTestingLibraryConfig.protocol.replace(':', ''), + }, + { + status: () => 200, + send: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function + set: () => {}, // eslint-disable-line @typescript-eslint/no-empty-function + locals: { + language: imaTestingLibraryConfig.locale, + host: imaTestingLibraryConfig.host, + protocol: imaTestingLibraryConfig.protocol, + path: '', + root: '', + languagePartPath: '', + }, + } + ); + + return response.content; +} + +/** + * Jest configuration for IMA testing library. + * We are entering undocumented territory here, jestConfig is a promise, but documentation does not mention, if it is allowed. + * It would be nice if there was a synchronous and more straightforward way of generating IMA SPA content. + */ +const jestConfig: Promise = (async () => ({ + setupFiles: ['@ima/core/setupJest.js'], + setupFilesAfterEnv: ['@ima/testing-library/jestSetupFileAfterEnv'], + testEnvironment: 'jsdom', + testEnvironmentOptions: { + html: await _getIMAResponseContent(), + url: `${imaTestingLibraryConfig.protocol}//${imaTestingLibraryConfig.host}/`, + }, +}))(); + +export default jestConfig; diff --git a/packages/testing-library/src/jestSetupFileAfterEnv.ts b/packages/testing-library/src/jestSetupFileAfterEnv.ts new file mode 100644 index 0000000000..ee0f0693eb --- /dev/null +++ b/packages/testing-library/src/jestSetupFileAfterEnv.ts @@ -0,0 +1,6 @@ +import '@testing-library/jest-dom'; +// @TODO It would be nice to mock fetch also, but there are some issues with fetch-mock being esm only, while +// we are importing it from commonjs. We should investigate this further. +// import fetchMock from 'fetch-mock'; + +// global.fetch = fetchMock.sandbox() as typeof fetch; diff --git a/packages/testing-library/src/localization.ts b/packages/testing-library/src/localization.ts new file mode 100644 index 0000000000..a4a07b6c17 --- /dev/null +++ b/packages/testing-library/src/localization.ts @@ -0,0 +1,80 @@ +import path from 'path'; + +import { assignRecursively } from '@ima/helpers'; +import MessageFormat from '@messageformat/core'; +import globby from 'globby'; + +import { requireFromProject } from './helpers'; + +/** + * @TODO The localization logic should be mostly taken from https://github.com/seznam/ima/blob/master/packages/cli/src/webpack/languages.ts + * This solution is similar, but not the same and there can be some inconsistencies. + */ + +/** + * Generates IMA formatted dictionary + * + * @param {string} locale + * @returns {object} + */ +function generateDictionary(locale = 'cs') { + // @TODO: locale should be taken from config and should be en by default + const { languages } = requireFromProject('./ima.config.js'); + const mf = new MessageFormat(locale); + const dictionaries: Record = {}; + const langFileGlobs = languages[locale]; + + globby.sync(langFileGlobs).forEach(file => { + try { + const filename = path + .basename(file) + .replace(locale.toUpperCase() + path.extname(file), ''); + + const dictJson = requireFromProject(file); + + dictionaries[filename] = assignRecursively( + dictionaries[filename] ?? {}, + _deepMapValues(dictJson, mf.compile.bind(mf)) + ); + } catch (e) { + console.error( + `Tried to load dictionary JSON at path "${file}", but recieved following error.` + ); + console.error(e); + } + }); + + return dictionaries; +} + +/** + * Apply function through full object or array values + * + * @param {object | Array} obj object to be manipulated + * @param {Function} fn function to run on values + * @returns {object | Array} + */ +function _deepMapValues( + obj: object | Array, + fn: typeof MessageFormat.prototype.compile +): + | ReturnType + | Record + | string { + if (Array.isArray(obj)) { + return obj.map(val => _deepMapValues(val, fn)); + } else if (typeof obj === 'function') { + // Skip already mapped values + return obj; + } else if (typeof obj === 'object' && obj !== null) { + return Object.keys(obj).reduce((acc, current) => { + // @ts-expect-error I don't know how to type `obj[current]`, help me! + acc[current] = _deepMapValues(obj[current], fn); + return acc; + }, {} as Record); + } else { + return fn(obj); + } +} + +export { generateDictionary }; diff --git a/packages/testing-library/src/rtl.tsx b/packages/testing-library/src/rtl.tsx new file mode 100644 index 0000000000..783abb02eb --- /dev/null +++ b/packages/testing-library/src/rtl.tsx @@ -0,0 +1,96 @@ +import path from 'node:path'; + +import * as imaCore from '@ima/core'; +import { PageContext } from '@ima/react-page-renderer'; +import { render, RenderOptions } from '@testing-library/react'; // eslint-disable-line import/named +import React, { ReactElement } from 'react'; + +import { getImaTestingLibraryConfig } from './configuration'; +import { requireFromProject } from './helpers'; +import { generateDictionary } from './localization'; + +export interface ContextValue { + $Utils: imaCore.Utils; +} + +export const FALLBACK_APP_MAIN_PATH = path.resolve(__dirname, 'app/main.js'); + +// Some operations take way too long to be executed with each render call, +// so we need to cache these values +const mainFile: Record< + string, + { + ima: typeof imaCore; + getInitialAppConfigFunctions: () => imaCore.InitAppConfig; + } +> = {}; +const dictionary: Record = {}; + +export function initImaApp() { + const config = getImaTestingLibraryConfig(); + + if (!mainFile[config.appMainPath]) { + mainFile[config.appMainPath] = requireFromProject(config.appMainPath); + } + + if (!dictionary[config.locale]) { + dictionary[config.locale] = generateDictionary(config.locale); + } + + const { ima, getInitialAppConfigFunctions } = mainFile[config.appMainPath]; + + // Init language files + // This must be initialized before oc.get('$Dictionary').init() is called (usualy part of initServices) + global.$IMA.i18n = dictionary[config.locale]; + + const app = ima.createImaApp(); + const bootConfig = ima.getClientBootConfig(getInitialAppConfigFunctions()); + + // Init app + ima.bootClientApp(app, bootConfig); + + return app; +} + +export function getContextValue( + app?: ReturnType +): ContextValue { + if (!app) { + app = initImaApp(); + } + + return { $Utils: app.oc.get('$ComponentUtils').getUtils() }; +} + +export function getContextWrapper(contextValue?: ContextValue) { + if (!contextValue) { + contextValue = getContextValue(); + } + + return function IMATestingLibraryContextWrapper({ + children, + }: { + children: React.ReactNode; + }) { + return ( + + {children} + + ); + }; +} + +const customRender = (ui: ReactElement, options?: RenderOptions) => { + let { wrapper, ...rest } = options ?? {}; // eslint-disable-line prefer-const + + if (!wrapper) { + wrapper = getContextWrapper(); + } + + const result = render(ui, { wrapper, ...rest }); + + return result; +}; + +export * from '@testing-library/react'; // eslint-disable-line import/export +export { customRender as render }; // eslint-disable-line import/export diff --git a/packages/testing-library/tsconfig.json b/packages/testing-library/tsconfig.json new file mode 100644 index 0000000000..00b55540fd --- /dev/null +++ b/packages/testing-library/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2018", + "module": "Node16", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable", + ], + "outDir": "dist", + "types": ["webpack", "core", "cli", "react-page-renderer", "less-plugin-glob"] + }, + "include": [ + "src" + ] +}