diff --git a/.gitignore b/.gitignore index ef455c1..11a9d71 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ coverage node_modules outputs packages +**/mock-test-utils/selectors +**/mock-test-utils/dom/index.ts diff --git a/package-lock.json b/package-lock.json index d4a8511..046b819 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "css-selector-tokenizer": "^0.8.0", "css.escape": "^1.5.1", "glob": "^7.2.0", + "lodash": "^4.17.21", "react-dom": "^18.2.0" }, "devDependencies": { @@ -26,6 +27,7 @@ "@types/babel__core": "^7.1.16", "@types/glob": "^8.1.0", "@types/jest": "^28.1.3", + "@types/lodash": "^4.17.13", "@types/node": "^18.0.0", "@types/react-dom": "^18.0.10", "@vitest/coverage-istanbul": "^1.2.2", @@ -1492,6 +1494,12 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true + }, "node_modules/@types/node": { "version": "18.19.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", @@ -1704,6 +1712,25 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@typescript-eslint/utils": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", @@ -2465,6 +2492,14 @@ "node": ">=8" } }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2873,29 +2908,6 @@ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", "dev": true }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/expect": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", @@ -3113,18 +3125,6 @@ "node": "*" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3169,25 +3169,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3304,15 +3285,6 @@ "node": ">= 14" } }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/husky": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", @@ -3477,18 +3449,6 @@ "optional": true, "peer": true }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3969,6 +3929,101 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/lint-staged/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/listr2": { "version": "8.2.4", "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", @@ -4372,33 +4427,6 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nwsapi": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", @@ -4579,14 +4607,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" - } - }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -5457,18 +5477,6 @@ "node": ">=8" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -5967,6 +5975,101 @@ } } }, + "node_modules/vitest/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/vitest/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/vitest/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/package.json b/package.json index cc318e9..673c084 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "homepage": "https://cloudscape.design", "scripts": { "lint": "eslint --ignore-path .gitignore --ext js,ts .", - "build": "tsc -b && node scripts/generate-package.js && node scripts/generate-doc.js && node scripts/generate-exports.js", + "build": "tsc -b && node scripts/generate-package.js && node scripts/generate-doc.js && node scripts/generate-exports.js && node scripts/build-mocks.js", "postbuild": "cp NOTICE README.md LICENSE lib/core && cp NOTICE README.md LICENSE lib/converter", "test": "vitest run", "posttest": "node ./scripts/verify-typescript.js", @@ -27,6 +27,7 @@ "css-selector-tokenizer": "^0.8.0", "css.escape": "^1.5.1", "glob": "^7.2.0", + "lodash": "^4.17.21", "react-dom": "^18.2.0" }, "devDependencies": { @@ -34,6 +35,7 @@ "@types/babel__core": "^7.1.16", "@types/glob": "^8.1.0", "@types/jest": "^28.1.3", + "@types/lodash": "^4.17.13", "@types/node": "^18.0.0", "@types/react-dom": "^18.0.10", "@vitest/coverage-istanbul": "^1.2.2", diff --git a/scripts/build-mocks.js b/scripts/build-mocks.js new file mode 100644 index 0000000..c358b50 --- /dev/null +++ b/scripts/build-mocks.js @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +const path = require('path'); +const { generateTestUtils } = require('../lib/converter/dist/generate-test-utils'); + +const mockComponents = [ + { + name: 'TestComponentA', + pluralName: 'TestComponentAs', + }, + { + name: 'TestComponentB', + pluralName: 'TestComponentBs', + }, +]; + +const testUtilsPath = path.resolve(__dirname, '../src/converter/test/mock-test-utils'); +generateTestUtils({ + components: mockComponents, + testUtilsPath, +}); diff --git a/src/converter/generate-component-finders.ts b/src/converter/generate-component-finders.ts new file mode 100644 index 0000000..648d39d --- /dev/null +++ b/src/converter/generate-component-finders.ts @@ -0,0 +1,105 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ComponentWrapperMetadata, TestUtilType } from './interfaces'; + +const componentWrapperImport = ({ wrapperName, wrapperImportPath }: ComponentWrapperMetadata) => ` +import ${wrapperName} from '${wrapperImportPath}';`; + +const componentWrapperExport = ({ wrapperName }: ComponentWrapperMetadata) => ` +export { ${wrapperName} };`; + +const componentFinders = ({ name, wrapperName, pluralName }: ComponentWrapperMetadata) => ` +ElementWrapper.prototype.find${name} = function(selector) { + const rootSelector = \`.$\{${wrapperName}.rootSelector}\`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, ${wrapperName}); +}; + +ElementWrapper.prototype.findAll${pluralName} = function(selector) { + return this.findAllComponents(${wrapperName}, selector); +};`; + +const componentFindersInterfaces = { + dom: ({ name, pluralName, wrapperName }: ComponentWrapperMetadata) => ` +/** + * Returns the wrapper of the first ${name} that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first ${name}. + * If no matching ${name} is found, returns \`null\`. + * + * @param {string} [selector] CSS Selector + * @returns {${wrapperName} | null} + */ +find${name}(selector?: string): ${wrapperName} | null; + +/** + * Returns an array of ${name} wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the ${pluralName} inside the current wrapper. + * If no matching ${name} is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array<${wrapperName}>} + */ +findAll${pluralName}(selector?: string): Array<${wrapperName}>;`, + + selectors: ({ name, pluralName, wrapperName }: ComponentWrapperMetadata) => ` +/** + * Returns a wrapper that matches the ${pluralName} with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches ${pluralName}. + * + * @param {string} [selector] CSS Selector + * @returns {${wrapperName}} + */ +find${name}(selector?: string): ${wrapperName}; + +/** + * Returns a multi-element wrapper that matches ${pluralName} with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches ${pluralName}. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper<${wrapperName}>} + */ +findAll${pluralName}(selector?: string): MultiElementWrapper<${wrapperName}>;`, +}; + +const defaultExport = { + dom: ` +export default function wrapper(root: Element = document.body) { + if (document && document.body && !document.body.contains(root)) { + console.warn('[AwsUi] [test-utils] provided element is not part of the document body, interactions may work incorrectly') + }; + return new ElementWrapper(root); +}`, + + selectors: ` +export default function wrapper(root: string = 'body') { + return new ElementWrapper(root); +}`, +}; + +export interface GenerateFindersParams { + components: ComponentWrapperMetadata[]; + testUtilType: TestUtilType; +} + +export const generateComponentFinders = ({ components, testUtilType }: GenerateFindersParams) => ` +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ElementWrapper } from '@cloudscape-design/test-utils-core/${testUtilType}'; +import { appendSelector } from '@cloudscape-design/test-utils-core/utils'; + +export { ElementWrapper }; +${components.map(componentWrapperImport).join('')} + +${components.map(componentWrapperExport).join('')} + +declare module '@cloudscape-design/test-utils-core/dist/${testUtilType}' { + interface ElementWrapper { + ${components.map(componentFindersInterfaces[testUtilType]).join('')} + } +} + +${components.map(componentFinders).join('')} + +${defaultExport[testUtilType]} +`; diff --git a/src/converter/generate-test-utils.ts b/src/converter/generate-test-utils.ts new file mode 100644 index 0000000..bb4d18d --- /dev/null +++ b/src/converter/generate-test-utils.ts @@ -0,0 +1,60 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import path from 'path'; +import fs from 'fs'; +import { generateComponentFinders } from './generate-component-finders'; +import { ComponentWrapperMetadata, GenerateTestUtilsParams, TestUtilType } from './interfaces'; +import { writeSourceFile } from './utils'; +import { kebabCase } from 'lodash'; +import { convertToSelectorUtil } from './convert-to-selectors'; +import glob from 'glob'; + +interface GenerateIndexFilesParams extends GenerateTestUtilsParams { + testUtilType: TestUtilType; +} + +function generateIndexFile({ testUtilsPath, components, testUtilType }: GenerateIndexFilesParams) { + const componenWrappersMetadata: ComponentWrapperMetadata[] = components.map( + ({ name, pluralName, testUtilsFolderName }) => ({ + name, + pluralName, + wrapperName: `${name}Wrapper`, + wrapperImportPath: `./${testUtilsFolderName ?? kebabCase(name)}`, + }) + ); + + const content = generateComponentFinders({ testUtilType, components: componenWrappersMetadata }); + const indexFilePath = path.join(testUtilsPath, testUtilType, 'index.ts'); + writeSourceFile(indexFilePath, content); +} + +function generateSelectorUtils(testUtilsPath: string) { + const domFolderPath = path.join(testUtilsPath, 'dom'); + const selectorsFolderPath = path.join(testUtilsPath, 'selectors'); + const conversionTargetRelativePaths = glob.sync(`**/*.{ts,tsx}`, { cwd: domFolderPath }); + + if (conversionTargetRelativePaths.length === 0) { + throw new Error(`No file with ts or tsx extension found at: ${domFolderPath}`); + } + + for (const fileRelativePath of conversionTargetRelativePaths) { + const domFilePath = path.join(domFolderPath, fileRelativePath); + const domFileContent = fs.readFileSync(domFilePath, 'utf-8'); + const selectorsFilePath = path.join(selectorsFolderPath, fileRelativePath); + const selectorsFileContent = convertToSelectorUtil(domFileContent); + + if (!selectorsFileContent) { + throw new Error('Converted file content is empty'); + } + writeSourceFile(selectorsFilePath, selectorsFileContent); + } +} + +/** + * Generates test utils index files for dom and selector and converts the dom test utils to selectors. + */ +export function generateTestUtils({ components, testUtilsPath }: GenerateTestUtilsParams) { + generateSelectorUtils(testUtilsPath); + generateIndexFile({ components, testUtilsPath, testUtilType: 'dom' }); + generateIndexFile({ components, testUtilsPath, testUtilType: 'selectors' }); +} diff --git a/src/converter/index.ts b/src/converter/index.ts index 071756b..465c2f2 100644 --- a/src/converter/index.ts +++ b/src/converter/index.ts @@ -3,3 +3,5 @@ export { convertToSelectorUtil as default } from './convert-to-selectors'; export { extractTestSelectorsUtil } from './extract-test-selectors'; +export { generateTestUtils } from './generate-test-utils'; +export { generateComponentFinders } from './generate-component-finders'; diff --git a/src/converter/interfaces.ts b/src/converter/interfaces.ts new file mode 100644 index 0000000..fc34c90 --- /dev/null +++ b/src/converter/interfaces.ts @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export type TestUtilType = 'dom' | 'selectors'; + +export interface ComponentMetadata { + /** + * Name of the component in pascal case. + * Examples: Button, Alert, ButtonDropdown + */ + name: string; + + /** + * Plural name of the component in pascal case. + * Examples: Buttons, Alerts, ButtonDropdowns + */ + pluralName: string; + + /** + * Folder name of the component test utils. + * If not specified, the kebab case of the component name will be used by default. + */ + testUtilsFolderName?: string; +} + +export interface GenerateTestUtilsParams { + /** + * List of components metadata to generate test utils. + */ + components: ComponentMetadata[]; + + /* + * Absolute path to the test utils folder. + * + * Component wrappers will be loaded from this path. + * Generated test utils will be stored in this folder. + * + * Expected file name format is kebab case. + * + */ + testUtilsPath: string; +} + +export interface ComponentWrapperMetadata extends ComponentMetadata { + /* + * Name of the component wrapper in pascal case + * Examples: ButtonWrapper, AlertWrapper, ButtonDropdownWrapper + */ + wrapperName: string; + + /** + * Relative path to import the wrapper. + */ + wrapperImportPath: string; +} diff --git a/src/converter/test/__snapshots__/test-utils-generator-snapshot.test.ts.snap b/src/converter/test/__snapshots__/test-utils-generator-snapshot.test.ts.snap new file mode 100644 index 0000000..610608c --- /dev/null +++ b/src/converter/test/__snapshots__/test-utils-generator-snapshot.test.ts.snap @@ -0,0 +1,178 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`index files > dom index file matches the snapshot 1`] = ` +" +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; +import { appendSelector } from '@cloudscape-design/test-utils-core/utils'; + +export { ElementWrapper }; + +import TestComponentAWrapper from './test-component-a'; +import TestComponentBWrapper from './test-component-b'; + + +export { TestComponentAWrapper }; +export { TestComponentBWrapper }; + +declare module '@cloudscape-design/test-utils-core/dist/dom' { + interface ElementWrapper { + +/** + * Returns the wrapper of the first TestComponentA that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first TestComponentA. + * If no matching TestComponentA is found, returns \`null\`. + * + * @param {string} [selector] CSS Selector + * @returns {TestComponentAWrapper | null} + */ +findTestComponentA(selector?: string): TestComponentAWrapper | null; + +/** + * Returns an array of TestComponentA wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the TestComponentAs inside the current wrapper. + * If no matching TestComponentA is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ +findAllTestComponentAs(selector?: string): Array; +/** + * Returns the wrapper of the first TestComponentB that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first TestComponentB. + * If no matching TestComponentB is found, returns \`null\`. + * + * @param {string} [selector] CSS Selector + * @returns {TestComponentBWrapper | null} + */ +findTestComponentB(selector?: string): TestComponentBWrapper | null; + +/** + * Returns an array of TestComponentB wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the TestComponentBs inside the current wrapper. + * If no matching TestComponentB is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ +findAllTestComponentBs(selector?: string): Array; + } +} + + +ElementWrapper.prototype.findTestComponentA = function(selector) { + const rootSelector = \`.\${TestComponentAWrapper.rootSelector}\`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TestComponentAWrapper); +}; + +ElementWrapper.prototype.findAllTestComponentAs = function(selector) { + return this.findAllComponents(TestComponentAWrapper, selector); +}; +ElementWrapper.prototype.findTestComponentB = function(selector) { + const rootSelector = \`.\${TestComponentBWrapper.rootSelector}\`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TestComponentBWrapper); +}; + +ElementWrapper.prototype.findAllTestComponentBs = function(selector) { + return this.findAllComponents(TestComponentBWrapper, selector); +}; + + +export default function wrapper(root: Element = document.body) { + if (document && document.body && !document.body.contains(root)) { + console.warn('[AwsUi] [test-utils] provided element is not part of the document body, interactions may work incorrectly') + }; + return new ElementWrapper(root); +} +" +`; + +exports[`index files > selectors index file matches the snapshot 1`] = ` +" +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ElementWrapper } from '@cloudscape-design/test-utils-core/selectors'; +import { appendSelector } from '@cloudscape-design/test-utils-core/utils'; + +export { ElementWrapper }; + +import TestComponentAWrapper from './test-component-a'; +import TestComponentBWrapper from './test-component-b'; + + +export { TestComponentAWrapper }; +export { TestComponentBWrapper }; + +declare module '@cloudscape-design/test-utils-core/dist/selectors' { + interface ElementWrapper { + +/** + * Returns a wrapper that matches the TestComponentAs with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches TestComponentAs. + * + * @param {string} [selector] CSS Selector + * @returns {TestComponentAWrapper} + */ +findTestComponentA(selector?: string): TestComponentAWrapper; + +/** + * Returns a multi-element wrapper that matches TestComponentAs with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches TestComponentAs. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ +findAllTestComponentAs(selector?: string): MultiElementWrapper; +/** + * Returns a wrapper that matches the TestComponentBs with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches TestComponentBs. + * + * @param {string} [selector] CSS Selector + * @returns {TestComponentBWrapper} + */ +findTestComponentB(selector?: string): TestComponentBWrapper; + +/** + * Returns a multi-element wrapper that matches TestComponentBs with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches TestComponentBs. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ +findAllTestComponentBs(selector?: string): MultiElementWrapper; + } +} + + +ElementWrapper.prototype.findTestComponentA = function(selector) { + const rootSelector = \`.\${TestComponentAWrapper.rootSelector}\`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TestComponentAWrapper); +}; + +ElementWrapper.prototype.findAllTestComponentAs = function(selector) { + return this.findAllComponents(TestComponentAWrapper, selector); +}; +ElementWrapper.prototype.findTestComponentB = function(selector) { + const rootSelector = \`.\${TestComponentBWrapper.rootSelector}\`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, TestComponentBWrapper); +}; + +ElementWrapper.prototype.findAllTestComponentBs = function(selector) { + return this.findAllComponents(TestComponentBWrapper, selector); +}; + + +export default function wrapper(root: string = 'body') { + return new ElementWrapper(root); +} +" +`; diff --git a/src/converter/test/generate-component-finders.test.tsx b/src/converter/test/generate-component-finders.test.tsx new file mode 100644 index 0000000..0fe2f14 --- /dev/null +++ b/src/converter/test/generate-component-finders.test.tsx @@ -0,0 +1,70 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { describe, test, expect } from 'vitest'; +import { generateComponentFinders } from '../generate-component-finders'; +import { ComponentWrapperMetadata } from '../interfaces'; + +const mockComponents: ComponentWrapperMetadata[] = [ + { + name: 'Alert', + pluralName: 'Alerts', + wrapperName: 'AlertWrapper', + wrapperImportPath: './test-utils/AlertWrapper', + testUtilsFolderName: '../test-utils', + }, + { + name: 'Status', + pluralName: 'Status', // The plural name is deliberately the same as singular + wrapperName: 'StatusWrapper', + wrapperImportPath: './test-utils/StatusWrapper', + testUtilsFolderName: '../test-utils', + }, +]; + +describe(`${generateComponentFinders.name}`, () => { + const testUtilTypes = ['dom', 'selectors'] as const; + + describe.each(testUtilTypes)('%s', testUtilType => { + const sourceFileContent = generateComponentFinders({ components: mockComponents, testUtilType }); + + test('it re-exports element wrapper', () => { + expect(sourceFileContent).toMatch('export { ElementWrapper }'); + }); + + test('it export component wrappers', () => { + expect(sourceFileContent).toMatch('export { AlertWrapper };'); + expect(sourceFileContent).toMatch('export { StatusWrapper };'); + }); + + test('it exports interfaces', () => { + expect(sourceFileContent).toMatch(`declare module '@cloudscape-design/test-utils-core/dist/${testUtilType}'`); + + if (testUtilType === 'dom') { + expect(sourceFileContent).toMatch(`findAlert(selector?: string): AlertWrapper | null`); + expect(sourceFileContent).toMatch(`findAllAlerts(selector?: string): Array`); + expect(sourceFileContent).toMatch(`findStatus(selector?: string): StatusWrapper | null`); + expect(sourceFileContent).toMatch(`findAllStatus(selector?: string): Array`); + } else { + expect(sourceFileContent).toMatch(`findAlert(selector?: string): AlertWrapper`); + expect(sourceFileContent).toMatch(`findAllAlerts(selector?: string): MultiElementWrapper`); + expect(sourceFileContent).toMatch(`findStatus(selector?: string): StatusWrapper`); + expect(sourceFileContent).toMatch(`findAllStatus(selector?: string): MultiElementWrapper`); + } + }); + + test('it adds finder implementations to the ElementWrapper', () => { + expect(sourceFileContent).toMatch('ElementWrapper.prototype.findAlert = function(selector)'); + expect(sourceFileContent).toMatch('ElementWrapper.prototype.findAllAlerts = function(selector)'); + expect(sourceFileContent).toMatch('ElementWrapper.prototype.findStatus = function(selector)'); + expect(sourceFileContent).toMatch('ElementWrapper.prototype.findAllStatus = function(selector)'); + }); + + test('it exports the wrapper creator', () => { + if (testUtilType === 'dom') { + expect(sourceFileContent).toMatch('export default function wrapper(root: Element = document.body)'); + } else { + expect(sourceFileContent).toMatch(`export default function wrapper(root: string = 'body')`); + } + }); + }); +}); diff --git a/src/converter/test/generate-test-utils.test.ts b/src/converter/test/generate-test-utils.test.ts new file mode 100644 index 0000000..b33d7b4 --- /dev/null +++ b/src/converter/test/generate-test-utils.test.ts @@ -0,0 +1,67 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { readFileSync, writeFileSync } from 'fs'; +import { describe, test, expect, beforeAll, beforeEach, vi } from 'vitest'; +import { convertToSelectorUtil } from '../convert-to-selectors'; +import { generateTestUtils } from '../generate-test-utils'; +import { ComponentMetadata } from '../interfaces'; + +vi.mock('fs'); +vi.mock('../convert-to-selectors'); + +const mockComponents: ComponentMetadata[] = [ + { + name: 'Alert', + pluralName: 'Alerts', + testUtilsFolderName: '../test-utils', + }, +]; + +describe(`${generateTestUtils.name}`, () => { + beforeAll(() => { + vi.mocked(readFileSync).mockReturnValue('file content'); + vi.mocked(convertToSelectorUtil).mockImplementation(input => input); + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('generates the selector utils for the given dom utils', () => { + generateTestUtils({ + components: mockComponents, + testUtilsPath: './test/mock-test-utils', + }); + + expect(convertToSelectorUtil).toHaveBeenCalledWith('file content'); + }); + + test('throws an error with the path if the specified path has no matching file', () => { + const runGenerateTestUtils = () => + generateTestUtils({ + components: mockComponents, + testUtilsPath: './some-invalid-path/mock-test-utils', + }); + + expect(runGenerateTestUtils).toThrowError( + new Error(`No file with ts or tsx extension found at: some-invalid-path/mock-test-utils/dom`) + ); + + expect(convertToSelectorUtil).not.toHaveBeenCalled(); + }); + + const testUtilsType = ['dom', 'selectors'] as const; + test.each(testUtilsType)('generates the index file for %s test utils', testUtilType => { + generateTestUtils({ + components: mockComponents, + testUtilsPath: './test/mock-test-utils', + }); + + const testUtilsFilePartialContent = 'ElementWrapper.prototype.findAlert'; + + expect(writeFileSync).toHaveBeenCalledWith( + `test/mock-test-utils/${testUtilType}/index.ts`, + expect.stringMatching(testUtilsFilePartialContent) + ); + }); +}); diff --git a/src/converter/test/mock-test-utils/dom/test-component-a/index.ts b/src/converter/test/mock-test-utils/dom/test-component-a/index.ts new file mode 100644 index 0000000..be88f90 --- /dev/null +++ b/src/converter/test/mock-test-utils/dom/test-component-a/index.ts @@ -0,0 +1,11 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ComponentWrapper, createWrapper } from '@cloudscape-design/test-utils-core/dom'; + +export default class TestComponentAWrapper extends ComponentWrapper { + static rootSelector = 'test-component-a-root'; + + findChild() { + return createWrapper().find('.test-component-a-child'); + } +} diff --git a/src/converter/test/mock-test-utils/dom/test-component-b/child-wrapper.ts b/src/converter/test/mock-test-utils/dom/test-component-b/child-wrapper.ts new file mode 100644 index 0000000..9f72569 --- /dev/null +++ b/src/converter/test/mock-test-utils/dom/test-component-b/child-wrapper.ts @@ -0,0 +1,11 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ComponentWrapper, createWrapper } from '@cloudscape-design/test-utils-core/dom'; + +export class ChildWrapper extends ComponentWrapper { + static rootSelector = 'test-component-b-child'; + + findContent() { + return createWrapper().find('.test-component-b-child-content'); + } +} diff --git a/src/converter/test/mock-test-utils/dom/test-component-b/index.ts b/src/converter/test/mock-test-utils/dom/test-component-b/index.ts new file mode 100644 index 0000000..4fecf06 --- /dev/null +++ b/src/converter/test/mock-test-utils/dom/test-component-b/index.ts @@ -0,0 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ComponentWrapper, createWrapper } from '@cloudscape-design/test-utils-core/dom'; +import { ChildWrapper } from './child-wrapper'; + +export default class TestComponentBWrapper extends ComponentWrapper { + static rootSelector = 'test-component-b-root'; + + findChild(): ChildWrapper { + return createWrapper().find(`.${ChildWrapper.rootSelector}`); + } +} diff --git a/src/converter/test/test-utils-generator-snapshot.test.ts b/src/converter/test/test-utils-generator-snapshot.test.ts new file mode 100644 index 0000000..dfa75f1 --- /dev/null +++ b/src/converter/test/test-utils-generator-snapshot.test.ts @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import fs from 'fs'; +import path from 'path'; +import { describe, test, expect } from 'vitest'; + +describe('index files', () => { + const testUtilTypes = ['dom', 'selectors'] as const; + + test.each(testUtilTypes)('%s index file matches the snapshot', testUtilType => { + const testUtilsIndexFilePath = path.resolve(__dirname, `./mock-test-utils/${testUtilType}/index.ts`); + const content = fs.readFileSync(testUtilsIndexFilePath, { encoding: 'utf8' }); + expect(content).toMatchSnapshot(); + }); +}); diff --git a/src/converter/test/test-utils-generator.test.tsx b/src/converter/test/test-utils-generator.test.tsx new file mode 100644 index 0000000..ae36a6f --- /dev/null +++ b/src/converter/test/test-utils-generator.test.tsx @@ -0,0 +1,104 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; +import ReactDOM from 'react-dom'; +import { generateTestUtils } from '../index'; +import { describe, test, expect } from 'vitest'; + +function renderTestNode() { + const testNode = ( + <> +
+

First Component A

+
+
+
+

First Component B

+
+
+
+

Second Component A

+
+
+
+

Second Component B

+
+
+ + ); + + const container = document.createElement('body'); + ReactDOM.render(testNode, container); + return container; +} + +describe(`${generateTestUtils.name}`, () => { + describe('generated dom index file', () => { + test('component finders return the first matching node', async () => { + const { default: createWrapper } = await import('./mock-test-utils/dom'); + const container = renderTestNode(); + const wrapper = createWrapper(container); + + expect(wrapper.findTestComponentA().getElement().textContent).toBe('First Component A'); + expect(wrapper.findTestComponentB().getElement().textContent).toBe('First Component B'); + }); + + test('component collection finders return all of the matching nodes', async () => { + const { default: createWrapper } = await import('./mock-test-utils/dom'); + const container = renderTestNode(); + const wrapper = createWrapper(container); + + const componentATextContents = wrapper + .findAllTestComponentAs() + .map(wrapper => wrapper.getElement().textContent) + .join(); + const componentBTextContents = wrapper + .findAllTestComponentBs() + .map(wrapper => wrapper.getElement().textContent) + .join(); + + expect(componentATextContents).toBe('First Component A,Second Component A'); + expect(componentBTextContents).toBe('First Component B,Second Component B'); + }); + }); + + describe('generated selectors index file', () => { + test('component finders return the selector matching the first component', async () => { + const { default: createWrapper } = await import('./mock-test-utils/selectors'); + const container = renderTestNode(); + const wrapper = createWrapper(); + + const componentASelector = wrapper.findTestComponentA().toSelector(); + const componentBSelector = wrapper.findTestComponentB().toSelector(); + + expect(container.querySelector(componentASelector).textContent).toBe('First Component A'); + expect(container.querySelector(componentBSelector).textContent).toBe('First Component B'); + }); + + test('component collection finders return multi-wrappers matching the component', async () => { + const { default: createWrapper } = await import('./mock-test-utils/selectors'); + const container = renderTestNode(); + const wrapper = createWrapper(); + + const secondComponentASelector = wrapper.findAllTestComponentAs().get(3).toSelector(); + const secondComponentBSelector = wrapper.findAllTestComponentBs().get(4).toSelector(); + + expect(container.querySelector(secondComponentASelector).textContent).toBe('Second Component A'); + expect(container.querySelector(secondComponentBSelector).textContent).toBe('Second Component B'); + }); + }); + + describe('generated selectors wrapper files', () => { + test('transpiled finders return selectors matching the correct node', async () => { + const { default: createWrapper } = await import('./mock-test-utils/selectors'); + const container = renderTestNode(); + const wrapper = createWrapper(); + + const componentAChildSelector = wrapper.findTestComponentA().findChild().toSelector(); + const componentBChildSelector = wrapper.findTestComponentB().findChild().toSelector(); + + expect(container.querySelector(componentAChildSelector).textContent).toBe('First Component A'); + expect(container.querySelector(componentBChildSelector).textContent).toBe('First Component B'); + }); + }); +}); diff --git a/src/converter/utils.ts b/src/converter/utils.ts new file mode 100644 index 0000000..deea3b6 --- /dev/null +++ b/src/converter/utils.ts @@ -0,0 +1,9 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import fs from 'fs'; +import path from 'path'; + +export function writeSourceFile(filepath: string, content: string) { + fs.mkdirSync(path.dirname(filepath), { recursive: true }); + fs.writeFileSync(filepath, content); +} diff --git a/test/mock-test-utils/dom/index.ts b/test/mock-test-utils/dom/index.ts new file mode 100644 index 0000000..10567f4 --- /dev/null +++ b/test/mock-test-utils/dom/index.ts @@ -0,0 +1,100 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ElementWrapper } from '@cloudscape-design/test-utils-core/dom'; +import { appendSelector } from '@cloudscape-design/test-utils-core/utils'; + +export { ElementWrapper }; + +import AlertWrapper from './../test-utils'; +import StatusWrapper from './../test-utils'; + +export { AlertWrapper }; +export { StatusWrapper }; + +declare module '@cloudscape-design/test-utils-core/dist/dom' { + interface ElementWrapper { + /** + * Returns the wrapper of the first Alert that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first Alert. + * If no matching Alert is found, returns `null`. + * + * @param {string} [selector] CSS Selector + * @returns {AlertWrapper | null} + */ + findAlert(selector?: string): AlertWrapper | null; + + /** + * Returns an array of Alert wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the Alerts inside the current wrapper. + * If no matching Alert is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ + findAllAlerts(selector?: string): Array; + /** + * Returns the wrapper of the first Status that matches the specified CSS selector. + * If no CSS selector is specified, returns the wrapper of the first Status. + * If no matching Status is found, returns `null`. + * + * @param {string} [selector] CSS Selector + * @returns {StatusWrapper | null} + */ + findStatus(selector?: string): StatusWrapper | null; + + /** + * Returns an array of Status wrapper that matches the specified CSS selector. + * If no CSS selector is specified, returns all of the Status inside the current wrapper. + * If no matching Status is found, returns an empty array. + * + * @param {string} [selector] CSS Selector + * @returns {Array} + */ + findAllStatus(selector?: string): Array; + } +} + +ElementWrapper.prototype.findAlert = function (selector) { + const rootSelector = `.${AlertWrapper.rootSelector}`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, AlertWrapper); +}; + +ElementWrapper.prototype.findAllAlerts = function (selector) { + return this.findAllComponents(AlertWrapper, selector); +}; +ElementWrapper.prototype.findStatus = function (selector) { + const rootSelector = `.${StatusWrapper.rootSelector}`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, StatusWrapper); +}; + +ElementWrapper.prototype.findAllStatus = function (selector) { + return this.findAllComponents(StatusWrapper, selector); +}; + +/** + * Returns the component metadata including its plural and wrapper name. + * + * @param {string} componentName Component name in pascal case. + * @returns {ComponentMetadata} + */ +export function getComponentMetadata(componentName: string) { + return { + Alert: { pluralName: 'Alerts', wrapperName: 'AlertWrapper' }, + Status: { pluralName: 'Status', wrapperName: 'StatusWrapper' }, + }[componentName]; +} + +export default function wrapper(root: Element = document.body) { + if (document && document.body && !document.body.contains(root)) { + console.warn( + '[AwsUi] [test-utils] provided element is not part of the document body, interactions may work incorrectly' + ); + } + return new ElementWrapper(root); +} diff --git a/test/mock-test-utils/selectors/index.ts b/test/mock-test-utils/selectors/index.ts new file mode 100644 index 0000000..da1be05 --- /dev/null +++ b/test/mock-test-utils/selectors/index.ts @@ -0,0 +1,91 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ElementWrapper } from '@cloudscape-design/test-utils-core/selectors'; +import { appendSelector } from '@cloudscape-design/test-utils-core/utils'; + +export { ElementWrapper }; + +import AlertWrapper from './../test-utils'; +import StatusWrapper from './../test-utils'; + +export { AlertWrapper }; +export { StatusWrapper }; + +declare module '@cloudscape-design/test-utils-core/dist/selectors' { + interface ElementWrapper { + /** + * Returns a wrapper that matches the Alerts with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches Alerts. + * + * @param {string} [selector] CSS Selector + * @returns {AlertWrapper} + */ + findAlert(selector?: string): AlertWrapper; + + /** + * Returns a multi-element wrapper that matches Alerts with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches Alerts. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ + findAllAlerts(selector?: string): MultiElementWrapper; + /** + * Returns a wrapper that matches the Status with the specified CSS selector. + * If no CSS selector is specified, returns a wrapper that matches Status. + * + * @param {string} [selector] CSS Selector + * @returns {StatusWrapper} + */ + findStatus(selector?: string): StatusWrapper; + + /** + * Returns a multi-element wrapper that matches Status with the specified CSS selector. + * If no CSS selector is specified, returns a multi-element wrapper that matches Status. + * + * @param {string} [selector] CSS Selector + * @returns {MultiElementWrapper} + */ + findAllStatus(selector?: string): MultiElementWrapper; + } +} + +ElementWrapper.prototype.findAlert = function (selector) { + const rootSelector = `.${AlertWrapper.rootSelector}`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, AlertWrapper); +}; + +ElementWrapper.prototype.findAllAlerts = function (selector) { + return this.findAllComponents(AlertWrapper, selector); +}; +ElementWrapper.prototype.findStatus = function (selector) { + const rootSelector = `.${StatusWrapper.rootSelector}`; + // casting to 'any' is needed to avoid this issue with generics + // https://github.com/microsoft/TypeScript/issues/29132 + return (this as any).findComponent(selector ? appendSelector(selector, rootSelector) : rootSelector, StatusWrapper); +}; + +ElementWrapper.prototype.findAllStatus = function (selector) { + return this.findAllComponents(StatusWrapper, selector); +}; + +/** + * Returns the component metadata including its plural and wrapper name. + * + * @param {string} componentName Component name in pascal case. + * @returns {ComponentMetadata} + */ +export function getComponentMetadata(componentName: string) { + return { + Alert: { pluralName: 'Alerts', wrapperName: 'AlertWrapper' }, + Status: { pluralName: 'Status', wrapperName: 'StatusWrapper' }, + }[componentName]; +} + +export default function wrapper(root = 'body') { + return new ElementWrapper(root); +} diff --git a/vite.config.ts b/vite.config.ts index 5ec3863..f09c94d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { defineConfig } from 'vitest/config'; +import path from 'path'; export default defineConfig({ test: { @@ -12,4 +13,9 @@ export default defineConfig({ exclude: ['**/debug-tools/**', '**/test/**'], }, }, + resolve: { + alias: { + '@cloudscape-design/test-utils-core': path.resolve(__dirname, './lib/core'), + }, + }, });