├── .babelrc.json ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── __mocks__ └── styleMock.js ├── __tests__ ├── AutoField.tsx ├── AutoFields.tsx ├── AutoForm.tsx ├── BaseForm.tsx ├── BoolField.tsx ├── DateField.tsx ├── ErrorField.tsx ├── ErrorsField.tsx ├── HiddenField.tsx ├── ListAddField.tsx ├── ListDelField.tsx ├── ListField.tsx ├── ListItemField.tsx ├── LongTextField.tsx ├── NestField.tsx ├── NumField.tsx ├── QuickForm.tsx ├── RadioField.tsx ├── SelectField.tsx ├── SubmitField.tsx ├── TextField.tsx ├── ValidateQuickForm.tsx ├── ValidatedForm.tsx ├── _createContext.ts ├── _createSchema.ts ├── _mount.tsx └── index.ts ├── examples ├── .gitignore ├── App.js ├── CodeBlock.js ├── index.html ├── index.js ├── package.json ├── schema │ ├── graphql-schema.js │ ├── json-schema.js │ └── simple-schema-2.js └── yarn.lock ├── jest.config.js ├── package.json ├── renovate.json ├── scripts ├── prepareRelease.sh ├── publishRelease.sh └── validateRelease.sh ├── setupEnzyme.js ├── src ├── AutoField.tsx ├── AutoFields.tsx ├── AutoForm.tsx ├── BaseForm.tsx ├── BoolField.tsx ├── DateField.tsx ├── ErrorField.tsx ├── ErrorsField.tsx ├── HiddenField.tsx ├── ListAddField.tsx ├── ListDelField.tsx ├── ListField.tsx ├── ListItemField.tsx ├── LongTextField.tsx ├── NestField.tsx ├── NumField.tsx ├── QuickForm.tsx ├── RadioField.tsx ├── SelectField.tsx ├── SubmitField.tsx ├── TextField.tsx ├── ValidatedForm.tsx ├── ValidatedQuickForm.tsx ├── index.ts └── wrapField.tsx ├── transform.js ├── tsconfig.build.json ├── tsconfig.es5.json ├── tsconfig.es6.json ├── tsconfig.esm.json ├── tsconfig.json └── yarn.lock /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [["@babel/plugin-proposal-class-properties", { "loose": false }]], 3 | "presets": [ 4 | [ 5 | "@babel/env", 6 | { 7 | "corejs": 3, 8 | "shippedProposals": true, 9 | "targets": { "node": "current" }, 10 | "useBuiltIns": "usage" 11 | } 12 | ], 13 | ["@babel/react"], 14 | ["@babel/typescript"] 15 | ] 16 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /.idea 3 | /lerna-debug.log 4 | 5 | jest.config.js 6 | **/__tests__/* 7 | 8 | /examples 9 | /__tests__ 10 | /__mocks__ 11 | 12 | /node_modules 13 | /npm-debug.log 14 | /npm-debug.log* 15 | /packages/**/*.d.ts 16 | /packages/**/*.js 17 | /packages/*/es6 18 | /packages/*/node_modules 19 | /website/.cache-loader 20 | /website/.docusaurus 21 | /website/build 22 | /website/node_modules 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:import/errors", "vazco"], 3 | "overrides": [ 4 | { 5 | "files": ["**/*.ts", "**/*.tsx"], 6 | "extends": ["plugin:import/typescript", "vazco/typescript"], 7 | "rules": { 8 | "@typescript-eslint/ban-ts-ignore": "off", 9 | "@typescript-eslint/class-name-casing": [ 10 | "error", 11 | { "allowUnderscorePrefix": true } 12 | ], 13 | "@typescript-eslint/no-non-null-assertion": "off", 14 | "@typescript-eslint/no-unnecessary-type-assertion": "off", 15 | "@typescript-eslint/explicit-function-return-type": "off", 16 | "@typescript-eslint/no-empty-function": "off", 17 | "@typescript-eslint/no-explicit-any": "off", 18 | "@typescript-eslint/no-unused-vars": "off", 19 | "@typescript-eslint/no-use-before-define": "off", 20 | "@typescript-eslint/require-await": "off", 21 | "@typescript-eslint/unbound-method": "off", 22 | "import/default": "off", 23 | "prefer-rest-params": "off", 24 | "react/no-children-prop": "off", 25 | "react/prop-types": "off", 26 | "prettier/prettier": "off", 27 | "complexity": "off" 28 | }, 29 | "settings": { 30 | "import/resolver": { 31 | "typescript": {} 32 | } 33 | } 34 | }, 35 | { 36 | "files": ["**/__mocks__/**/*", "**/__tests__/*"], 37 | "env": { 38 | "jest": true 39 | } 40 | }, 41 | { 42 | "files": ["website/**/*"], 43 | "settings": { 44 | "import/resolver": { 45 | "alias": { 46 | "map": [ 47 | ["@docusaurus", "@docusaurus/core/lib/client/exports"], 48 | ["@theme", "@docusaurus/theme-classic/src/theme"] 49 | ] 50 | } 51 | } 52 | } 53 | } 54 | ], 55 | "root": true, 56 | "rules": { 57 | "import/default": "off", 58 | "import/order": [ 59 | "error", 60 | { 61 | "groups": [ 62 | ["builtin", "external", "internal"], 63 | ["index", "parent", "sibling"], 64 | ["unknown"] 65 | ], 66 | "newlines-between": "always" 67 | } 68 | ], 69 | "react/prop-types": "off" 70 | }, 71 | "settings": { 72 | "react": { 73 | "version": "detect" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build, Lint & Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | types: [opened, synchronize, reopened, ready_for_review] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 60 13 | steps: 14 | - name: Checkout master 15 | uses: actions/checkout@v2 16 | - name: Use Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 12.x 20 | - name: Cache Dependencies 21 | id: cache-dependencies 22 | uses: actions/cache@master 23 | with: 24 | path: | 25 | node_modules 26 | */*/node_modules 27 | key: ${{ runner.os }}-${{ hashFiles('**/package.json') }} 28 | - name: Install dependencies 29 | if: steps.cache-dependencies.outputs.cache-hit != 'true' 30 | run: yarn 31 | - name: Build 32 | run: yarn build 33 | - name: Lint 34 | run: yarn lint 35 | - name: Unit Test 36 | run: yarn test -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release workflow 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 60 10 | env: 11 | CI: true 12 | steps: 13 | - name: Checkout master 14 | uses: actions/checkout@v2 15 | - name: Use Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 12.x 19 | - name: Cache Dependencies 20 | id: cache-dependencies 21 | uses: actions/cache@master 22 | with: 23 | path: | 24 | node_modules 25 | */*/node_modules 26 | key: ${{ runner.os }}-${{ hashFiles('**/package.json') }} 27 | - name: Build 28 | run: yarn build 29 | - name: Lint 30 | run: yarn lint 31 | - name: Unit Tests 32 | run: yarn test 33 | - name: Publish 34 | run: echo "//registry.npmjs.org/:_authToken=${{secrets.NPM_AUTH_TOKEN}}" > ~/.npmrc && TAG=${GITHUB_REF#"refs/tags/"} npm run release:publish -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Custom 107 | reproductions 108 | .idea/ 109 | 110 | /dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | __mocks__ 3 | .vscode 4 | .circleci 5 | examples 6 | coverage 7 | jest.config.js 8 | .prettierignore 9 | .prettierrc.json 10 | .eslintignore 11 | .eslintrc.json 12 | .babelrc.json 13 | setupEnzyme.js 14 | transform.js -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /.idea 3 | /lerna-debug.log 4 | /lerna.json 5 | /node_modules 6 | /npm-debug.log 7 | /npm-debug.log* 8 | /packages/**/*.d.ts 9 | /packages/**/*.js 10 | /packages/*/es6 11 | /packages/*/node_modules 12 | /website/.cache-loader 13 | /website/.docusaurus 14 | /website/build 15 | /website/node_modules 16 | 17 | jest.config.js 18 | 19 | # Broke with Prettier upgrade. 20 | /docs/examples-common-forms.mdx 21 | /docs/examples-custom-fields.mdx 22 | /docs/installation.mdx 23 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 80, 4 | "singleQuote": true, 5 | "tabWidth": 2 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Current TS File", 6 | "type": "node", 7 | "request": "launch", 8 | "env": {}, 9 | "args": [ 10 | "${relativeFile}" 11 | ], 12 | "runtimeArgs": [ 13 | "-r", 14 | "ts-node/register" 15 | ], 16 | "cwd": "${workspaceRoot}", 17 | "protocol": "inspector", 18 | "internalConsoleOptions": "openOnSessionStart" 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Jest Current File", 24 | "program": "${workspaceFolder}/node_modules/.bin/jest", 25 | "args": [ 26 | "${fileBasenameNoExtension}", 27 | "--config", 28 | "jest.config.js" 29 | ], 30 | "console": "integratedTerminal", 31 | "internalConsoleOptions": "neverOpen", 32 | "disableOptimisticBPs": true, 33 | "windows": { 34 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repository was moved to kie-tools 2 | This repository was moved to [kie-tools monorepo](https://github.com/kiegroup/kie-tools/tree/main/packages/uniforms-patternfly). The kie-tools monorepo uses this library in some of their packages. Therefore, the maintainability of the library will be improved, and things such as bug fixing and updating dependencies will be easier as well. This package will still exist in `npm`, and the new package maintained by kie-tools will be `@kie-tools/uniforms-patternfly`. 3 | 4 | # Basic usage 5 | 6 | `uniforms` is a plugin for React to be able to create dynamic forms with built-in state management and form validation. 7 | `uniforms` provides you with simple re-usable form components which allows for rapid prototyping and cleaner React components. 8 | 9 | This package extends uniforms to provide [Patternfly React](https://www.patternfly.org/v4/) components inside your forms. 10 | For more information about `uniforms` please go to https://uniforms.tools/ 11 | 12 | Looking for building mobile enabled forms? Check [Uniforms-ionic](https://github.com/aerogear/uniforms-ionic) package that provides Ionic extensions 13 | 14 | ### 1. Install the required packages 15 | 16 | To start using uniforms, we have to install three independent packages: 17 | 18 | 1. Core 19 | 2. Bridge 20 | 3. Theme 21 | 22 | In this example, we will use the JSON Schema to describe our desired data format and style our form using the Pattenfly UI theme. 23 | 24 | ```shell 25 | npm install uniforms@3.6.2 26 | npm install uniforms-bridge-json-schema@3.6.2 27 | npm install uniforms-patternfly 28 | npm install @patternfly/react-core @patternfly/react-icons 29 | ``` 30 | 31 | Don't forget that it's necessary to correctly load the styles from Patternfly. To do it, we recommend taking a look into the 32 | [Patternfly React Seed](https://github.com/patternfly/patternfly-react-seed), or you can simply load the styles directly into 33 | your `index.html` like in the example app of this repo. 34 | 35 | Obs: If you use a previous version of the `tslib` indirectly (version 1), it should be necessary to add this dependency as well. 36 | ```shell 37 | npm install tslib@^2.3.1 38 | ``` 39 | 40 | ### 2. Start by defining a schema 41 | 42 | After we've installed required packages, it's time to define our schema. We can do it in a plain JSON, which is a valid JSON Schema instance: 43 | 44 | ```js 45 | const schema = { 46 | type: 'object', 47 | properties: { 48 | foo: { 49 | type: 'string' 50 | }, 51 | } 52 | }; 53 | 54 | ``` 55 | 56 | ### 3. Then create the bridge 57 | 58 | Now that we have the schema, we can create the uniforms bridge of it, by using the corresponding uniforms bridge package. 59 | Creating the bridge instance is necessary - without it, uniforms would not be able to process form generation and validation. 60 | As we are using the JSON Schema, we have to import the `uniforms-bridge-json-schema` package. Also, because we're doing an 61 | example of a JSON Schema, it's necessary to use a JSON Schema validation library, and in this example we'll be using the AJV. 62 | 63 | ```js 64 | import { JSONSchemaBridge } from 'uniforms-bridge-json-schema'; 65 | import AJV from 'ajv'; 66 | 67 | const ajv = new Ajv({ allErrors: true, useDefaults: true }); 68 | 69 | function createValidator(schema) { 70 | const validator = ajv.compile(schema); 71 | 72 | return (model) => { 73 | validator(model); 74 | return validator.errors?.length ? { details: validator.errors } : null; 75 | }; 76 | } 77 | 78 | const bridge = new JSONSchemaBridge(schema, createValidator(schema)); 79 | ``` 80 | 81 | ### 4. Finally, use it in a form! 🎉 82 | 83 | Uniforms theme packages provide the `AutoForm` component, which is able to generate the form based on the given schema. 84 | All we have to do now is to pass the previously created Bridge to the `AutoForm`: 85 | 86 | ```js 87 | import React from 'react'; 88 | import { AutoForm } from 'uniforms-patternfly'; 89 | 90 | import schema from './schema'; 91 | 92 | export default function MyForm() { 93 | return ; 94 | } 95 | ``` 96 | 97 | And that's it! `AutoForm` will generate a complete form with labeled fields, errors list (if any) and a submit button. 98 | 99 | Also, it will take care of validation and handle model changes. In case you need more advanced feature, take a deeper look 100 | into the Uniforms docs. 101 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /__tests__/AutoField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | AutoField, 4 | BoolField, 5 | DateField, 6 | ListField, 7 | NestField, 8 | NumField, 9 | RadioField, 10 | SelectField, 11 | TextField, 12 | } from '../src'; 13 | 14 | import createContext from './_createContext'; 15 | import mount from './_mount'; 16 | 17 | test(' - works', () => { 18 | const element = ; 19 | const wrapper = mount(element, createContext({ x: { type: String } })); 20 | 21 | expect(wrapper.find(AutoField)).toHaveLength(1); 22 | }); 23 | 24 | test(' - renders RadioField', () => { 25 | const element = ; 26 | const wrapper = mount( 27 | element, 28 | createContext({ 29 | x: { 30 | type: String, 31 | allowedValues: ['x', 'y'], 32 | uniforms: { checkboxes: true }, 33 | }, 34 | }), 35 | ); 36 | 37 | expect(wrapper.find(RadioField)).toHaveLength(1); 38 | }); 39 | 40 | test(' - renders SelectField', () => { 41 | const element = ; 42 | const wrapper = mount( 43 | element, 44 | createContext({ 45 | x: { type: Array, allowedValues: ['x', 'y'] }, 46 | 'x.$': { type: String }, 47 | }), 48 | ); 49 | 50 | expect(wrapper.find(SelectField)).toHaveLength(1); 51 | }); 52 | 53 | test(' - renders DateField', () => { 54 | const element = ; 55 | const wrapper = mount(element, createContext({ x: { type: Date } })); 56 | 57 | expect(wrapper.find(DateField)).toHaveLength(1); 58 | }); 59 | 60 | test(' - renders ListField', () => { 61 | const element = ; 62 | const wrapper = mount( 63 | element, 64 | createContext({ x: { type: Array }, 'x.$': { type: String } }), 65 | ); 66 | 67 | expect(wrapper.find(ListField)).toHaveLength(1); 68 | }); 69 | 70 | test(' - renders NumField', () => { 71 | const element = ; 72 | const wrapper = mount(element, createContext({ x: { type: Number } })); 73 | 74 | expect(wrapper.find(NumField)).toHaveLength(1); 75 | }); 76 | 77 | test(' - renders NestField', () => { 78 | const element = ; 79 | const wrapper = mount(element, createContext({ x: { type: Object } })); 80 | 81 | expect(wrapper.find(NestField)).toHaveLength(1); 82 | }); 83 | 84 | test(' - renders TextField', () => { 85 | const element = ; 86 | const wrapper = mount(element, createContext({ x: { type: String } })); 87 | 88 | expect(wrapper.find(TextField)).toHaveLength(1); 89 | }); 90 | 91 | test(' - renders BoolField', () => { 92 | const element = ; 93 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 94 | 95 | expect(wrapper.find(BoolField)).toHaveLength(1); 96 | }); 97 | 98 | test(' - renders Component (model)', () => { 99 | const Component = jest.fn(() => null); 100 | 101 | const element = ; 102 | mount( 103 | element, 104 | createContext({ x: { type: String, uniforms: { component: Component } } }), 105 | ); 106 | 107 | expect(Component).toHaveBeenCalledTimes(1); 108 | }); 109 | 110 | test(' - renders Component (specified)', () => { 111 | const Component = jest.fn(() => null); 112 | 113 | const element = ; 114 | mount(element, createContext({ x: { type: String } })); 115 | 116 | expect(Component).toHaveBeenCalledTimes(1); 117 | }); 118 | -------------------------------------------------------------------------------- /__tests__/AutoFields.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AutoFields } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | test(' - works', () => { 8 | const element = ; 9 | const wrapper = mount(element, createContext({ x: { type: String } })); 10 | 11 | expect(wrapper.find('AutoFields')).toHaveLength(1); 12 | }); 13 | 14 | test(' - render all fields by default', () => { 15 | const element = ; 16 | const wrapper = mount( 17 | element, 18 | createContext({ 19 | x: { type: String }, 20 | y: { type: String }, 21 | z: { type: String }, 22 | }), 23 | ); 24 | 25 | expect(wrapper.find('input')).toHaveLength(3); 26 | }); 27 | 28 | test(' - renders only specified fields', () => { 29 | const element = ; 30 | const wrapper = mount( 31 | element, 32 | createContext({ 33 | x: { type: String }, 34 | y: { type: String }, 35 | z: { type: String }, 36 | }), 37 | ); 38 | 39 | expect(wrapper.find('input').someWhere(e => e.prop('name') === 'z')).toBe( 40 | false, 41 | ); 42 | }); 43 | 44 | test(' - does not render ommited fields', () => { 45 | const element = ; 46 | const wrapper = mount( 47 | element, 48 | createContext({ 49 | x: { type: String }, 50 | y: { type: String }, 51 | z: { type: String }, 52 | }), 53 | ); 54 | 55 | expect(wrapper.find('input').someWhere(e => e.prop('name') === 'x')).toBe( 56 | false, 57 | ); 58 | }); 59 | 60 | test(' - works with custom component', () => { 61 | const Component = jest.fn(() => null); 62 | 63 | const element = ; 64 | mount( 65 | element, 66 | createContext({ 67 | x: { type: String }, 68 | y: { type: String }, 69 | z: { type: String }, 70 | }), 71 | ); 72 | 73 | expect(Component).toHaveBeenCalledTimes(3); 74 | }); 75 | 76 | test(' - wraps fields in specified element', () => { 77 | const element = ; 78 | const wrapper = mount( 79 | element, 80 | createContext({ 81 | x: { type: String }, 82 | y: { type: String }, 83 | z: { type: String }, 84 | }), 85 | ); 86 | 87 | expect(wrapper.find('section').find('input')).toHaveLength(3); 88 | }); 89 | -------------------------------------------------------------------------------- /__tests__/AutoForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AutoForm } from '../src'; 3 | 4 | import createSchema from './_createSchema'; 5 | import mount from './_mount'; 6 | 7 | test(' - works', () => { 8 | const element = ; 9 | const wrapper = mount(element); 10 | 11 | expect(wrapper.find(AutoForm)).toHaveLength(1); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/BaseForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BaseForm } from '../src'; 3 | 4 | import createSchema from './_createSchema'; 5 | import mount from './_mount'; 6 | 7 | test(' - works', () => { 8 | const element = ; 9 | const wrapper = mount(element); 10 | 11 | expect(wrapper.find(BaseForm)).toHaveLength(1); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/BoolField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BoolField } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | test(' - renders an input', () => { 8 | const element = ; 9 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 10 | 11 | expect(wrapper.find('input')).toHaveLength(1); 12 | }); 13 | 14 | test(' - renders a input with correct id (inherited)', () => { 15 | const element = ; 16 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 17 | 18 | expect(wrapper.find('input')).toHaveLength(1); 19 | expect(wrapper.find('input').prop('id')).toBeTruthy(); 20 | }); 21 | 22 | test(' - renders a input with correct id (specified)', () => { 23 | const element = ; 24 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 25 | 26 | expect(wrapper.find('input')).toHaveLength(1); 27 | expect(wrapper.find('input').prop('id')).toBe('y'); 28 | }); 29 | 30 | test(' - renders a input with correct name', () => { 31 | const element = ; 32 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 33 | 34 | expect(wrapper.find('input')).toHaveLength(1); 35 | expect(wrapper.find('input').prop('name')).toBe('x'); 36 | }); 37 | 38 | test(' - renders an input with correct type', () => { 39 | const element = ; 40 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 41 | 42 | expect(wrapper.find('input')).toHaveLength(1); 43 | expect(wrapper.find('input').prop('type')).toBe('checkbox'); 44 | }); 45 | 46 | test(' - renders an input with correct disabled state', () => { 47 | const element = ; 48 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 49 | 50 | expect(wrapper.find('input')).toHaveLength(1); 51 | expect(wrapper.find('input').prop('disabled')).toBe(true); 52 | }); 53 | 54 | test(' - renders a input with correct label (specified)', () => { 55 | const element = ; 56 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 57 | 58 | expect(wrapper.find('label')).toHaveLength(1); 59 | expect(wrapper.find('label').text()).toBe('BoolFieldLabel'); 60 | expect(wrapper.find('label').prop('htmlFor')).toBe( 61 | wrapper.find('input').prop('id'), 62 | ); 63 | }); 64 | 65 | test(' - renders a input with correct value (default)', () => { 66 | const element = ; 67 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 68 | 69 | expect(wrapper.find('input')).toHaveLength(1); 70 | expect(wrapper.find('input').prop('checked')).toBe(false); 71 | }); 72 | 73 | test(' - renders a input with correct value (model)', () => { 74 | const element = ; 75 | const wrapper = mount( 76 | element, 77 | createContext({ x: { type: Boolean } }, { model: { x: true } }), 78 | ); 79 | 80 | expect(wrapper.find('input')).toHaveLength(1); 81 | expect(wrapper.find('input').prop('checked')).toBe(true); 82 | }); 83 | 84 | test(' - renders a input with correct value (specified)', () => { 85 | const element = ; 86 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 87 | 88 | expect(wrapper.find('input')).toHaveLength(1); 89 | expect(wrapper.find('input').prop('checked')).toBe(true); 90 | }); 91 | 92 | test(' - renders a input which correctly reacts on change', () => { 93 | const onChange = jest.fn(); 94 | 95 | const element = ; 96 | const wrapper = mount( 97 | element, 98 | createContext({ x: { type: Boolean } }, { onChange }), 99 | ); 100 | 101 | expect(wrapper.find('input')).toHaveLength(1); 102 | expect(wrapper.find('input').simulate('change')).toBeTruthy(); 103 | expect(onChange).toHaveBeenLastCalledWith('x', true); 104 | }); 105 | 106 | test(' - renders a wrapper with unknown props', () => { 107 | const element = ; 108 | const wrapper = mount(element, createContext({ x: { type: Boolean } })); 109 | 110 | expect( 111 | wrapper 112 | .find('div') 113 | .at(0) 114 | .prop('data-x'), 115 | ).toBe('x'); 116 | expect( 117 | wrapper 118 | .find('div') 119 | .at(0) 120 | .prop('data-y'), 121 | ).toBe('y'); 122 | expect( 123 | wrapper 124 | .find('div') 125 | .at(0) 126 | .prop('data-z'), 127 | ).toBe('z'); 128 | }); 129 | -------------------------------------------------------------------------------- /__tests__/DateField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { act } from 'react-dom/test-utils'; 3 | import { DateField } from '../src'; 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | test(' - renders an input', () => { 8 | const element = ; 9 | const wrapper = mount(element, createContext({ x: { type: Date } })); 10 | 11 | expect(wrapper.find('input')).toHaveLength(2); 12 | }); 13 | 14 | test(' - renders a input with correct id (inherited)', () => { 15 | const element = ; 16 | const wrapper = mount(element, createContext({ x: { type: Date } })); 17 | 18 | expect(wrapper.find('input')).toHaveLength(2); 19 | expect(wrapper.find('DatePicker').prop('id')).toBeTruthy(); 20 | expect(wrapper.find('TimePicker').prop('id')).toBeTruthy(); 21 | }); 22 | 23 | test(' - renders a input with correct id (specified)', () => { 24 | const element = ; 25 | const wrapper = mount(element, createContext({ x: { type: Date } })); 26 | 27 | expect(wrapper.find('input')).toHaveLength(2); 28 | expect(wrapper.find('Flex').prop('id')).toBe('y'); 29 | expect(wrapper.find('DatePicker').prop('id')).toBe('date-picker-y'); 30 | expect(wrapper.find('TimePicker').prop('id')).toBe('time-picker-y'); 31 | }); 32 | 33 | test(' - renders a input with correct name', () => { 34 | const element = ; 35 | const wrapper = mount(element, createContext({ x: { type: Date } })); 36 | 37 | expect(wrapper.find('input')).toHaveLength(2); 38 | expect(wrapper.find('Flex').prop('name')).toBe('x'); 39 | }); 40 | 41 | test(' - renders an input with correct disabled state', () => { 42 | const element = ; 43 | const wrapper = mount(element, createContext({ x: { type: Date } })); 44 | 45 | expect(wrapper.find('input')).toHaveLength(2); 46 | expect(wrapper.find('DatePicker').prop('isDisabled')).toBe(true); 47 | expect(wrapper.find('TimePicker').prop('isDisabled')).toBe(true); 48 | }); 49 | 50 | test(' - renders a input with correct label (specified)', () => { 51 | const element = ( 52 | 53 | ); 54 | const wrapper = mount(element, createContext({ x: { type: Date } })); 55 | 56 | expect(wrapper.find('label')).toHaveLength(1); 57 | expect(wrapper.find('label').text()).toBe('DateFieldLabel'); 58 | expect(wrapper.find('label').prop('htmlFor')).toBe( 59 | wrapper.find('Flex').prop('id') 60 | ); 61 | }); 62 | 63 | test(' - renders a input with correct label (specified)', () => { 64 | const element = ; 65 | const wrapper = mount(element, createContext({ x: { type: Date } })); 66 | 67 | expect(wrapper.find('label')).toHaveLength(1); 68 | expect(wrapper.find('label').text()).toBe('DateFieldLabel *'); 69 | expect(wrapper.find('label').prop('htmlFor')).toBe( 70 | wrapper.find('Flex').prop('id') 71 | ); 72 | }); 73 | 74 | test(' - renders a input with correct value (default)', () => { 75 | const element = ; 76 | const wrapper = mount(element, createContext({ x: { type: Date } })); 77 | 78 | expect(wrapper.find('input')).toHaveLength(2); 79 | expect(wrapper.find('DatePicker').find('input').prop('value')).toBe(''); 80 | expect(wrapper.find('TimePicker').find('input').prop('value')).toBe(''); 81 | }); 82 | 83 | test(' - renders a input with correct value (model)', () => { 84 | const now = new Date(); 85 | const element = ; 86 | const wrapper = mount( 87 | element, 88 | createContext({ x: { type: Date } }, { model: { x: now } }) 89 | ); 90 | 91 | expect(wrapper.find('input')).toHaveLength(2); 92 | expect(wrapper.find('TimePicker').prop('value')).toEqual( 93 | `${now.getUTCHours()}:${now.getUTCMinutes()}` 94 | ); 95 | }); 96 | 97 | test(' - renders a input with correct value (specified)', () => { 98 | const now = new Date(); 99 | const element = ; 100 | const wrapper = mount(element, createContext({ x: { type: Date } })); 101 | 102 | expect(wrapper.find('input')).toHaveLength(2); 103 | expect(wrapper.find('DatePicker').find('input').prop('value')).toEqual( 104 | now.toISOString().slice(0, -14) 105 | ); 106 | }); 107 | 108 | test(' - renders a input which correctly reacts on change (DatePicker)', () => { 109 | const onChange = jest.fn(); 110 | 111 | const now = '2000-04-04'; 112 | const element = ; 113 | const wrapper = mount( 114 | element, 115 | createContext({ x: { type: Date } }, { onChange }) 116 | ); 117 | 118 | act(() => { 119 | wrapper.find('DatePicker').find('input').prop('onChange')!({ 120 | currentTarget: { value: now }, 121 | } as any); 122 | }); 123 | 124 | expect(onChange).toHaveBeenLastCalledWith('x', new Date(`${now}T00:00:00Z`)); 125 | }); 126 | 127 | test(' - renders a input which correctly reacts on change (DatePicker - empty)', () => { 128 | const onChange = jest.fn(); 129 | 130 | const element = ; 131 | const wrapper = mount( 132 | element, 133 | createContext({ x: { type: Date } }, { onChange }) 134 | ); 135 | 136 | act(() => { 137 | wrapper.find('DatePicker').find('input').prop('onChange')!({ 138 | currentTarget: { value: '' }, 139 | } as any); 140 | }); 141 | 142 | expect(onChange).toHaveBeenLastCalledWith('x', undefined); 143 | }); 144 | 145 | test(' - renders a input which correctly reacts on change (TimePicker - invalid)', () => { 146 | const onChange = jest.fn(); 147 | 148 | const now = '10:00'; 149 | const element = ; 150 | const wrapper = mount( 151 | element, 152 | createContext({ x: { type: Date } }, { onChange }) 153 | ); 154 | 155 | act(() => { 156 | wrapper.find('TimePicker').find('input').prop('onChange')!({ 157 | currentTarget: { value: now }, 158 | } as any); 159 | }); 160 | 161 | expect(onChange).not.toHaveBeenCalled(); 162 | }); 163 | 164 | test(' - renders a input which correctly reacts on change (TimePicker - valid)', () => { 165 | const onChange = jest.fn(); 166 | 167 | const date = '2000-04-04'; 168 | const time = '10:30'; 169 | const element = ; 170 | const wrapper = mount( 171 | element, 172 | createContext({ x: { type: Date } }, { onChange }) 173 | ); 174 | 175 | act(() => { 176 | wrapper.find('TimePicker').find('input').prop('onChange')!({ 177 | currentTarget: { value: time }, 178 | } as any); 179 | }); 180 | 181 | expect(onChange).toHaveBeenLastCalledWith('x', new Date(`${date}T10:30:00Z`)); 182 | }); 183 | 184 | test(' - renders a wrapper with unknown props', () => { 185 | const element = ; 186 | const wrapper = mount(element, createContext({ x: { type: Date } })); 187 | 188 | expect(wrapper.find('div').at(0).prop('data-x')).toBe('x'); 189 | expect(wrapper.find('div').at(0).prop('data-y')).toBe('y'); 190 | expect(wrapper.find('div').at(0).prop('data-z')).toBe('z'); 191 | }); 192 | 193 | test(' - test max property - valid', () => { 194 | const onChange = jest.fn(); 195 | 196 | const date = '1998-12-31'; 197 | const max = '1999-01-01T00:00:00Z'; 198 | const element = ( 199 | 200 | ); 201 | const wrapper = mount( 202 | element, 203 | createContext({ x: { type: Date } }, { onChange }) 204 | ); 205 | 206 | expect(wrapper.text().includes('Should be before')).toBe(false); 207 | }); 208 | 209 | test(' - test max property - invalid', () => { 210 | const onChange = jest.fn(); 211 | 212 | const date = '1999-01-02'; 213 | const max = '1999-01-01T00:00:00Z'; 214 | const element = ( 215 | 216 | ); 217 | const wrapper = mount( 218 | element, 219 | createContext({ x: { type: Date } }, { onChange }) 220 | ); 221 | 222 | expect(wrapper.text().includes('Should be before')).toBe(true); 223 | }); 224 | 225 | test(' - test min property - valid', () => { 226 | const onChange = jest.fn(); 227 | 228 | const date = '1999-01-02'; 229 | const min = '1999-01-01T00:00:00Z'; 230 | const element = ( 231 | 232 | ); 233 | const wrapper = mount( 234 | element, 235 | createContext({ x: { type: Date } }, { onChange }) 236 | ); 237 | 238 | expect(wrapper.text().includes('Should be after')).toBe(false); 239 | }); 240 | 241 | test(' - test min property - invalid', () => { 242 | const onChange = jest.fn(); 243 | 244 | const date = '1998-12-31'; 245 | const min = '1999-01-01T00:00:00Z'; 246 | const element = ( 247 | 248 | ); 249 | const wrapper = mount( 250 | element, 251 | createContext({ x: { type: Date } }, { onChange }) 252 | ); 253 | 254 | expect(wrapper.text().includes('Should be after')).toBe(true); 255 | }); 256 | -------------------------------------------------------------------------------- /__tests__/ErrorField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ErrorField } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | const error = { 8 | error: 'validation-error', 9 | reason: 'X is required', 10 | details: [{ name: 'x', type: 'required', details: { value: null } }], 11 | message: 'X is required [validation-error]', 12 | }; 13 | 14 | test(' - works', () => { 15 | const element = ; 16 | const wrapper = mount(element, createContext({ x: { type: String } })); 17 | 18 | expect(wrapper.find(ErrorField)).toHaveLength(1); 19 | }); 20 | 21 | test(' - renders correct error message (context)', () => { 22 | const element = ; 23 | const wrapper = mount( 24 | element, 25 | createContext({ x: { type: String } }, { error }), 26 | ); 27 | 28 | expect(wrapper.find(ErrorField)).toHaveLength(1); 29 | expect(wrapper.find(ErrorField).text()).toBe('X is required'); 30 | }); 31 | 32 | test(' - renders correct error message (specified)', () => { 33 | const element = ( 34 | 39 | ); 40 | const wrapper = mount(element, createContext({ x: { type: String } })); 41 | 42 | expect(wrapper.find(ErrorField)).toHaveLength(1); 43 | expect(wrapper.find(ErrorField).text()).toBe('X is required'); 44 | }); 45 | -------------------------------------------------------------------------------- /__tests__/ErrorsField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ErrorsField } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | const error = { 8 | error: 'validation-error', 9 | reason: 'X is required', 10 | details: [ 11 | { name: 'x', type: 'required', details: { value: null } }, 12 | { name: 'y', type: 'required', details: { value: null } }, 13 | { name: 'z', type: 'required', details: { value: null } }, 14 | ], 15 | message: 'X is required [validation-error]', 16 | }; 17 | 18 | test(' - works', () => { 19 | const element = ; 20 | const wrapper = mount(element, createContext({ x: { type: String } })); 21 | 22 | expect(wrapper.find(ErrorsField)).toHaveLength(1); 23 | }); 24 | 25 | test(' - renders list of correct error messages (context)', () => { 26 | const element = ; 27 | const wrapper = mount( 28 | element, 29 | createContext( 30 | { x: { type: String }, y: { type: String }, z: { type: String } }, 31 | { error }, 32 | ), 33 | ); 34 | 35 | expect(wrapper.find('li')).toHaveLength(3); 36 | expect( 37 | wrapper 38 | .find('li') 39 | .at(0) 40 | .text(), 41 | ).toBe('X is required'); 42 | expect( 43 | wrapper 44 | .find('li') 45 | .at(1) 46 | .text(), 47 | ).toBe('Y is required'); 48 | expect( 49 | wrapper 50 | .find('li') 51 | .at(2) 52 | .text(), 53 | ).toBe('Z is required'); 54 | }); 55 | 56 | test(' - renders children (specified)', () => { 57 | const element = ; 58 | const wrapper = mount( 59 | element, 60 | createContext( 61 | { x: { type: String }, y: { type: String }, z: { type: String } }, 62 | { error }, 63 | ), 64 | ); 65 | 66 | expect(wrapper.find(ErrorsField).text()).toEqual( 67 | expect.stringContaining('Error message list'), 68 | ); 69 | }); 70 | -------------------------------------------------------------------------------- /__tests__/HiddenField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { HiddenField } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | test(' - renders an input', () => { 8 | const element = ; 9 | const wrapper = mount(element, createContext({ x: { type: String } })); 10 | 11 | expect(wrapper.find('input')).toHaveLength(1); 12 | }); 13 | 14 | test(' - renders an input with correct id (inherited)', () => { 15 | const element = ; 16 | const wrapper = mount(element, createContext({ x: { type: String } })); 17 | 18 | expect(wrapper.find('input')).toHaveLength(1); 19 | expect(wrapper.find('input').prop('id')).toBeTruthy(); 20 | }); 21 | 22 | test(' - renders an input with correct id (specified)', () => { 23 | const element = ; 24 | const wrapper = mount(element, createContext({ x: { type: String } })); 25 | 26 | expect(wrapper.find('input')).toHaveLength(1); 27 | expect(wrapper.find('input').prop('id')).toBe('y'); 28 | }); 29 | 30 | test(' - renders an input with correct name', () => { 31 | const element = ; 32 | const wrapper = mount(element, createContext({ x: { type: String } })); 33 | 34 | expect(wrapper.find('input')).toHaveLength(1); 35 | expect(wrapper.find('input').prop('name')).toBe('x'); 36 | }); 37 | 38 | test(' - renders an input with correct type', () => { 39 | const element = ; 40 | const wrapper = mount(element, createContext({ x: { type: String } })); 41 | 42 | expect(wrapper.find('input')).toHaveLength(1); 43 | expect(wrapper.find('input').prop('type')).toBe('hidden'); 44 | }); 45 | 46 | test(' - renders an input with correct value (default)', () => { 47 | const element = ; 48 | const wrapper = mount(element, createContext({ x: { type: String } })); 49 | 50 | expect(wrapper.find('input')).toHaveLength(1); 51 | expect(wrapper.find('input').prop('value')).toBe(''); 52 | }); 53 | 54 | test(' - renders an input with correct value (model)', () => { 55 | const element = ; 56 | const wrapper = mount( 57 | element, 58 | createContext({ x: { type: String } }, { model: { x: 'y' } }), 59 | ); 60 | 61 | expect(wrapper.find('input')).toHaveLength(1); 62 | expect(wrapper.find('input').prop('value')).toBe('y'); 63 | }); 64 | 65 | test(' - renders an input which correctly reacts on model change', () => { 66 | const onChange = jest.fn(); 67 | 68 | const element = ; 69 | const wrapper = mount( 70 | element, 71 | createContext({ x: { type: String } }, { onChange }), 72 | ); 73 | 74 | wrapper.setProps({ value: 'y' }); 75 | 76 | expect(onChange).toHaveBeenLastCalledWith('x', 'y'); 77 | }); 78 | 79 | test(' - renders an input which correctly reacts on model change (empty)', () => { 80 | const onChange = jest.fn(); 81 | 82 | const element = ; 83 | const wrapper = mount( 84 | element, 85 | createContext({ x: { type: String } }, { onChange }), 86 | ); 87 | 88 | wrapper.setProps({ value: undefined }); 89 | 90 | expect(onChange).not.toHaveBeenCalled(); 91 | }); 92 | 93 | test(' - renders an input which correctly reacts on model change (same value)', () => { 94 | const onChange = jest.fn(); 95 | 96 | const element = ; 97 | const wrapper = mount( 98 | element, 99 | createContext({ x: { type: String } }, { model: { x: 'y' }, onChange }), 100 | ); 101 | 102 | wrapper.setProps({ value: 'y' }); 103 | 104 | expect(onChange).not.toHaveBeenCalled(); 105 | }); 106 | 107 | test(' - renders nothing', () => { 108 | const element = ; 109 | const wrapper = mount(element, createContext({ x: { type: String } })); 110 | 111 | expect(wrapper.children()).toHaveLength(0); 112 | }); 113 | 114 | test(' - renders nothing which correctly reacts on model change', () => { 115 | const onChange = jest.fn(); 116 | 117 | const element = ; 118 | const wrapper = mount( 119 | element, 120 | createContext({ x: { type: String } }, { onChange }), 121 | ); 122 | 123 | wrapper.setProps({ value: 'y' }); 124 | 125 | expect(onChange).toHaveBeenLastCalledWith('x', 'y'); 126 | }); 127 | 128 | test(' - renders nothing which correctly reacts on model change (empty)', () => { 129 | const onChange = jest.fn(); 130 | 131 | const element = ; 132 | const wrapper = mount( 133 | element, 134 | createContext({ x: { type: String } }, { onChange }), 135 | ); 136 | 137 | wrapper.setProps({ value: undefined }); 138 | 139 | expect(onChange).not.toHaveBeenCalled(); 140 | }); 141 | 142 | test(' - renders nothing which correctly reacts on model change (same value)', () => { 143 | const onChange = jest.fn(); 144 | 145 | const element = ; 146 | const wrapper = mount( 147 | element, 148 | createContext({ x: { type: String } }, { model: { x: 'y' }, onChange }), 149 | ); 150 | 151 | wrapper.setProps({ value: 'y' }); 152 | 153 | expect(onChange).not.toHaveBeenCalled(); 154 | }); 155 | -------------------------------------------------------------------------------- /__tests__/ListAddField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import merge from 'lodash/merge'; 3 | import { Button } from '@patternfly/react-core'; 4 | import { ListAddField } from '../src'; 5 | 6 | import createContext from './_createContext'; 7 | import mount from './_mount'; 8 | 9 | 10 | const onChange = jest.fn(); 11 | const context = (schema?: {}) => 12 | createContext( 13 | merge({ x: { type: Array, maxCount: 3 }, 'x.$': String }, schema), 14 | { onChange, model: { x: [] } }, 15 | ); 16 | 17 | beforeEach(() => { 18 | onChange.mockClear(); 19 | }); 20 | ; 21 | 22 | test(' - works', () => { 23 | const element = ; 24 | const wrapper = mount(element, context()); 25 | 26 | expect(wrapper.find(ListAddField)).toHaveLength(1); 27 | }); 28 | 29 | test(' - prevents onClick when disabled', () => { 30 | const element = ; 31 | const wrapper = mount(element, context()); 32 | 33 | expect(wrapper.find(Button).simulate('click')).toBeTruthy(); 34 | expect(onChange).not.toHaveBeenCalled(); 35 | }); 36 | 37 | test(' - prevents onClick when limit reached', () => { 38 | const element = ; 39 | const wrapper = mount(element, context({ x: { maxCount: 0 } })); 40 | 41 | expect(wrapper.find(Button).simulate('click')).toBeTruthy(); 42 | expect(onChange).not.toHaveBeenCalled(); 43 | }); 44 | 45 | test(' - correctly reacts on click', () => { 46 | const element = ; 47 | const wrapper = mount(element, context()); 48 | 49 | expect(wrapper.find(Button).simulate('click')).toBeTruthy(); 50 | expect(onChange).toHaveBeenLastCalledWith('x', ['y']); 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/ListDelField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { merge } from 'lodash'; 3 | import { ListDelField } from '../src'; 4 | import { Button } from '@patternfly/react-core'; 5 | 6 | import createContext from './_createContext'; 7 | import mount from './_mount'; 8 | 9 | 10 | const onChange = jest.fn(); 11 | const context = (schema?: {}) => 12 | createContext( 13 | merge({ x: { type: Array, maxCount: 3 }, 'x.$': String }, schema), 14 | { onChange, model: { x: ['x', 'y', 'z'] } }, 15 | ); 16 | 17 | beforeEach(() => { 18 | onChange.mockClear(); 19 | }); 20 | 21 | test(' - works', () => { 22 | const element = ; 23 | const wrapper = mount(element, context()); 24 | 25 | expect(wrapper.find(ListDelField)).toHaveLength(1); 26 | }); 27 | 28 | test(' - prevents onClick when disabled', () => { 29 | const element = ; 30 | const wrapper = mount(element, context()); 31 | 32 | expect(wrapper.find(Button).simulate('click')).toBeTruthy(); 33 | expect(onChange).not.toHaveBeenCalled(); 34 | }); 35 | 36 | test(' - prevents onClick when limit reached', () => { 37 | const element = ; 38 | const wrapper = mount(element, context({ x: { minCount: 3 } })); 39 | 40 | expect(wrapper.find(Button).simulate('click')).toBeTruthy(); 41 | expect(onChange).not.toHaveBeenCalled(); 42 | }); 43 | 44 | test(' - correctly reacts on click', () => { 45 | const element = ; 46 | const wrapper = mount(element, context()); 47 | 48 | expect(wrapper.find(Button).simulate('click')).toBeTruthy(); 49 | expect(onChange).toHaveBeenLastCalledWith('x', ['x', 'z']); 50 | }); 51 | -------------------------------------------------------------------------------- /__tests__/ListField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ListField } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | test(' - works', () => { 8 | const element = ; 9 | const wrapper = mount( 10 | element, 11 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 12 | ); 13 | 14 | expect(wrapper.find(ListField)).toHaveLength(1); 15 | }); 16 | 17 | test(' - renders ListAddField', () => { 18 | const element = ; 19 | const wrapper = mount( 20 | element, 21 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 22 | ); 23 | 24 | expect(wrapper.find('ListAdd')).toHaveLength(1); 25 | expect(wrapper.find('ListAdd').prop('name')).toBe('x.$'); 26 | }); 27 | 28 | test(' - renders correct label (specified)', () => { 29 | const element = ; 30 | const wrapper = mount( 31 | element, 32 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 33 | ); 34 | 35 | expect(wrapper.find('label').at(0).text()).toEqual( 36 | expect.stringContaining('ListFieldLabel') 37 | ); 38 | }); 39 | 40 | test(' - renders correct numer of items with initialCount (specified)', () => { 41 | const element = ; 42 | const wrapper = mount( 43 | element, 44 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 45 | ); 46 | 47 | expect(wrapper.find('input')).toHaveLength(3); 48 | }); 49 | 50 | test(' - passes itemProps to its children', () => { 51 | const element = ( 52 | 53 | ); 54 | const wrapper = mount( 55 | element, 56 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 57 | ); 58 | 59 | expect(wrapper.find('ListItemField').first().prop('data-xyz')).toBe(1); 60 | }); 61 | 62 | test(' - renders children (specified)', () => { 63 | const Child = jest.fn(() => null); 64 | 65 | const element = ( 66 | 67 | 68 | 69 | ); 70 | mount( 71 | element, 72 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 73 | ); 74 | 75 | expect(Child).toHaveBeenCalledTimes(2); 76 | }); 77 | 78 | test(' - renders children with correct name (children)', () => { 79 | const Child = jest.fn(() =>
); 80 | 81 | const element = ( 82 | 83 | 84 | 85 | ); 86 | const wrapper = mount( 87 | element, 88 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 89 | ); 90 | 91 | expect(wrapper.find(Child).at(0).prop('name')).toBe('0'); 92 | expect(wrapper.find(Child).at(1).prop('name')).toBe('1'); 93 | }); 94 | 95 | test(' - renders children with correct name (value)', () => { 96 | const element = ; 97 | const wrapper = mount( 98 | element, 99 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 100 | ); 101 | 102 | expect(wrapper.find('ListItemField').at(0).prop('name')).toBe('0'); 103 | expect(wrapper.find('ListItemField').at(1).prop('name')).toBe('1'); 104 | }); 105 | -------------------------------------------------------------------------------- /__tests__/ListItemField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AutoField, ListItemField } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | test(' - works', () => { 8 | const element = ; 9 | const wrapper = mount( 10 | element, 11 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 12 | ); 13 | 14 | expect(wrapper.find(ListItemField)).toHaveLength(1); 15 | }); 16 | 17 | test(' - renders AutoField', () => { 18 | const element = ; 19 | const wrapper = mount( 20 | element, 21 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 22 | ); 23 | 24 | expect(wrapper.find(AutoField)).toHaveLength(1); 25 | }); 26 | 27 | test(' - renders children if specified', () => { 28 | const Child: () => null = jest.fn(() => null); 29 | 30 | const element = ( 31 | 32 | 33 | 34 | ); 35 | mount( 36 | element, 37 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 38 | ); 39 | 40 | expect(Child).toHaveBeenCalledTimes(1); 41 | }); 42 | 43 | test(' - renders ListDelField', () => { 44 | const element = ; 45 | const wrapper = mount( 46 | element, 47 | createContext({ x: { type: Array }, 'x.$': { type: String } }) 48 | ); 49 | 50 | expect(wrapper.find('ListDel')).toHaveLength(1); 51 | expect(wrapper.find('ListDel').prop('name')).toBe('x.1'); 52 | }); 53 | -------------------------------------------------------------------------------- /__tests__/LongTextField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LongTextField } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | test(' - renders a textarea', () => { 8 | const element = ; 9 | const wrapper = mount(element, createContext({ x: { type: String } })); 10 | 11 | expect(wrapper.find('textarea')).toHaveLength(1); 12 | }); 13 | 14 | test(' - renders a textarea with correct disabled state', () => { 15 | const element = ; 16 | const wrapper = mount(element, createContext({ x: { type: String } })); 17 | 18 | expect(wrapper.find('textarea')).toHaveLength(1); 19 | expect(wrapper.find('textarea').prop('disabled')).toBe(true); 20 | }); 21 | 22 | test(' - renders a textarea with correct id (inherited)', () => { 23 | const element = ; 24 | const wrapper = mount(element, createContext({ x: { type: String } })); 25 | 26 | expect(wrapper.find('textarea')).toHaveLength(1); 27 | expect(wrapper.find('textarea').prop('id')).toBeTruthy(); 28 | }); 29 | 30 | test(' - renders a textarea with correct id (specified)', () => { 31 | const element = ; 32 | const wrapper = mount(element, createContext({ x: { type: String } })); 33 | 34 | expect(wrapper.find('textarea')).toHaveLength(1); 35 | expect(wrapper.find('textarea').prop('id')).toBe('y'); 36 | }); 37 | 38 | test(' - renders a textarea with correct name', () => { 39 | const element = ; 40 | const wrapper = mount(element, createContext({ x: { type: String } })); 41 | 42 | expect(wrapper.find('textarea')).toHaveLength(1); 43 | expect(wrapper.find('textarea').prop('name')).toBe('x'); 44 | }); 45 | 46 | test(' - renders a textarea with correct placeholder', () => { 47 | const element = ; 48 | const wrapper = mount(element, createContext({ x: { type: String } })); 49 | 50 | expect(wrapper.find('textarea')).toHaveLength(1); 51 | expect(wrapper.find('textarea').prop('placeholder')).toBe('y'); 52 | }); 53 | 54 | test(' - renders a textarea with correct value (default)', () => { 55 | const element = ; 56 | const wrapper = mount(element, createContext({ x: { type: String } })); 57 | 58 | expect(wrapper.find('textarea')).toHaveLength(1); 59 | expect(wrapper.find('textarea').prop('value')).toBe(''); 60 | }); 61 | 62 | test(' - renders a textarea with correct value (model)', () => { 63 | const element = ; 64 | const wrapper = mount( 65 | element, 66 | createContext({ x: { type: String } }, { model: { x: 'y' } }), 67 | ); 68 | 69 | expect(wrapper.find('textarea')).toHaveLength(1); 70 | expect(wrapper.find('textarea').prop('value')).toBe('y'); 71 | }); 72 | 73 | test(' - renders a textarea with correct value (specified)', () => { 74 | const element = ; 75 | const wrapper = mount(element, createContext({ x: { type: String } })); 76 | 77 | expect(wrapper.find('textarea')).toHaveLength(1); 78 | expect(wrapper.find('textarea').prop('value')).toBe('y'); 79 | }); 80 | 81 | test(' - renders a textarea which correctly reacts on change', () => { 82 | const onChange = jest.fn(); 83 | 84 | const element = ; 85 | const wrapper = mount( 86 | element, 87 | createContext({ x: { type: String } }, { onChange }), 88 | ); 89 | 90 | expect(wrapper.find('textarea')).toHaveLength(1); 91 | expect( 92 | wrapper.find('textarea').simulate('change', { target: { value: 'y' } }), 93 | ).toBeTruthy(); 94 | expect(onChange).toHaveBeenLastCalledWith('x', 'y'); 95 | }); 96 | 97 | test(' - renders a textarea which correctly reacts on change (empty)', () => { 98 | const onChange = jest.fn(); 99 | 100 | const element = ; 101 | const wrapper = mount( 102 | element, 103 | createContext({ x: { type: String } }, { onChange }), 104 | ); 105 | 106 | expect(wrapper.find('textarea')).toHaveLength(1); 107 | expect( 108 | wrapper.find('textarea').simulate('change', { target: { value: '' } }), 109 | ).toBeTruthy(); 110 | expect(onChange).toHaveBeenLastCalledWith('x', ''); 111 | }); 112 | 113 | test(' - renders a textarea which correctly reacts on change (same value)', () => { 114 | const onChange = jest.fn(); 115 | 116 | const element = ; 117 | const wrapper = mount( 118 | element, 119 | createContext({ x: { type: String } }, { model: { x: 'y' }, onChange }), 120 | ); 121 | 122 | expect(wrapper.find('textarea')).toHaveLength(1); 123 | expect( 124 | wrapper.find('textarea').simulate('change', { target: { value: 'y' } }), 125 | ).toBeTruthy(); 126 | expect(onChange).toHaveBeenLastCalledWith('x', 'y'); 127 | }); 128 | 129 | test(' - renders a label', () => { 130 | const element = ; 131 | const wrapper = mount(element, createContext({ x: { type: String } })); 132 | 133 | expect(wrapper.find('label')).toHaveLength(1); 134 | expect(wrapper.find('label').text()).toBe('y'); 135 | expect(wrapper.find('label').prop('htmlFor')).toBe( 136 | wrapper.find('textarea').prop('id'), 137 | ); 138 | }); 139 | 140 | test(' - renders a wrapper with unknown props', () => { 141 | const element = ; 142 | const wrapper = mount(element, createContext({ x: { type: String } })); 143 | 144 | expect( 145 | wrapper 146 | .find('div') 147 | .at(0) 148 | .prop('data-x'), 149 | ).toBe('x'); 150 | expect( 151 | wrapper 152 | .find('div') 153 | .at(0) 154 | .prop('data-y'), 155 | ).toBe('y'); 156 | expect( 157 | wrapper 158 | .find('div') 159 | .at(0) 160 | .prop('data-z'), 161 | ).toBe('z'); 162 | }); 163 | -------------------------------------------------------------------------------- /__tests__/NestField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from '@patternfly/react-core'; 3 | import { AutoField, NestField } from '../src'; 4 | 5 | import createContext from './_createContext'; 6 | import mount from './_mount'; 7 | 8 | test(' - renders an for each field', () => { 9 | const element = ; 10 | const wrapper = mount( 11 | element, 12 | createContext({ 13 | x: { type: Object }, 14 | 'x.a': { type: String }, 15 | 'x.b': { type: Number }, 16 | }), 17 | ); 18 | 19 | expect(wrapper.find(AutoField)).toHaveLength(2); 20 | // expect( 21 | // wrapper 22 | // .find(AutoField) 23 | // .at(0) 24 | // .prop('name'), 25 | // ).toBe('x.a'); 26 | // expect( 27 | // wrapper 28 | // .find(AutoField) 29 | // .at(1) 30 | // .prop('name'), 31 | // ).toBe('x.b'); 32 | }); 33 | 34 | test(' - renders custom content if given', () => { 35 | const element = ( 36 | 37 |
38 | 39 | ); 40 | const wrapper = mount( 41 | element, 42 | createContext({ 43 | x: { type: Object }, 44 | 'x.a': { type: String }, 45 | 'x.b': { type: Number }, 46 | }), 47 | ); 48 | 49 | expect(wrapper.find(AutoField)).toHaveLength(0); 50 | expect(wrapper.find('article').at(1)).toHaveLength(1); 51 | expect(wrapper.find('article').at(1).prop('data-test')).toBe('content'); 52 | }); 53 | 54 | test(' - renders a label', () => { 55 | const element = ; 56 | const wrapper = mount( 57 | element, 58 | createContext({ 59 | x: { type: Object }, 60 | 'x.a': { type: String }, 61 | 'x.b': { type: Number }, 62 | }), 63 | ); 64 | 65 | expect(wrapper.find('label')).toHaveLength(3); 66 | expect( 67 | wrapper 68 | .find('label') 69 | .at(0) 70 | .text(), 71 | ).toBe('y'); 72 | }); 73 | 74 | test(' - renders a wrapper with unknown props', () => { 75 | const element = ; 76 | const wrapper = mount( 77 | element, 78 | createContext({ 79 | x: { type: Object }, 80 | 'x.a': { type: String }, 81 | 'x.b': { type: Number }, 82 | }), 83 | ); 84 | 85 | expect( 86 | wrapper 87 | .find(Card) 88 | .at(0) 89 | .prop('data-x'), 90 | ).toBe('x'); 91 | expect( 92 | wrapper 93 | .find(Card) 94 | .at(0) 95 | .prop('data-y'), 96 | ).toBe('y'); 97 | expect( 98 | wrapper 99 | .find(Card) 100 | .at(0) 101 | .prop('data-z'), 102 | ).toBe('z'); 103 | }); 104 | -------------------------------------------------------------------------------- /__tests__/NumField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NumField } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | test(' - renders an input', () => { 8 | const element = ; 9 | const wrapper = mount(element, createContext({ x: { type: Number } })); 10 | 11 | expect(wrapper.find('input')).toHaveLength(1); 12 | }); 13 | 14 | test(' - renders an input with correct disabled state', () => { 15 | const element = ; 16 | const wrapper = mount(element, createContext({ x: { type: Number } })); 17 | 18 | expect(wrapper.find('input')).toHaveLength(1); 19 | expect(wrapper.find('input').prop('disabled')).toBe(true); 20 | }); 21 | 22 | test(' - renders an input with correct id (inherited)', () => { 23 | const element = ; 24 | const wrapper = mount(element, createContext({ x: { type: Number } })); 25 | 26 | expect(wrapper.find('input')).toHaveLength(1); 27 | expect(wrapper.find('input').prop('id')).toBeTruthy(); 28 | }); 29 | 30 | test(' - renders an input with correct id (specified)', () => { 31 | const element = ; 32 | const wrapper = mount(element, createContext({ x: { type: Number } })); 33 | 34 | expect(wrapper.find('input')).toHaveLength(1); 35 | expect(wrapper.find('input').prop('id')).toBe('y'); 36 | }); 37 | 38 | test(' - renders an input with correct max', () => { 39 | const element = ; 40 | const wrapper = mount(element, createContext({ x: { type: Number } })); 41 | 42 | expect(wrapper.find('input')).toHaveLength(1); 43 | expect(wrapper.find('input').prop('max')).toBe(10); 44 | }); 45 | 46 | test(' - renders an input with correct min', () => { 47 | const element = ; 48 | const wrapper = mount(element, createContext({ x: { type: Number } })); 49 | 50 | expect(wrapper.find('input')).toHaveLength(1); 51 | expect(wrapper.find('input').prop('min')).toBe(10); 52 | }); 53 | 54 | test(' - renders an input with correct name', () => { 55 | const element = ; 56 | const wrapper = mount(element, createContext({ x: { type: Number } })); 57 | 58 | expect(wrapper.find('input')).toHaveLength(1); 59 | expect(wrapper.find('input').prop('name')).toBe('x'); 60 | }); 61 | 62 | test(' - renders an input with correct placeholder', () => { 63 | const element = ; 64 | const wrapper = mount(element, createContext({ x: { type: Number } })); 65 | 66 | expect(wrapper.find('input')).toHaveLength(1); 67 | expect(wrapper.find('input').prop('placeholder')).toBe('y'); 68 | }); 69 | 70 | test(' - renders an input with correct step (decimal)', () => { 71 | const element = ; 72 | const wrapper = mount(element, createContext({ x: { type: Number } })); 73 | 74 | expect(wrapper.find('input')).toHaveLength(1); 75 | expect(wrapper.find('input').prop('step')).toBe(0.01); 76 | }); 77 | 78 | test(' - renders an input with correct step (integer)', () => { 79 | const element = ; 80 | const wrapper = mount(element, createContext({ x: { type: Number } })); 81 | 82 | expect(wrapper.find('input')).toHaveLength(1); 83 | expect(wrapper.find('input').prop('step')).toBe(1); 84 | }); 85 | 86 | test(' - renders an input with correct type', () => { 87 | const element = ; 88 | const wrapper = mount(element, createContext({ x: { type: Number } })); 89 | 90 | expect(wrapper.find('input')).toHaveLength(1); 91 | expect(wrapper.find('input').prop('type')).toBe('number'); 92 | }); 93 | 94 | test(' - renders an input with correct value (default)', () => { 95 | const element = ; 96 | const wrapper = mount(element, createContext({ x: { type: Number } })); 97 | 98 | expect(wrapper.find('input')).toHaveLength(1); 99 | expect(wrapper.find('input').prop('value')).toBe(''); 100 | }); 101 | 102 | test(' - renders an input with correct value (model)', () => { 103 | const onChange = jest.fn(); 104 | 105 | const element = ; 106 | const wrapper = mount( 107 | element, 108 | createContext({ x: { type: Number } }, { model: { x: 1 }, onChange }), 109 | ); 110 | 111 | expect(wrapper.find('input')).toHaveLength(1); 112 | expect(wrapper.find('input').prop('value')).toBe(1); 113 | 114 | // NOTE: All following tests are here to cover hacky NumField implementation. 115 | const spy = jest.spyOn(global.console, 'error').mockImplementation(() => {}); 116 | 117 | [ 118 | { value: 0.1 }, 119 | { value: undefined }, 120 | { value: undefined }, 121 | { value: 2 }, 122 | { value: 2 }, 123 | { value: 1, decimal: false }, 124 | { value: 1, decimal: false }, 125 | ].forEach(({ decimal = true, value }) => { 126 | wrapper.setProps({ decimal }); 127 | 128 | expect( 129 | wrapper.find('input').simulate('change', { target: { value: '' } }), 130 | ).toBeTruthy(); 131 | expect( 132 | wrapper.find('input').simulate('change', { target: { value } }), 133 | ).toBeTruthy(); 134 | expect(onChange).toHaveBeenLastCalledWith('x', value); 135 | 136 | wrapper.setProps({ value: undefined }); 137 | wrapper.setProps({ value }); 138 | expect(wrapper.find('input').prop('value')).toBe(value ?? ''); 139 | }); 140 | 141 | spy.mockRestore(); 142 | }); 143 | 144 | test(' - renders an input with correct value (specified)', () => { 145 | const element = ; 146 | const wrapper = mount(element, createContext({ x: { type: Number } })); 147 | 148 | expect(wrapper.find('input')).toHaveLength(1); 149 | expect(wrapper.find('input').prop('value')).toBe(2); 150 | }); 151 | 152 | test(' - renders an input which correctly reacts on change', () => { 153 | const onChange = jest.fn(); 154 | 155 | const element = ; 156 | const wrapper = mount( 157 | element, 158 | createContext({ x: { type: Number } }, { onChange }), 159 | ); 160 | 161 | expect(wrapper.find('input')).toHaveLength(1); 162 | expect( 163 | wrapper.find('input').simulate('change', { target: { value: '1' } }), 164 | ).toBeTruthy(); 165 | expect(onChange).toHaveBeenLastCalledWith('x', 1); 166 | }); 167 | 168 | test(' - renders an input which correctly reacts on change (decimal on decimal)', () => { 169 | const onChange = jest.fn(); 170 | 171 | const element = ; 172 | const wrapper = mount( 173 | element, 174 | createContext({ x: { type: Number } }, { onChange }), 175 | ); 176 | 177 | expect(wrapper.find('input')).toHaveLength(1); 178 | expect( 179 | wrapper.find('input').simulate('change', { target: { value: '2.5' } }), 180 | ).toBeTruthy(); 181 | expect(onChange).toHaveBeenLastCalledWith('x', 2.5); 182 | }); 183 | 184 | test(' - renders an input which correctly reacts on change (decimal on integer)', () => { 185 | const onChange = jest.fn(); 186 | 187 | const element = ; 188 | const wrapper = mount( 189 | element, 190 | createContext({ x: { type: Number } }, { onChange }), 191 | ); 192 | 193 | expect(wrapper.find('input')).toHaveLength(1); 194 | expect( 195 | wrapper.find('input').simulate('change', { target: { value: '2.5' } }), 196 | ).toBeTruthy(); 197 | expect(onChange).toHaveBeenLastCalledWith('x', 2); 198 | }); 199 | 200 | test(' - renders an input which correctly reacts on change (empty)', () => { 201 | const onChange = jest.fn(); 202 | 203 | const element = ; 204 | const wrapper = mount( 205 | element, 206 | createContext({ x: { type: Number } }, { onChange }), 207 | ); 208 | 209 | expect(wrapper.find('input')).toHaveLength(1); 210 | expect( 211 | wrapper.find('input').simulate('change', { target: { value: '' } }), 212 | ).toBeTruthy(); 213 | expect(onChange).toHaveBeenLastCalledWith('x', undefined); 214 | }); 215 | 216 | test(' - renders an input which correctly reacts on change (same value)', () => { 217 | const onChange = jest.fn(); 218 | 219 | const element = ; 220 | const wrapper = mount( 221 | element, 222 | createContext({ x: { type: Number } }, { model: { x: 1 }, onChange }), 223 | ); 224 | 225 | expect(wrapper.find('input')).toHaveLength(1); 226 | expect( 227 | wrapper.find('input').simulate('change', { target: { value: '1' } }), 228 | ).toBeTruthy(); 229 | expect(onChange).toHaveBeenLastCalledWith('x', 1); 230 | }); 231 | 232 | test(' - renders an input which correctly reacts on change (zero)', () => { 233 | const onChange = jest.fn(); 234 | 235 | const element = ; 236 | const wrapper = mount( 237 | element, 238 | createContext({ x: { type: Number } }, { onChange }), 239 | ); 240 | 241 | expect(wrapper.find('input')).toHaveLength(1); 242 | expect( 243 | wrapper.find('input').simulate('change', { target: { value: '0' } }), 244 | ).toBeTruthy(); 245 | expect(onChange).toHaveBeenLastCalledWith('x', 0); 246 | }); 247 | 248 | test(' - renders a label', () => { 249 | const element = ; 250 | const wrapper = mount(element, createContext({ x: { type: Number } })); 251 | 252 | expect(wrapper.find('label')).toHaveLength(1); 253 | expect(wrapper.find('label').text()).toBe('y'); 254 | expect(wrapper.find('label').prop('htmlFor')).toBe( 255 | wrapper.find('input').prop('id'), 256 | ); 257 | }); 258 | 259 | test(' - renders a label', () => { 260 | const element = ; 261 | const wrapper = mount(element, createContext({ x: { type: Number } })); 262 | 263 | expect(wrapper.find('label')).toHaveLength(1); 264 | expect(wrapper.find('label').text()).toBe('y *'); 265 | expect(wrapper.find('label').prop('htmlFor')).toBe( 266 | wrapper.find('input').prop('id'), 267 | ); 268 | }); 269 | 270 | test(' - renders a wrapper with unknown props', () => { 271 | const element = ; 272 | const wrapper = mount(element, createContext({ x: { type: Number } })); 273 | 274 | expect( 275 | wrapper 276 | .find('div') 277 | .at(0) 278 | .prop('data-x'), 279 | ).toBe('x'); 280 | expect( 281 | wrapper 282 | .find('div') 283 | .at(0) 284 | .prop('data-y'), 285 | ).toBe('y'); 286 | expect( 287 | wrapper 288 | .find('div') 289 | .at(0) 290 | .prop('data-z'), 291 | ).toBe('z'); 292 | }); 293 | -------------------------------------------------------------------------------- /__tests__/QuickForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { QuickForm } from '../src'; 3 | 4 | import createSchema from './_createSchema'; 5 | import mount from './_mount'; 6 | 7 | test(' - renders', () => { 8 | const element = ; 9 | const wrapper = mount(element); 10 | 11 | expect(wrapper).toHaveLength(1); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/RadioField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RadioField } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | 7 | test(' - renders a set of checkboxes', () => { 8 | const element = ; 9 | const wrapper = mount( 10 | element, 11 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 12 | ); 13 | 14 | expect(wrapper.find('input')).toHaveLength(2); 15 | }); 16 | 17 | test(' - renders a set of checkboxes with correct disabled state', () => { 18 | const element = ; 19 | const wrapper = mount( 20 | element, 21 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 22 | ); 23 | 24 | expect(wrapper.find('input')).toHaveLength(2); 25 | expect( 26 | wrapper 27 | .find('input') 28 | .at(0) 29 | .prop('disabled'), 30 | ).toBe(true); 31 | expect( 32 | wrapper 33 | .find('input') 34 | .at(1) 35 | .prop('disabled'), 36 | ).toBe(true); 37 | }); 38 | 39 | test(' - renders a set of checkboxes with correct id (inherited)', () => { 40 | const element = ; 41 | const wrapper = mount( 42 | element, 43 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 44 | ); 45 | 46 | expect(wrapper.find('input')).toHaveLength(2); 47 | expect( 48 | wrapper 49 | .find('input') 50 | .at(0) 51 | .prop('id'), 52 | ).toBeTruthy(); 53 | expect( 54 | wrapper 55 | .find('input') 56 | .at(1) 57 | .prop('id'), 58 | ).toBeTruthy(); 59 | }); 60 | 61 | test(' - renders a set of checkboxes with correct id (specified)', () => { 62 | const element = ; 63 | const wrapper = mount( 64 | element, 65 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 66 | ); 67 | 68 | expect(wrapper.find('input')).toHaveLength(2); 69 | expect( 70 | wrapper 71 | .find('input') 72 | .at(0) 73 | .prop('id'), 74 | ).toBe('y'); 75 | expect( 76 | wrapper 77 | .find('input') 78 | .at(1) 79 | .prop('id'), 80 | ).toBe('y'); 81 | }); 82 | 83 | test(' - renders a set of checkboxes with correct name', () => { 84 | const element = ; 85 | const wrapper = mount( 86 | element, 87 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 88 | ); 89 | 90 | expect(wrapper.find('input')).toHaveLength(2); 91 | expect( 92 | wrapper 93 | .find('input') 94 | .at(0) 95 | .prop('name'), 96 | ).toBe('x'); 97 | expect( 98 | wrapper 99 | .find('input') 100 | .at(1) 101 | .prop('name'), 102 | ).toBe('x'); 103 | }); 104 | 105 | test(' - renders a set of checkboxes with correct options', () => { 106 | const element = ; 107 | const wrapper = mount( 108 | element, 109 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 110 | ); 111 | 112 | expect(wrapper.find('label')).toHaveLength(2); 113 | expect( 114 | wrapper 115 | .find('label') 116 | .at(0) 117 | .text(), 118 | ).toBe('a'); 119 | expect( 120 | wrapper 121 | .find('label') 122 | .at(1) 123 | .text(), 124 | ).toBe('b'); 125 | }); 126 | 127 | test(' - renders a set of checkboxes with correct options (transform)', () => { 128 | const element = x.toUpperCase()} />; 129 | const wrapper = mount( 130 | element, 131 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 132 | ); 133 | 134 | expect(wrapper.find('label')).toHaveLength(2); 135 | expect( 136 | wrapper 137 | .find('label') 138 | .at(0) 139 | .text(), 140 | ).toBe('A'); 141 | expect( 142 | wrapper 143 | .find('label') 144 | .at(1) 145 | .text(), 146 | ).toBe('B'); 147 | }); 148 | 149 | test(' - renders a set of checkboxes with correct value (default)', () => { 150 | const element = ; 151 | const wrapper = mount( 152 | element, 153 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 154 | ); 155 | 156 | expect(wrapper.find('input')).toHaveLength(2); 157 | expect( 158 | wrapper 159 | .find('input') 160 | .at(0) 161 | .prop('checked'), 162 | ).toBe(false); 163 | expect( 164 | wrapper 165 | .find('input') 166 | .at(1) 167 | .prop('checked'), 168 | ).toBe(false); 169 | }); 170 | 171 | test(' - renders a set of checkboxes with correct value (model)', () => { 172 | const element = ; 173 | const wrapper = mount( 174 | element, 175 | createContext( 176 | { x: { type: String, allowedValues: ['a', 'b'] } }, 177 | { model: { x: 'b' } }, 178 | ), 179 | ); 180 | 181 | expect(wrapper.find('input')).toHaveLength(2); 182 | expect( 183 | wrapper 184 | .find('input') 185 | .at(0) 186 | .prop('checked'), 187 | ).toBe(false); 188 | expect( 189 | wrapper 190 | .find('input') 191 | .at(1) 192 | .prop('checked'), 193 | ).toBe(true); 194 | }); 195 | 196 | test(' - renders a set of checkboxes with correct value (specified)', () => { 197 | const element = ; 198 | const wrapper = mount( 199 | element, 200 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 201 | ); 202 | 203 | expect(wrapper.find('input')).toHaveLength(2); 204 | expect( 205 | wrapper 206 | .find('input') 207 | .at(0) 208 | .prop('checked'), 209 | ).toBe(false); 210 | expect( 211 | wrapper 212 | .find('input') 213 | .at(1) 214 | .prop('checked'), 215 | ).toBe(true); 216 | }); 217 | 218 | test(' - renders a set of checkboxes which correctly reacts on change', () => { 219 | const onChange = jest.fn(); 220 | 221 | const element = ; 222 | const wrapper = mount( 223 | element, 224 | createContext( 225 | { x: { type: String, allowedValues: ['a', 'b'] } }, 226 | { onChange }, 227 | ), 228 | ); 229 | 230 | expect(wrapper.find('input')).toHaveLength(2); 231 | expect( 232 | wrapper 233 | .find('input') 234 | .at(1) 235 | .simulate('change'), 236 | ).toBeTruthy(); 237 | expect(onChange).toHaveBeenLastCalledWith('x', 'b'); 238 | }); 239 | 240 | test(' - renders a set of checkboxes which correctly reacts on change (same value)', () => { 241 | const onChange = jest.fn(); 242 | 243 | const element = ; 244 | const wrapper = mount( 245 | element, 246 | createContext( 247 | { x: { type: String, allowedValues: ['a', 'b'] } }, 248 | { model: { x: 'b' }, onChange }, 249 | ), 250 | ); 251 | 252 | expect(wrapper.find('input')).toHaveLength(2); 253 | expect( 254 | wrapper 255 | .find('input') 256 | .at(0) 257 | .simulate('change'), 258 | ).toBeTruthy(); 259 | expect(onChange).toHaveBeenLastCalledWith('x', 'a'); 260 | }); 261 | 262 | test(' - renders a label', () => { 263 | const element = ; 264 | const wrapper = mount( 265 | element, 266 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 267 | ); 268 | 269 | expect(wrapper.find('label')).toHaveLength(3); 270 | expect( 271 | wrapper 272 | .find('label') 273 | .at(0) 274 | .text(), 275 | ).toBe('y'); 276 | }); 277 | 278 | test(' - renders a label', () => { 279 | const element = ; 280 | const wrapper = mount( 281 | element, 282 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 283 | ); 284 | 285 | expect(wrapper.find('label')).toHaveLength(3); 286 | expect( 287 | wrapper 288 | .find('label') 289 | .at(0) 290 | .text(), 291 | ).toBe('y *'); 292 | }); 293 | 294 | test(' - renders a wrapper with unknown props', () => { 295 | const element = ; 296 | const wrapper = mount( 297 | element, 298 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 299 | ); 300 | 301 | expect( 302 | wrapper 303 | .find('div') 304 | .at(0) 305 | .prop('data-x'), 306 | ).toBe('x'); 307 | expect( 308 | wrapper 309 | .find('div') 310 | .at(0) 311 | .prop('data-y'), 312 | ).toBe('y'); 313 | expect( 314 | wrapper 315 | .find('div') 316 | .at(0) 317 | .prop('data-z'), 318 | ).toBe('z'); 319 | }); 320 | -------------------------------------------------------------------------------- /__tests__/SelectField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select, Radio } from '@patternfly/react-core'; 3 | import { act } from '@testing-library/react'; 4 | import { SelectField } from '../src'; 5 | 6 | import createContext from './_createContext'; 7 | import mount from './_mount'; 8 | 9 | test(' - renders a select', () => { 10 | const element = ; 11 | const wrapper = mount( 12 | element, 13 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 14 | ); 15 | 16 | expect(wrapper.find(Select)).toHaveLength(1); 17 | }); 18 | 19 | test(' - renders a select with correct disabled state', () => { 20 | const element = ; 21 | const wrapper = mount( 22 | element, 23 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 24 | ); 25 | 26 | expect(wrapper.find(Select)).toHaveLength(1); 27 | expect(wrapper.find(Select).prop('isDisabled')).toBe(true); 28 | }); 29 | 30 | test(' - renders a select with correct id (inherited)', () => { 31 | const element = ; 32 | const wrapper = mount( 33 | element, 34 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 35 | ); 36 | 37 | expect(wrapper.find(Select)).toHaveLength(1); 38 | expect(wrapper.find(Select).prop('id')).toBeTruthy(); 39 | }); 40 | 41 | test(' - renders a select with correct id (specified)', () => { 42 | const element = ; 43 | const wrapper = mount( 44 | element, 45 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 46 | ); 47 | 48 | expect(wrapper.find(Select)).toHaveLength(1); 49 | expect(wrapper.find(Select).prop('id')).toBe('y'); 50 | }); 51 | 52 | test(' - renders a select with correct name', () => { 53 | const element = ; 54 | const wrapper = mount( 55 | element, 56 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 57 | ); 58 | 59 | expect(wrapper.find(Select)).toHaveLength(1); 60 | expect(wrapper.find(Select).prop('name')).toBe('x'); 61 | }); 62 | 63 | test(' - renders a select with correct options', () => { 64 | const element = ; 65 | const wrapper = mount( 66 | element, 67 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 68 | ); 69 | 70 | expect(wrapper.find(Select)).toHaveLength(1); 71 | expect(wrapper.find(Select).prop('children')).toHaveLength(2); 72 | expect(wrapper.find(Select).prop('children')[0].props.value).toBe('a'); 73 | expect(wrapper.find(Select).prop('children')[0].props.children).toBe('a'); 74 | expect(wrapper.find(Select).prop('children')[1].props.value).toBe('b'); 75 | expect(wrapper.find(Select).prop('children')[1].props.children).toBe('b'); 76 | }); 77 | 78 | test(' - renders a select with correct options (transform)', () => { 79 | const element = x.toUpperCase()} />; 80 | const wrapper = mount( 81 | element, 82 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 83 | ); 84 | 85 | expect(wrapper.find(Select)).toHaveLength(1); 86 | expect(wrapper.find(Select).prop('children')).toHaveLength(2); 87 | expect(wrapper.find(Select).prop('children')[0].props.value).toBe('a'); 88 | expect(wrapper.find(Select).prop('children')[0].props.children).toBe('A'); 89 | expect(wrapper.find(Select).prop('children')[1].props.value).toBe('b'); 90 | expect(wrapper.find(Select).prop('children')[1].props.children).toBe('B'); 91 | }); 92 | 93 | test(' - renders a select with correct placeholder (implicit)', () => { 94 | const element = ; 95 | const wrapper = mount( 96 | element, 97 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 98 | ); 99 | 100 | expect(wrapper.find(Select)).toHaveLength(1); 101 | expect(wrapper.find(Select).prop('placeholderText')).toBe('y'); 102 | expect(wrapper.find(Select).prop('value')).toBe(undefined); 103 | }); 104 | 105 | test(' - renders a select with correct value (default)', () => { 106 | const element = ; 107 | const wrapper = mount( 108 | element, 109 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 110 | ); 111 | 112 | expect(wrapper.find(Select)).toHaveLength(1); 113 | expect(wrapper.find(Select).prop('value')).toBe(undefined); 114 | }); 115 | 116 | test(' - renders a select with correct value (model)', () => { 117 | const element = ; 118 | const wrapper = mount( 119 | element, 120 | createContext( 121 | { x: { type: String, allowedValues: ['a', 'b'] } }, 122 | { model: { x: 'b' } }, 123 | ), 124 | ); 125 | 126 | expect(wrapper.find(Select)).toHaveLength(1); 127 | expect(wrapper.find(Select).prop('value')).toBe('b'); 128 | }); 129 | 130 | test(' - renders a select with correct value (specified)', () => { 131 | const element = ; 132 | const wrapper = mount( 133 | element, 134 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 135 | ); 136 | 137 | expect(wrapper.find(Select)).toHaveLength(1); 138 | expect(wrapper.find(Select).prop('value')).toBe('b'); 139 | }); 140 | 141 | test(' - renders a select which correctly reacts on change', () => { 142 | const onChange = jest.fn(); 143 | const element = ; 144 | const wrapper = mount( 145 | element, 146 | createContext( 147 | { x: { type: String, allowedValues: ['a', 'b'] } }, 148 | { onChange }, 149 | ), 150 | ); 151 | 152 | act(() => { 153 | const changeEvent = wrapper.find(Select).prop('onSelect')('event', 'b'); 154 | expect(changeEvent).toBeFalsy(); 155 | }) 156 | 157 | expect(wrapper.find(Select)).toHaveLength(1); 158 | expect(onChange).toHaveBeenLastCalledWith('x', 'b'); 159 | }); 160 | 161 | test(' - renders a select which correctly reacts on change (array)', () => { 162 | const onChange = jest.fn(); 163 | 164 | const element = ; 165 | const wrapper = mount( 166 | element, 167 | createContext( 168 | { 169 | x: { type: Array }, 170 | 'x.$': { type: String, allowedValues: ['a', 'b'] }, 171 | }, 172 | { onChange }, 173 | ), 174 | ); 175 | 176 | act(() => { 177 | const changeEvent = wrapper.find(Select).prop('onSelect')('event', 'b'); 178 | expect(changeEvent).toBeFalsy(); 179 | }); 180 | 181 | expect(wrapper.find(Select)).toHaveLength(1); 182 | expect(onChange).toHaveBeenLastCalledWith('x', ['b']); 183 | }); 184 | 185 | test(' - renders a select which correctly reacts on change (placeholder)', () => { 186 | const onChange = jest.fn(); 187 | 188 | const element = ; 189 | const wrapper = mount( 190 | element, 191 | createContext( 192 | { x: { type: String, allowedValues: ['a', 'b'] } }, 193 | { onChange }, 194 | ), 195 | ); 196 | 197 | act(() => { 198 | const changeEvent = wrapper.find(Select).prop('onSelect')('event', 'test') 199 | expect(changeEvent).toBeUndefined(); 200 | }); 201 | 202 | expect(wrapper.find(Select)).toHaveLength(1); 203 | expect(onChange).toHaveBeenCalled() 204 | }); 205 | 206 | test(' - renders a select which correctly reacts on change (same value)', () => { 207 | const onChange = jest.fn(); 208 | 209 | const element = ; 210 | const wrapper = mount( 211 | element, 212 | createContext( 213 | { x: { type: String, allowedValues: ['a', 'b'] } }, 214 | { model: { x: 'b' }, onChange }, 215 | ), 216 | ); 217 | 218 | act(() => { 219 | const changeEvent = wrapper.find(Select).prop('onSelect')('event', 'b'); 220 | expect(changeEvent).toBeFalsy(); 221 | }); 222 | 223 | expect(wrapper.find(Select)).toHaveLength(1); 224 | expect(onChange).toHaveBeenLastCalledWith('x', 'b'); 225 | }); 226 | 227 | test(' - renders a label', () => { 228 | const element = ; 229 | const wrapper = mount( 230 | element, 231 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 232 | ); 233 | 234 | expect(wrapper.find('label')).toHaveLength(1); 235 | expect(wrapper.find('label').text()).toBe('y'); 236 | expect(wrapper.find('label').prop('htmlFor')).toBe( 237 | wrapper.find(Select).prop('id'), 238 | ); 239 | }); 240 | 241 | test(' - renders a label', () => { 242 | const element = ; 243 | const wrapper = mount( 244 | element, 245 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 246 | ); 247 | 248 | expect(wrapper.find('label')).toHaveLength(1); 249 | expect(wrapper.find('label').text()).toBe('y *'); 250 | expect(wrapper.find('label').prop('htmlFor')).toBe( 251 | wrapper.find(Select).prop('id'), 252 | ); 253 | }); 254 | 255 | test(' - renders a number label', () => { 256 | const element = ; 257 | const wrapper = mount( 258 | element, 259 | createContext({ x: { type: Number, allowedValues: [1, 2] } }), 260 | ); 261 | 262 | expect(wrapper.find('label')).toHaveLength(1); 263 | expect(wrapper.find('label').text()).toBe('1 *'); 264 | expect(wrapper.find('label').prop('htmlFor')).toBe( 265 | wrapper.find(Select).prop('id'), 266 | ); 267 | }); 268 | 269 | test(' - renders a wrapper with unknown props', () => { 270 | const element = ; 271 | const wrapper = mount( 272 | element, 273 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 274 | ); 275 | 276 | expect( 277 | wrapper 278 | .find('div') 279 | .at(0) 280 | .prop('data-x'), 281 | ).toBe('x'); 282 | expect( 283 | wrapper 284 | .find('div') 285 | .at(0) 286 | .prop('data-y'), 287 | ).toBe('y'); 288 | expect( 289 | wrapper 290 | .find('div') 291 | .at(0) 292 | .prop('data-z'), 293 | ).toBe('z'); 294 | }); 295 | 296 | test(' - renders a set of checkboxes', () => { 297 | const element = ; 298 | const wrapper = mount( 299 | element, 300 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 301 | ); 302 | 303 | expect(wrapper.find('input')).toHaveLength(2); 304 | }); 305 | 306 | test(' - renders a set of checkboxes with correct disabled state', () => { 307 | const element = ; 308 | const wrapper = mount( 309 | element, 310 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 311 | ); 312 | 313 | expect(wrapper.find('input')).toHaveLength(2); 314 | expect( 315 | wrapper 316 | .find('input') 317 | .at(0) 318 | .prop('disabled'), 319 | ).toBe(true); 320 | expect( 321 | wrapper 322 | .find('input') 323 | .at(1) 324 | .prop('disabled'), 325 | ).toBe(true); 326 | }); 327 | 328 | test(' - renders a set of checkboxes with correct id (inherited)', () => { 329 | const element = ; 330 | const wrapper = mount( 331 | element, 332 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 333 | ); 334 | 335 | expect(wrapper.find('input')).toHaveLength(2); 336 | expect( 337 | wrapper 338 | .find('input') 339 | .at(0) 340 | .prop('id'), 341 | ).toBeTruthy(); 342 | expect( 343 | wrapper 344 | .find('input') 345 | .at(1) 346 | .prop('id'), 347 | ).toBeTruthy(); 348 | }); 349 | 350 | test(' - renders a set of checkboxes with correct id (specified)', () => { 351 | const element = ; 352 | const wrapper = mount( 353 | element, 354 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 355 | ); 356 | 357 | expect(wrapper.find('input')).toHaveLength(2); 358 | expect( 359 | wrapper 360 | .find(Radio) 361 | .at(0) 362 | .prop('id'), 363 | ).toBe('y-a'); 364 | expect( 365 | wrapper 366 | .find(Radio) 367 | .at(1) 368 | .prop('id'), 369 | ).toBe('y-b'); 370 | }); 371 | 372 | test(' - renders a set of checkboxes with correct name', () => { 373 | const element = ; 374 | const wrapper = mount( 375 | element, 376 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 377 | ); 378 | 379 | expect(wrapper.find('input')).toHaveLength(2); 380 | expect( 381 | wrapper 382 | .find('input') 383 | .at(0) 384 | .prop('name'), 385 | ).toBe('x'); 386 | expect( 387 | wrapper 388 | .find('input') 389 | .at(1) 390 | .prop('name'), 391 | ).toBe('x'); 392 | }); 393 | 394 | test(' - renders a set of checkboxes with correct options', () => { 395 | const element = ; 396 | const wrapper = mount( 397 | element, 398 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 399 | ); 400 | 401 | expect(wrapper.find('label')).toHaveLength(2); 402 | expect( 403 | wrapper 404 | .find('label') 405 | .at(0) 406 | .text(), 407 | ).toBe('a'); 408 | expect( 409 | wrapper 410 | .find('label') 411 | .at(1) 412 | .text(), 413 | ).toBe('b'); 414 | }); 415 | 416 | test(' - renders a set of checkboxes with correct options (transform)', () => { 417 | const element = ( 418 | x.toUpperCase()} /> 419 | ); 420 | const wrapper = mount( 421 | element, 422 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 423 | ); 424 | 425 | expect(wrapper.find('label')).toHaveLength(2); 426 | expect( 427 | wrapper 428 | .find('label') 429 | .at(0) 430 | .text(), 431 | ).toBe('A'); 432 | expect( 433 | wrapper 434 | .find('label') 435 | .at(1) 436 | .text(), 437 | ).toBe('B'); 438 | }); 439 | 440 | test(' - renders a set of checkboxes with correct value (default)', () => { 441 | const element = ; 442 | const wrapper = mount( 443 | element, 444 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 445 | ); 446 | 447 | expect(wrapper.find('input')).toHaveLength(2); 448 | expect( 449 | wrapper 450 | .find('input') 451 | .at(0) 452 | .prop('checked'), 453 | ).toBe(false); 454 | expect( 455 | wrapper 456 | .find('input') 457 | .at(1) 458 | .prop('checked'), 459 | ).toBe(false); 460 | }); 461 | 462 | test(' - renders a set of checkboxes with correct value (model)', () => { 463 | const element = ; 464 | const wrapper = mount( 465 | element, 466 | createContext( 467 | { x: { type: String, allowedValues: ['a', 'b'] } }, 468 | { model: { x: 'b' } }, 469 | ), 470 | ); 471 | 472 | expect(wrapper.find('input')).toHaveLength(2); 473 | expect( 474 | wrapper 475 | .find('input') 476 | .at(0) 477 | .prop('checked'), 478 | ).toBe(false); 479 | expect( 480 | wrapper 481 | .find('input') 482 | .at(1) 483 | .prop('checked'), 484 | ).toBe(true); 485 | }); 486 | 487 | test(' - renders a set of checkboxes with correct value (specified)', () => { 488 | const element = ; 489 | const wrapper = mount( 490 | element, 491 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 492 | ); 493 | 494 | expect(wrapper.find('input')).toHaveLength(2); 495 | expect( 496 | wrapper 497 | .find('input') 498 | .at(0) 499 | .prop('checked'), 500 | ).toBe(false); 501 | expect( 502 | wrapper 503 | .find('input') 504 | .at(1) 505 | .prop('checked'), 506 | ).toBe(true); 507 | }); 508 | 509 | test(' - renders a set of checkboxes which correctly reacts on change', () => { 510 | const onChange = jest.fn(); 511 | 512 | const element = ; 513 | const wrapper = mount( 514 | element, 515 | createContext( 516 | { x: { type: String, allowedValues: ['a', 'b'] } }, 517 | { onChange }, 518 | ), 519 | ); 520 | 521 | expect(wrapper.find('input')).toHaveLength(2); 522 | expect( 523 | wrapper 524 | .find('input') 525 | .at(1) 526 | .simulate('change'), 527 | ).toBeTruthy(); 528 | expect(onChange).toHaveBeenLastCalledWith('x', 'b'); 529 | }); 530 | 531 | test(' - renders a set of checkboxes which correctly reacts on change (array check)', () => { 532 | const onChange = jest.fn(); 533 | 534 | const element = ; 535 | const wrapper = mount( 536 | element, 537 | createContext( 538 | { 539 | x: { type: Array }, 540 | 'x.$': { type: String, allowedValues: ['a', 'b'] }, 541 | }, 542 | { onChange }, 543 | ), 544 | ); 545 | 546 | expect(wrapper.find('input')).toHaveLength(2); 547 | expect( 548 | wrapper 549 | .find('input') 550 | .at(1) 551 | .simulate('change'), 552 | ).toBeTruthy(); 553 | expect(onChange).toHaveBeenLastCalledWith('x', ['b']); 554 | }); 555 | 556 | test(' - renders a set of checkboxes which correctly reacts on change (array uncheck)', () => { 557 | const onChange = jest.fn(); 558 | 559 | const element = ; 560 | const wrapper = mount( 561 | element, 562 | createContext( 563 | { 564 | x: { type: Array }, 565 | 'x.$': { type: String, allowedValues: ['a', 'b'] }, 566 | }, 567 | { onChange }, 568 | ), 569 | ); 570 | 571 | expect(wrapper.find('input')).toHaveLength(2); 572 | expect( 573 | wrapper 574 | .find('input') 575 | .at(1) 576 | .simulate('change'), 577 | ).toBeTruthy(); 578 | expect(onChange).toHaveBeenLastCalledWith('x', []); 579 | }); 580 | 581 | test(' - renders a set of checkboxes which correctly reacts on change (same value)', () => { 582 | const onChange = jest.fn(); 583 | 584 | const element = ; 585 | const wrapper = mount( 586 | element, 587 | createContext( 588 | { x: { type: String, allowedValues: ['a', 'b'] } }, 589 | { model: { x: 'b' }, onChange }, 590 | ), 591 | ); 592 | 593 | expect(wrapper.find('input')).toHaveLength(2); 594 | expect( 595 | wrapper 596 | .find('input') 597 | .at(0) 598 | .simulate('change'), 599 | ).toBeTruthy(); 600 | expect(onChange).toHaveBeenLastCalledWith('x', 'a'); 601 | }); 602 | 603 | test(' - renders a label', () => { 604 | const element = ; 605 | const wrapper = mount( 606 | element, 607 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 608 | ); 609 | 610 | expect(wrapper.find('label')).toHaveLength(3); 611 | expect( 612 | wrapper 613 | .find('label') 614 | .at(0) 615 | .text(), 616 | ).toBe('y'); 617 | }); 618 | 619 | test(' - renders a wrapper with unknown props', () => { 620 | const element = ( 621 | 622 | ); 623 | const wrapper = mount( 624 | element, 625 | createContext({ x: { type: String, allowedValues: ['a', 'b'] } }), 626 | ); 627 | 628 | expect( 629 | wrapper 630 | .find('div') 631 | .at(0) 632 | .prop('data-x'), 633 | ).toBe('x'); 634 | expect( 635 | wrapper 636 | .find('div') 637 | .at(0) 638 | .prop('data-y'), 639 | ).toBe('y'); 640 | expect( 641 | wrapper 642 | .find('div') 643 | .at(0) 644 | .prop('data-z'), 645 | ).toBe('z'); 646 | }); 647 | -------------------------------------------------------------------------------- /__tests__/SubmitField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@patternfly/react-core'; 3 | 4 | import { SubmitField } from '../src'; 5 | import createContext from './_createContext'; 6 | import mount from './_mount'; 7 | 8 | test(' - renders', () => { 9 | const element = ; 10 | const wrapper = mount(element, createContext()); 11 | 12 | expect(wrapper).toHaveLength(1); 13 | }); 14 | 15 | test(' - renders disabled if error', () => { 16 | const element = ; 17 | const wrapper = mount(element, createContext(undefined, { error: {} })); 18 | 19 | expect(wrapper).toHaveLength(1); 20 | expect(wrapper.find(Button).prop('isDisabled')).toBe(true); 21 | }); 22 | 23 | test(' - renders enabled if error and enabled', () => { 24 | const element = ; 25 | const wrapper = mount(element, createContext(undefined, { error: {} })); 26 | 27 | expect(wrapper).toHaveLength(1); 28 | expect(wrapper.find(Button).prop('isDisabled')).toBe(false); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/TextField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextField } from '../src'; 3 | 4 | import createContext from './_createContext'; 5 | import mount from './_mount'; 6 | import { act } from 'react-dom/test-utils'; 7 | 8 | test(' - renders an input', () => { 9 | const element = ; 10 | const wrapper = mount(element, createContext({ x: { type: String } })); 11 | 12 | expect(wrapper.find('input')).toHaveLength(1); 13 | }); 14 | 15 | test(' - renders an input with correct disabled state', () => { 16 | const element = ; 17 | const wrapper = mount(element, createContext({ x: { type: String } })); 18 | 19 | expect(wrapper.find('input')).toHaveLength(1); 20 | expect(wrapper.find('input').prop('disabled')).toBe(true); 21 | }); 22 | 23 | test(' - renders an input with correct id (inherited)', () => { 24 | const element = ; 25 | const wrapper = mount(element, createContext({ x: { type: String } })); 26 | 27 | expect(wrapper.find('input')).toHaveLength(1); 28 | expect(wrapper.find('input').prop('id')).toBeTruthy(); 29 | }); 30 | 31 | test(' - renders an input with correct id (specified)', () => { 32 | const element = ; 33 | const wrapper = mount(element, createContext({ x: { type: String } })); 34 | 35 | expect(wrapper.find('input')).toHaveLength(1); 36 | expect(wrapper.find('input').prop('id')).toBe('y'); 37 | }); 38 | 39 | test(' - renders an input with correct name', () => { 40 | const element = ; 41 | const wrapper = mount(element, createContext({ x: { type: String } })); 42 | 43 | expect(wrapper.find('input')).toHaveLength(1); 44 | expect(wrapper.find('input').prop('name')).toBe('x'); 45 | }); 46 | 47 | test(' - renders an input with correct placeholder', () => { 48 | const element = ; 49 | const wrapper = mount(element, createContext({ x: { type: String } })); 50 | 51 | expect(wrapper.find('input')).toHaveLength(1); 52 | expect(wrapper.find('input').prop('placeholder')).toBe('y'); 53 | }); 54 | 55 | test(' - renders an input with correct type', () => { 56 | const element = ; 57 | const wrapper = mount(element, createContext({ x: { type: String } })); 58 | 59 | expect(wrapper.find('input')).toHaveLength(1); 60 | expect(wrapper.find('input').prop('type')).toBe('text'); 61 | }); 62 | 63 | test(' - renders an input with correct value (default)', () => { 64 | const element = ; 65 | const wrapper = mount(element, createContext({ x: { type: String } })); 66 | 67 | expect(wrapper.find('input')).toHaveLength(1); 68 | expect(wrapper.find('input').prop('value')).toBe(''); 69 | }); 70 | 71 | test(' - renders an input with correct value (model)', () => { 72 | const element = ; 73 | const wrapper = mount( 74 | element, 75 | createContext({ x: { type: String } }, { model: { x: 'y' } }) 76 | ); 77 | 78 | expect(wrapper.find('input')).toHaveLength(1); 79 | expect(wrapper.find('input').prop('value')).toBe('y'); 80 | }); 81 | 82 | test(' - renders an input with correct value (specified)', () => { 83 | const element = ; 84 | const wrapper = mount(element, createContext({ x: { type: String } })); 85 | 86 | expect(wrapper.find('input')).toHaveLength(1); 87 | expect(wrapper.find('input').prop('value')).toBe('y'); 88 | }); 89 | 90 | test(' - renders an input which correctly reacts on change', () => { 91 | const onChange = jest.fn(); 92 | 93 | const element = ; 94 | const wrapper = mount( 95 | element, 96 | createContext({ x: { type: String } }, { onChange }) 97 | ); 98 | 99 | expect(wrapper.find('input')).toHaveLength(1); 100 | expect( 101 | wrapper.find('input').simulate('change', { target: { value: 'y' } }) 102 | ).toBeTruthy(); 103 | expect(onChange).toHaveBeenLastCalledWith('x', 'y'); 104 | }); 105 | 106 | test(' - renders an input which correctly reacts on change (empty)', () => { 107 | const onChange = jest.fn(); 108 | 109 | const element = ; 110 | const wrapper = mount( 111 | element, 112 | createContext({ x: { type: String } }, { onChange }) 113 | ); 114 | 115 | expect(wrapper.find('input')).toHaveLength(1); 116 | expect( 117 | wrapper.find('input').simulate('change', { target: { value: '' } }) 118 | ).toBeTruthy(); 119 | expect(onChange).toHaveBeenLastCalledWith('x', ''); 120 | }); 121 | 122 | test(' - renders an input which correctly reacts on change (same value)', () => { 123 | const onChange = jest.fn(); 124 | 125 | const element = ; 126 | const wrapper = mount( 127 | element, 128 | createContext({ x: { type: String } }, { model: { x: 'y' }, onChange }) 129 | ); 130 | 131 | expect(wrapper.find('input')).toHaveLength(1); 132 | expect( 133 | wrapper.find('input').simulate('change', { target: { value: 'y' } }) 134 | ).toBeTruthy(); 135 | expect(onChange).toHaveBeenLastCalledWith('x', 'y'); 136 | }); 137 | 138 | test(' - renders a label', () => { 139 | const element = ; 140 | const wrapper = mount(element, createContext({ x: { type: String } })); 141 | 142 | expect(wrapper.find('label')).toHaveLength(1); 143 | expect(wrapper.find('label').text()).toBe('y'); 144 | expect(wrapper.find('label').prop('htmlFor')).toBe( 145 | wrapper.find('input').prop('id') 146 | ); 147 | }); 148 | 149 | test(' - renders a label', () => { 150 | const element = ; 151 | const wrapper = mount(element, createContext({ x: { type: String } })); 152 | 153 | expect(wrapper.find('label')).toHaveLength(1); 154 | expect(wrapper.find('label').text()).toBe('y *'); 155 | expect(wrapper.find('label').prop('htmlFor')).toBe( 156 | wrapper.find('input').prop('id') 157 | ); 158 | }); 159 | 160 | test(' - renders a help', () => { 161 | const element = ; 162 | const wrapper = mount(element, createContext({ x: { type: String } })); 163 | 164 | expect(wrapper.find('div.pf-c-form__helper-text')).toHaveLength(1); 165 | expect(wrapper.find('div.pf-c-form__helper-text').text()).toBe('y'); 166 | }); 167 | 168 | test(' - renders a wrapper with unknown props', () => { 169 | const element = ; 170 | const wrapper = mount(element, createContext({ x: { type: String } })); 171 | 172 | expect(wrapper.find('div').at(0).prop('data-x')).toBe('x'); 173 | expect(wrapper.find('div').at(0).prop('data-y')).toBe('y'); 174 | expect(wrapper.find('div').at(0).prop('data-z')).toBe('z'); 175 | }); 176 | 177 | test(' - renders a initial value on date field (DatePicker)', () => { 178 | const date = '2000-04-04'; 179 | const element = ( 180 | 181 | ); 182 | const wrapper = mount( 183 | element, 184 | createContext({ x: { type: String } }, { model: { x: date } }) 185 | ); 186 | 187 | expect(wrapper.find('input')).toHaveLength(1); 188 | expect(wrapper.find('input').prop('value')).toBe(date); 189 | }); 190 | 191 | test(' - renders a disabled date field (DatePicker)', () => { 192 | const element = ( 193 | 200 | ); 201 | const wrapper = mount(element, createContext({ x: { type: String } })); 202 | 203 | expect(wrapper.find('input')).toHaveLength(1); 204 | expect(wrapper.find('input').prop('disabled')).toBe(true); 205 | }); 206 | 207 | test(' - renders a input which correctly reacts on change (DatePicker)', () => { 208 | const onChange = jest.fn(); 209 | 210 | const date = '2000-04-04'; 211 | const element = ( 212 | 213 | ); 214 | const wrapper = mount( 215 | element, 216 | createContext({ x: { type: String } }, { onChange }) 217 | ); 218 | 219 | expect(wrapper.find('label')).toHaveLength(1); 220 | expect(wrapper.find('label').text()).toBe('y *'); 221 | 222 | act(() => { 223 | wrapper.find('DatePicker').find('input').prop('onChange')!({ 224 | currentTarget: { value: date }, 225 | } as any); 226 | }); 227 | 228 | expect(onChange).toHaveBeenLastCalledWith('x', date); 229 | }); 230 | 231 | test(' - renders a input which correctly reacts on change (DatePicker - empty)', () => { 232 | const onChange = jest.fn(); 233 | 234 | const date = ''; 235 | const element = ( 236 | 237 | ); 238 | const wrapper = mount( 239 | element, 240 | createContext({ x: { type: String } }, { onChange }) 241 | ); 242 | 243 | expect(wrapper.find('label')).toHaveLength(1); 244 | expect(wrapper.find('label').text()).toBe('y *'); 245 | 246 | act(() => { 247 | wrapper.find('DatePicker').find('input').prop('onChange')!({ 248 | currentTarget: { value: date }, 249 | } as any); 250 | }); 251 | 252 | expect(onChange).toHaveBeenLastCalledWith('x', date); 253 | }); 254 | 255 | test(' - renders a initial value on time field (TimePicker)', () => { 256 | const time = '10:00'; 257 | const element = ( 258 | 259 | ); 260 | const wrapper = mount( 261 | element, 262 | createContext({ x: { type: String } }, { model: { x: time } }) 263 | ); 264 | 265 | expect(wrapper.find('TimePicker')).toHaveLength(1); 266 | expect(wrapper.find('TimePicker').prop('value')).toBe(time); 267 | }); 268 | 269 | test(' - renders a disabled date field (TimePicker)', () => { 270 | const element = ( 271 | 278 | ); 279 | const wrapper = mount(element, createContext({ x: { type: String } })); 280 | 281 | expect(wrapper.find('TimePicker')).toHaveLength(1); 282 | expect(wrapper.find('input').prop('disabled')).toBe(true); 283 | }); 284 | 285 | test(' - renders a input which correctly reacts on change (TimePicker)', () => { 286 | const onChange = jest.fn(); 287 | 288 | const time = '10:10'; 289 | const element = ( 290 | 291 | ); 292 | const wrapper = mount( 293 | element, 294 | createContext({ x: { type: String } }, { onChange }) 295 | ); 296 | 297 | act(() => { 298 | wrapper.find('TimePicker').find('input').prop('onChange')!({ 299 | currentTarget: { value: time }, 300 | } as any); 301 | }); 302 | 303 | expect(wrapper.find('label')).toHaveLength(1); 304 | expect(wrapper.find('label').text()).toBe('y *'); 305 | expect(onChange).toHaveBeenLastCalledWith('x', '10:10:00'); 306 | }); 307 | 308 | test(' - renders a input which correctly reacts on change (TimePicker - empty)', () => { 309 | const onChange = jest.fn(); 310 | 311 | const time = ''; 312 | const element = ( 313 | 314 | ); 315 | const wrapper = mount( 316 | element, 317 | createContext({ x: { type: String } }, { onChange }) 318 | ); 319 | 320 | expect(wrapper.find('label')).toHaveLength(1); 321 | expect(wrapper.find('label').text()).toBe('y *'); 322 | 323 | act(() => { 324 | wrapper.find('TimePicker').find('input').prop('onChange')!({ 325 | currentTarget: { value: time }, 326 | } as any); 327 | }); 328 | 329 | expect(onChange).toHaveBeenLastCalledWith('x', time); 330 | }); 331 | 332 | test(' - test max property (TimePicker - valid)', () => { 333 | const time = '10:00'; 334 | const max = '12:00'; 335 | const element = ( 336 | 337 | ); 338 | const wrapper = mount(element, createContext({ x: { type: String } })); 339 | 340 | expect(wrapper.text().includes('Should be before')).toBe(false); 341 | }); 342 | 343 | test(' - test max property (TimePicker - invalid)', () => { 344 | const time = '13:00'; 345 | const max = '12:00'; 346 | const element = ( 347 | 348 | ); 349 | const wrapper = mount(element, createContext({ x: { type: String } })); 350 | 351 | expect(wrapper.text().includes('Should be before')).toBe(true); 352 | }); 353 | 354 | test(' - test min property (TimePicker - valid)', () => { 355 | const time = '13:00'; 356 | const min = '12:00'; 357 | const element = ( 358 | 359 | ); 360 | const wrapper = mount(element, createContext({ x: { type: String } })); 361 | 362 | expect(wrapper.text().includes('Should be after')).toBe(false); 363 | }); 364 | 365 | test(' - test min property (TimePicker - invalid)', () => { 366 | const time = '10:00'; 367 | const min = '12:00'; 368 | const element = ( 369 | 370 | ); 371 | const wrapper = mount(element, createContext({ x: { type: String } })); 372 | 373 | expect(wrapper.text().includes('Should be after')).toBe(true); 374 | }); 375 | 376 | test(' - test max property (DatePicker - valid)', () => { 377 | const date = '2000-01-01'; 378 | const max = '2000-01-02'; 379 | const element = ( 380 | 381 | ); 382 | const wrapper = mount(element, createContext({ x: { type: String } })); 383 | 384 | expect(wrapper.text().includes('Should be before')).toBe(false); 385 | }); 386 | 387 | test(' - test max property (DatePicker - invalid)', () => { 388 | const date = '2000-01-02'; 389 | const max = '2000-01-01'; 390 | const element = ( 391 | 392 | ); 393 | const wrapper = mount(element, createContext({ x: { type: String } })); 394 | 395 | expect(wrapper.text().includes('Should be before')).toBe(true); 396 | }); 397 | 398 | test(' - test min property (DatePicker - valid)', () => { 399 | const date = '2000-01-02'; 400 | const min = '2000-01-01'; 401 | const element = ( 402 | 403 | ); 404 | const wrapper = mount(element, createContext({ x: { type: String } })); 405 | 406 | expect(wrapper.text().includes('Should be after')).toBe(false); 407 | }); 408 | 409 | test(' - test min property (DatePicker - invalid)', () => { 410 | const date = '2000-01-01'; 411 | const min = '2000-01-02'; 412 | const element = ( 413 | 414 | ); 415 | const wrapper = mount(element, createContext({ x: { type: String } })); 416 | 417 | expect(wrapper.text().includes('Should be after')).toBe(true); 418 | }); 419 | -------------------------------------------------------------------------------- /__tests__/ValidateQuickForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ValidatedQuickForm } from '../src'; 3 | 4 | import createSchema from './_createSchema'; 5 | import mount from './_mount'; 6 | 7 | test(' - works', () => { 8 | const element = ; 9 | const wrapper = mount(element); 10 | 11 | expect(wrapper.find(ValidatedQuickForm)).toHaveLength(1); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/ValidatedForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ValidatedForm } from '../src'; 3 | 4 | import createSchema from './_createSchema'; 5 | import mount from './_mount'; 6 | 7 | test(' - works', () => { 8 | const element = ; 9 | const wrapper = mount(element); 10 | 11 | expect(wrapper.find(ValidatedForm)).toHaveLength(1); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/_createContext.ts: -------------------------------------------------------------------------------- 1 | import { Context, randomIds } from 'uniforms'; 2 | 3 | import createSchema from './_createSchema'; 4 | 5 | const randomId = randomIds(); 6 | 7 | export default function createContext( 8 | schema?: {}, 9 | context?: Partial>, 10 | ): { context: Context } { 11 | return { 12 | context: { 13 | changed: false, 14 | changedMap: {}, 15 | error: null, 16 | model: {}, 17 | name: [], 18 | onChange() {}, 19 | onSubmit() {}, 20 | randomId, 21 | submitting: false, 22 | validating: false, 23 | ...context, 24 | schema: createSchema(schema), 25 | state: { 26 | disabled: false, 27 | label: false, 28 | placeholder: false, 29 | showInlineError: false, 30 | ...context?.state, 31 | }, 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /__tests__/_createSchema.ts: -------------------------------------------------------------------------------- 1 | import SimpleSchema from 'simpl-schema'; 2 | import { SimpleSchema2Bridge } from 'uniforms-bridge-simple-schema-2'; 3 | 4 | export default function createSchema(schema = {}) { 5 | return new SimpleSchema2Bridge(new SimpleSchema(schema)); 6 | } -------------------------------------------------------------------------------- /__tests__/_mount.tsx: -------------------------------------------------------------------------------- 1 | import { mount as enzyme } from 'enzyme'; 2 | import { context } from 'uniforms'; 3 | 4 | function mount(node: any, options: any) { 5 | if (options === undefined) { 6 | return enzyme(node); 7 | } 8 | return enzyme(node, { 9 | wrappingComponent: context.Provider, 10 | wrappingComponentProps: { value: options.context }, 11 | }); 12 | } 13 | 14 | export default mount as typeof enzyme; -------------------------------------------------------------------------------- /__tests__/index.ts: -------------------------------------------------------------------------------- 1 | import * as unstyled from '../src'; 2 | 3 | it('exports everything', () => { 4 | expect(unstyled).toEqual({ 5 | AutoFields: expect.any(Function), 6 | AutoField: expect.any(Function), 7 | AutoForm: expect.any(Function), 8 | BaseForm: expect.any(Function), 9 | BoolField: expect.any(Function), 10 | DateField: expect.any(Function), 11 | ErrorField: expect.any(Function), 12 | ErrorsField: expect.any(Function), 13 | HiddenField: expect.any(Function), 14 | ListAddField: expect.any(Function), 15 | ListDelField: expect.any(Function), 16 | ListField: expect.any(Function), 17 | ListItemField: expect.any(Function), 18 | LongTextField: expect.any(Function), 19 | NestField: expect.any(Function), 20 | NumField: expect.any(Function), 21 | QuickForm: expect.any(Function), 22 | RadioField: expect.any(Function), 23 | SelectField: expect.any(Function), 24 | SubmitField: expect.any(Function), 25 | TextField: expect.any(Function), 26 | ValidatedForm: expect.any(Function), 27 | ValidatedQuickForm: expect.any(Function), 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist 3 | .cache 4 | -------------------------------------------------------------------------------- /examples/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { AutoForm } from 'uniforms-patternfly/dist/es6'; 3 | 4 | import { CodeBlock } from './CodeBlock'; 5 | import schema from './schema/json-schema'; 6 | // import schema from './schema/simple-schema-2'; 7 | 8 | function App() { 9 | const [model, setModel] = useState(undefined); 10 | return ( 11 |
12 |
13 | 14 | setModel(m)} 18 | showInlineError 19 | /> 20 |
21 |
22 | ); 23 | } 24 | 25 | const containerStyle = { 26 | display: 'flex', 27 | alignItems: 'center', 28 | justifyContent: 'center', 29 | minHeight: '100vh', 30 | width: '100vw', 31 | padding: '10em 0' 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /examples/CodeBlock.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const CodeBlock = ({ model }) => { 4 | 5 | if (!model) return <>; 6 | 7 | return ( 8 |
9 |

Result:

10 |
11 |         {(JSON.stringify(model, null, 2))}
12 |       
13 |
14 | ); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | uniforms 8 | 12 | 13 | 14 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uniforms-patternfly-example", 3 | "private": true, 4 | "root": true, 5 | "scripts": { 6 | "build": "parcel build --no-cache index.html", 7 | "start": "parcel serve --no-cache index.html" 8 | }, 9 | "dependencies": { 10 | "@patternfly/react-core": "4.135.0", 11 | "@patternfly/react-icons": "4.11.0", 12 | "ajv": "6.10.2", 13 | "react": "16.13.1", 14 | "react-dom": "16.13.1", 15 | "react-scripts": "3.1.1", 16 | "simpl-schema": "1.5.7", 17 | "uniforms": "3.5.1", 18 | "uniforms-bridge-graphql": "3.5.1", 19 | "uniforms-bridge-json-schema": "3.5.1", 20 | "uniforms-bridge-simple-schema": "3.5.1", 21 | "uniforms-bridge-simple-schema-2": "3.5.1", 22 | "uniforms-patternfly": "4.7.9" 23 | }, 24 | "devDependencies": { 25 | "parcel-bundler": "^1.12.5", 26 | "tslib": "2.2.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/schema/graphql-schema.js: -------------------------------------------------------------------------------- 1 | import { GraphQLBridge } from 'uniforms-bridge-graphql'; 2 | import { buildASTSchema, parse } from 'graphql'; 3 | 4 | const schema = ` 5 | type Address { 6 | a: Float 7 | b: String! 8 | titleA: String! 9 | d: String! 10 | title: String! 11 | } 12 | 13 | # This is required by buildASTSchema 14 | type Query { anything: ID } 15 | `; 16 | 17 | const validator = () => { 18 | /* Empty object for no errors */ 19 | }; 20 | 21 | const args = { 22 | a: { label: 'Horse' }, 23 | b: { placeholder: 'Horse', required: false }, 24 | titleA: { label: 'Horse' }, 25 | title: { label: 'Horse A', placeholder: 'Horse B' } 26 | }; 27 | 28 | export default new GraphQLBridge( 29 | buildASTSchema(parse(schema)).getType('Address'), 30 | validator, 31 | args 32 | ); 33 | -------------------------------------------------------------------------------- /examples/schema/json-schema.js: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv'; 2 | import { JSONSchemaBridge } from 'uniforms-bridge-json-schema'; 3 | 4 | const ajv = new Ajv({ allErrors: true, useDefaults: true }); 5 | 6 | function createValidator(schema) { 7 | const validator = ajv.compile(schema); 8 | return model => { 9 | validator(model); 10 | if (validator.errors && validator.errors.length) { 11 | throw { details: validator.errors }; 12 | } 13 | }; 14 | } 15 | 16 | const schema = { 17 | type: 'object', 18 | properties: { 19 | flight: { 20 | type: 'object', 21 | properties: { 22 | flightNumber: { 23 | type: 'string' 24 | }, 25 | seat: { 26 | type: 'string' 27 | }, 28 | gate: { 29 | type: 'string' 30 | }, 31 | departure: { 32 | type: 'string', 33 | format: 'date-time' 34 | }, 35 | arrival: { 36 | type: 'string', 37 | format: 'date-time' 38 | } 39 | }, 40 | disabled: true 41 | }, 42 | hotel: { 43 | type: 'object', 44 | properties: { 45 | name: { 46 | type: 'string' 47 | }, 48 | addresses: { 49 | type: 'array', 50 | items: { 51 | $ref: "#/definitions/address" 52 | } 53 | }, 54 | phone: { 55 | type: 'string' 56 | }, 57 | bookingNumber: { 58 | type: 'string' 59 | }, 60 | room: { 61 | type: 'string' 62 | }, 63 | numberOfBeds: { 64 | placeholder: "Select...", 65 | enum: [1, 2, 3], 66 | type: 'number' 67 | } 68 | }, 69 | } 70 | }, 71 | definitions: { 72 | address: { 73 | type: 'object', 74 | properties: { 75 | street: { 76 | type: 'string' 77 | }, 78 | city: { 79 | type: 'string' 80 | }, 81 | zipCode: { 82 | type: 'string' 83 | }, 84 | country: { 85 | placeholder: "Select...", 86 | enum: ["Brazil", "Ireland", "USA"], 87 | type: 'string' 88 | } 89 | } 90 | } 91 | }, 92 | phases: ['complete', 'release'] 93 | }; 94 | 95 | const schemaValidator = createValidator(schema); 96 | 97 | export default new JSONSchemaBridge(schema, schemaValidator); 98 | -------------------------------------------------------------------------------- /examples/schema/simple-schema-2.js: -------------------------------------------------------------------------------- 1 | import SimpleSchema2Bridge from 'uniforms-bridge-simple-schema-2'; 2 | import SimpleSchema from 'simpl-schema'; 3 | 4 | const schema = new SimpleSchema({ 5 | date: { 6 | type: Date, 7 | defaultValue: new Date() 8 | }, 9 | 10 | adult: { 11 | type: Boolean 12 | }, 13 | 14 | size: { 15 | type: String, 16 | defaultValue: 'm', 17 | allowedValues: ['xs', 's', 'm', 'l', 'xl'] 18 | }, 19 | 20 | rating: { 21 | type: Number, 22 | allowedValues: [1, 2, 3, 4, 5], 23 | uniforms: { 24 | checkboxes: true 25 | } 26 | }, 27 | 28 | hello: { 29 | type: Object 30 | }, 31 | 32 | 'hello.something': { 33 | type: String 34 | }, 35 | 36 | 'hello.somethingelse': { 37 | type: String 38 | }, 39 | 40 | friends: { 41 | type: Array, 42 | minCount: 1, 43 | }, 44 | 45 | 'friends.$': { 46 | type: Object, 47 | uniforms: { 48 | label: false 49 | } 50 | }, 51 | 52 | 'friends.$.name': { 53 | type: String, 54 | min: 3 55 | }, 56 | 57 | 'friends.$.age': { 58 | type: Number, 59 | min: 0, 60 | max: 150 61 | } 62 | }); 63 | 64 | export default new SimpleSchema2Bridge(schema); 65 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ['src/*.{ts,tsx}'], 3 | moduleNameMapper: { 4 | '\\.(css|less)$': '/__mocks__/styleMock.js', 5 | '^uniforms$': '/node_modules/uniforms/src', 6 | '^uniforms/es5$': '/node_modules/uniforms/src', 7 | '^uniforms-bridge-simple-schema-2$': '/node_modules/uniforms-bridge-simple-schema-2/src', 8 | '^uniforms-patternfly$': '/src', 9 | }, 10 | setupFiles: ['/setupEnzyme.js'], 11 | testMatch: ['**/__tests__/**/!(_)*.{ts,tsx}', '!**/*.d.ts', '!**/helpers/*.ts'], 12 | moduleDirectories: [ 13 | "node_modules", 14 | "/src" 15 | ], 16 | preset: "ts-jest", 17 | transformIgnorePatterns: ["node_modules/(?!uniforms)"], 18 | transform: { 19 | '^.+\\.(js|ts|tsx)$': './transform.js' 20 | } 21 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uniforms-patternfly", 3 | "version": "4.7.9", 4 | "description": "Patternfly forms for uniforms", 5 | "repository": "git@github.com:aerogear/uniforms-patternfly.git", 6 | "author": "Gianluca ", 7 | "license": "Apache-2.0", 8 | "main": "dist/es5/index.js", 9 | "module": "dist/esm/index.js", 10 | "files": [ 11 | "dist/es5/*.d.ts", 12 | "dist/es5/*.js", 13 | "dist/esm/*.d.ts", 14 | "dist/esm/*.js", 15 | "dist/es6/*.d.ts", 16 | "dist/es6/*.js", 17 | "src/*.ts", 18 | "src/*.tsx" 19 | ], 20 | "scripts": { 21 | "build": "tsc --build --incremental tsconfig.build.json", 22 | "buildProd": "tsc --build tsconfig.build.json", 23 | "watch": "tsc --watch --incremental", 24 | "clean": "tsc --build --clean tsconfig.build.json && rimraf dist coverage", 25 | "coverage": "jest --runInBand --coverage", 26 | "lint": "eslint --ext js,ts,tsx src", 27 | "reset": "rimraf dist node_modules", 28 | "release:validate": "./scripts/validateRelease.sh TAG=1.2.3", 29 | "release:publish": "./scripts/publishRelease.sh", 30 | "test": "jest --runInBand", 31 | "install:example": "rimraf ./examples/node_modules/uniforms-patternfly/dist && cp -a ./dist/ ./examples/node_modules/uniforms-patternfly/dist/" 32 | }, 33 | "dependencies": { 34 | "classnames": "^2.0.0", 35 | "invariant": "^2.0.0", 36 | "lodash": "^4.0.0", 37 | "tslib": "^2.2.0", 38 | "uniforms": "^3.5.5" 39 | }, 40 | "devDependencies": { 41 | "@babel/plugin-proposal-class-properties": "7.10.4", 42 | "@babel/polyfill": "7.10.4", 43 | "@babel/preset-env": "7.11.0", 44 | "@babel/preset-react": "7.10.4", 45 | "@babel/preset-typescript": "7.10.4", 46 | "@patternfly/react-core": "4.135.0", 47 | "@patternfly/react-icons": "4.11.0", 48 | "@testing-library/react": "10.4.9", 49 | "@types/classnames": "2.2.11", 50 | "@types/enzyme": "3.10.8", 51 | "@types/invariant": "2.2.34", 52 | "@types/jest": "25.2.3", 53 | "@types/lodash": "4.14.168", 54 | "@typescript-eslint/eslint-plugin": "2.34.0", 55 | "@typescript-eslint/parser": "2.34.0", 56 | "babel-eslint": "10.1.0", 57 | "enzyme": "3.11.0", 58 | "enzyme-adapter-react-16": "1.15.6", 59 | "eslint": "6.8.0", 60 | "eslint-config-prettier": "6.15.0", 61 | "eslint-config-vazco": "5.2.0", 62 | "eslint-import-resolver-alias": "1.1.2", 63 | "eslint-import-resolver-typescript": "2.3.0", 64 | "eslint-plugin-babel": "5.3.1", 65 | "eslint-plugin-import": "2.22.1", 66 | "eslint-plugin-prettier": "3.3.1", 67 | "eslint-plugin-react": "7.22.0", 68 | "eslint-plugin-vazco": "1.0.0", 69 | "jest": "25.5.4", 70 | "prettier": "2.2.1", 71 | "prop-types": "15.7.2", 72 | "react": "16.13.1", 73 | "react-dom": "16.13.1", 74 | "rimraf": "2.7.1", 75 | "simpl-schema": "1.10.2", 76 | "ts-jest": "25.5.1", 77 | "ts-node": "8.10.2", 78 | "typescript": "3.8.3", 79 | "uniforms-bridge-simple-schema-2": "^3.5.5" 80 | }, 81 | "peerDependencies": { 82 | "react": "^17.0.0 || ^16.8.0", 83 | "@patternfly/react-core": "^4.135.0", 84 | "@patternfly/react-icons": "^4.11.0" 85 | }, 86 | "engines": { 87 | "npm": ">=5.0.0" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "groupName": "all" 6 | } 7 | -------------------------------------------------------------------------------- /scripts/prepareRelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Preparing release" 3 | 4 | set -e 5 | 6 | rm -Rf node_modules 7 | yarn 8 | yarn clean 9 | yarn test 10 | yarn lint 11 | 12 | # don't run in CI 13 | if [ ! "$CI" = true ]; then 14 | npm publish 15 | fi 16 | 17 | echo "Repository is ready for release." 18 | 19 | -------------------------------------------------------------------------------- /scripts/publishRelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # explicit declaration that this script needs a $TAG variable passed in e.g TAG=1.2.3 ./script.sh 4 | TAG=$TAG 5 | 6 | RELEASE_SYNTAX='^[0-9]+\.[0-9]+\.[0-9]+$' 7 | PRERELEASE_SYNTAX='^[0-9]+\.[0-9]+\.[0-9]+(-.+)+$' 8 | 9 | if [ ! "$CI" = true ]; then 10 | echo "Warning: this script should not be run outside of the CI" 11 | echo "If you really need to run this script, you can use" 12 | echo "CI=true ./scripts/publishRelease.sh" 13 | exit 1 14 | fi 15 | 16 | if [[ "$(echo $TAG | grep -E $RELEASE_SYNTAX)" == "$TAG" ]]; then 17 | TAG=$TAG npm run release:validate 18 | echo "publishing a new release: $TAG" 19 | npm publish 20 | elif [[ "$(echo $TAG | grep -E $PRERELEASE_SYNTAX)" == "$TAG" ]]; then 21 | npm publish --tag next 22 | echo "publishing a new pre release: $TAG" 23 | "npm publish --tag next" 24 | else 25 | echo "Error: the tag $TAG is not valid. exiting..." 26 | exit 1 27 | fi 28 | 29 | 30 | -------------------------------------------------------------------------------- /scripts/validateRelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # explicit declaration that this script needs a $TAG variable passed in e.g TAG=1.2.3 ./script.sh 4 | TAG=$TAG 5 | TAG_SYNTAX='^[0-9]+\.[0-9]+\.[0-9]+(-.+)*$' 6 | 7 | # get version found in lerna.json. This is the source of truth 8 | PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') 9 | 10 | # validate tag has format x.y.z 11 | if [[ "$(echo $TAG | grep -E $TAG_SYNTAX)" == "" ]]; then 12 | echo "tag $TAG is invalid. Must be in the format x.y.z or x.y.z-SOME_TEXT" 13 | exit 1 14 | fi 15 | 16 | # validate that TAG == version found in lerna.json 17 | if [[ $TAG != $PACKAGE_VERSION ]]; then 18 | echo "tag $TAG is not the same as package version found in package.json $PACKAGE_VERSION" 19 | exit 1 20 | fi 21 | 22 | 23 | echo "Ready for release" 24 | -------------------------------------------------------------------------------- /setupEnzyme.js: -------------------------------------------------------------------------------- 1 | import Adapter from 'enzyme-adapter-react-16'; 2 | import Enzyme from 'enzyme'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /src/AutoField.tsx: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import { createAutoField } from 'uniforms'; 3 | 4 | import BoolField from './BoolField'; 5 | import DateField from './DateField'; 6 | import ListField from './ListField'; 7 | import NestField from './NestField'; 8 | import NumField from './NumField'; 9 | import RadioField from './RadioField'; 10 | import SelectField from './SelectField'; 11 | import TextField from './TextField'; 12 | 13 | export type AutoFieldProps = Parameters[0]; 14 | 15 | const AutoField = createAutoField(props => { 16 | if (props.allowedValues) { 17 | return props.checkboxes && props.fieldType !== Array 18 | ? RadioField 19 | : SelectField; 20 | } 21 | 22 | switch (props.fieldType) { 23 | case Array: 24 | return ListField; 25 | case Boolean: 26 | return BoolField; 27 | case Date: 28 | return DateField; 29 | case Number: 30 | return NumField; 31 | case Object: 32 | return NestField; 33 | case String: 34 | return TextField; 35 | } 36 | 37 | return invariant(false, 'Unsupported field type: %s', props.fieldType); 38 | }); 39 | 40 | export default AutoField; -------------------------------------------------------------------------------- /src/AutoFields.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType, createElement } from 'react'; 2 | import { useForm } from 'uniforms'; 3 | 4 | import AutoField from './AutoField'; 5 | 6 | export type AutoFieldsProps = { 7 | autoField?: ComponentType<{ name: string }>; 8 | element?: ComponentType | string; 9 | fields?: string[]; 10 | omitFields?: string[]; 11 | }; 12 | 13 | export default function AutoFields({ 14 | autoField = AutoField, 15 | element = 'div', 16 | fields, 17 | omitFields = [], 18 | ...props 19 | }: AutoFieldsProps) { 20 | const { schema } = useForm(); 21 | 22 | return createElement( 23 | element!, 24 | props, 25 | (fields ?? schema.getSubfields()) 26 | .filter((field) => !omitFields!.includes(field)) 27 | .map((field) => createElement(autoField!, { key: field, name: field })) 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/AutoForm.tsx: -------------------------------------------------------------------------------- 1 | import { AutoForm } from 'uniforms'; 2 | 3 | import ValidatedQuickForm from './ValidatedQuickForm'; 4 | 5 | function Auto(parent: any): any { 6 | class _ extends AutoForm.Auto(parent) { 7 | static Auto = Auto; 8 | } 9 | 10 | return _; 11 | } 12 | 13 | export default Auto(ValidatedQuickForm); 14 | -------------------------------------------------------------------------------- /src/BaseForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form } from '@patternfly/react-core'; 3 | import { BaseForm, context } from 'uniforms'; 4 | 5 | function Patternfly(parent: any): any { 6 | class _ extends parent { 7 | static Patternfly = Patternfly; 8 | 9 | static displayName = `Patternfly${parent.displayName}`; 10 | 11 | render() { 12 | return ( 13 | 14 |
15 | 16 | ); 17 | } 18 | } 19 | 20 | return _; 21 | } 22 | 23 | export default Patternfly(BaseForm); 24 | -------------------------------------------------------------------------------- /src/BoolField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Checkbox, 4 | CheckboxProps, 5 | Switch, 6 | SwitchProps, 7 | } from '@patternfly/react-core'; 8 | import { connectField, FieldProps } from 'uniforms'; 9 | 10 | import wrapField from './wrapField'; 11 | 12 | enum ComponentType { 13 | checkbox = 'checkbox', 14 | switch = 'switch', 15 | } 16 | 17 | export type BoolFieldProps = FieldProps< 18 | boolean, 19 | CheckboxProps & SwitchProps, 20 | { 21 | appearance?: ComponentType; 22 | inputRef: React.RefObject & 23 | React.RefObject; 24 | } 25 | >; 26 | 27 | function Bool({ 28 | appearance, 29 | disabled, 30 | id, 31 | inputRef, 32 | label, 33 | name, 34 | onChange, 35 | value, 36 | ...props 37 | }: BoolFieldProps) { 38 | const Component = appearance === ComponentType.switch ? Switch : Checkbox; 39 | return wrapField( 40 | { id, ...props }, 41 | disabled || onChange(!value)} 47 | ref={inputRef} 48 | label={label} 49 | /> 50 | ); 51 | } 52 | 53 | Bool.defaultProps = { appearance: ComponentType.checkbox }; 54 | 55 | export default connectField(Bool); 56 | -------------------------------------------------------------------------------- /src/DateField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useMemo } from 'react'; 2 | import { connectField, FieldProps } from 'uniforms'; 3 | import { 4 | DatePicker, 5 | Flex, 6 | FlexItem, 7 | InputGroup, 8 | TextInputProps, 9 | TimePicker, 10 | } from '@patternfly/react-core'; 11 | 12 | import wrapField from './wrapField'; 13 | 14 | export type DateFieldProps = FieldProps< 15 | Date, 16 | TextInputProps, 17 | { 18 | inputRef: React.RefObject; 19 | max?: string; 20 | min?: string; 21 | } 22 | >; 23 | 24 | const DateConstructor = (typeof global === 'object' ? global : window).Date; 25 | 26 | function DateField(props: DateFieldProps) { 27 | const parseDate = useCallback(() => { 28 | if (!props.value) { 29 | return ''; 30 | } 31 | return props.value.toISOString().slice(0, -14); 32 | }, [props.value]); 33 | 34 | const parseTime = useCallback(() => { 35 | if (!props.value) { 36 | return ''; 37 | } 38 | return `${props.value.getUTCHours()}:${props.value.getUTCMinutes()}`; 39 | }, [props.value]); 40 | 41 | const onDateChange = useCallback( 42 | (value: string, date?: Date) => { 43 | if (!date) { 44 | props.onChange(date); 45 | } else { 46 | const newDate = new DateConstructor(date); 47 | const time = parseTime(); 48 | if (time !== '') { 49 | newDate.setUTCHours(parseInt(time?.split(':')[0])); 50 | newDate.setUTCMinutes(parseInt(time?.split(':')[1]?.split(' ')[0])); 51 | } else { 52 | newDate.setUTCHours(0); 53 | newDate.setUTCMinutes(0); 54 | } 55 | props.onChange(newDate); 56 | } 57 | }, 58 | [props.onChange, parseTime] 59 | ); 60 | 61 | const isInvalid = useMemo(() => { 62 | if (props.value) { 63 | if (props.min) { 64 | const minDate = new Date(props.min); 65 | if (minDate.toString() === 'Invalid Date') { 66 | return false; 67 | } else if (props.value < minDate) { 68 | return `Should be after ${minDate.toISOString()}`; 69 | } 70 | } 71 | if (props.max) { 72 | const maxDate = new Date(props.max); 73 | if (maxDate.toString() === 'Invalid Date') { 74 | return false; 75 | } else if (props.value > maxDate) { 76 | return `Should be before ${maxDate.toISOString()}`; 77 | } 78 | } 79 | } 80 | return false; 81 | }, [props.value]); 82 | 83 | const onTimeChange = useCallback( 84 | (time: string, hours?: number, minutes?: number) => { 85 | if (props.value) { 86 | const newDate = new DateConstructor(props.value); 87 | if (hours && minutes) { 88 | newDate.setUTCHours(hours); 89 | newDate.setUTCMinutes(minutes); 90 | } else if (time !== '') { 91 | const localeHours = parseInt(time?.split(':')[0]); 92 | const localeMinutes = parseInt(time?.split(':')[1]?.split(' ')[0]); 93 | if (!isNaN(localeHours) && !isNaN(localeMinutes)) { 94 | newDate.setUTCHours(localeHours); 95 | newDate.setUTCMinutes(localeMinutes); 96 | } 97 | } else { 98 | newDate.setUTCHours(0); 99 | newDate.setUTCMinutes(0); 100 | } 101 | props.onChange(newDate); 102 | } 103 | }, 104 | [props.onChange, props.value] 105 | ); 106 | 107 | return wrapField( 108 | props, 109 | 116 | 117 | 118 | 126 | 136 | 137 | 138 | {isInvalid && ( 139 |
147 | {isInvalid} 148 |
149 | )} 150 |
151 | ); 152 | } 153 | 154 | export default connectField(DateField); 155 | -------------------------------------------------------------------------------- /src/ErrorField.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLProps } from 'react'; 2 | import { connectField, filterDOMProps } from 'uniforms'; 3 | 4 | export type ErrorFieldProps = { 5 | error?: any; 6 | errorMessage?: string; 7 | } & HTMLProps; 8 | 9 | const Error = ({ children, error, errorMessage, ...props }: ErrorFieldProps) => 10 | !error ? null : ( 11 |
12 | {children ? ( 13 | children 14 | ) : ( 15 |
{errorMessage}
16 | )} 17 |
18 | ); 19 | 20 | Error.defaultProps = { 21 | style: { 22 | backgroundColor: 'rgba(255, 85, 0, 0.2)', 23 | border: '1px solid rgb(255, 85, 0)', 24 | borderRadius: '7px', 25 | margin: '20px 0px', 26 | padding: '10px', 27 | }, 28 | }; 29 | 30 | export default connectField(Error, { initialValue: false }); 31 | -------------------------------------------------------------------------------- /src/ErrorsField.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLProps } from 'react'; 2 | import { useForm, filterDOMProps } from 'uniforms'; 3 | 4 | export type ErrorsFieldProps = HTMLProps; 5 | 6 | function ErrorsField({ children, ...props }: ErrorsFieldProps) { 7 | const { error, schema } = useForm(); 8 | 9 | return !error && !children ? null : ( 10 |
11 | {children} 12 |
    13 | {schema.getErrorMessages(error).map((message, index) => ( 14 |
  • 15 | {message} 16 |
  • 17 | ))} 18 |
19 |
20 | ); 21 | } 22 | 23 | ErrorsField.defaultProps = { 24 | style: { 25 | backgroundColor: 'rgba(255, 85, 0, 0.2)', 26 | border: '1px solid rgb(255, 85, 0)', 27 | borderRadius: '7px', 28 | margin: '20px 0px', 29 | padding: '10px', 30 | }, 31 | }; 32 | 33 | export default ErrorsField; 34 | -------------------------------------------------------------------------------- /src/HiddenField.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLProps, Ref, useEffect } from 'react'; 2 | import { useField, filterDOMProps } from 'uniforms'; 3 | 4 | export type HiddenFieldProps = { 5 | inputRef?: Ref; 6 | name: string; 7 | noDOM?: boolean; 8 | value?: any; 9 | } & HTMLProps; 10 | 11 | export default function HiddenField({ value, ...rawProps }: HiddenFieldProps) { 12 | const props = useField(rawProps.name, rawProps, { initialValue: false })[0]; 13 | 14 | useEffect(() => { 15 | if (value !== undefined && value !== props.value) props.onChange(value); 16 | }); 17 | 18 | return props.noDOM ? null : ( 19 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/ListAddField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cloneDeep from 'lodash/cloneDeep'; 3 | import { Button, ButtonProps } from '@patternfly/react-core'; 4 | import { PlusCircleIcon } from '@patternfly/react-icons'; 5 | import { 6 | connectField, 7 | FieldProps, 8 | filterDOMProps, 9 | joinName, 10 | useField, 11 | } from 'uniforms'; 12 | 13 | export type ListAddFieldProps = FieldProps< 14 | unknown, 15 | ButtonProps, 16 | { 17 | initialCount?: number; 18 | parent?: any; 19 | name: string; 20 | disabled?: boolean; 21 | value?: unknown; 22 | } 23 | >; 24 | 25 | function ListAdd({ 26 | disabled = false, 27 | name, 28 | value, 29 | ...props 30 | }: ListAddFieldProps) { 31 | const nameParts = joinName(null, name); 32 | const parentName = joinName(nameParts.slice(0, -1)); 33 | const parent = useField<{ maxCount?: number }, unknown[]>( 34 | parentName, 35 | {}, 36 | { absoluteName: true } 37 | )[0]; 38 | 39 | const limitNotReached = 40 | !disabled && !(parent.maxCount! <= parent.value!.length); 41 | 42 | return ( 43 | 56 | ); 57 | } 58 | 59 | export default connectField(ListAdd, { 60 | initialValue: false, 61 | kind: 'leaf', 62 | }); 63 | -------------------------------------------------------------------------------- /src/ListDelField.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { Button, ButtonProps } from '@patternfly/react-core'; 3 | import { MinusCircleIcon } from '@patternfly/react-icons'; 4 | import { 5 | connectField, 6 | FieldProps, 7 | filterDOMProps, 8 | joinName, 9 | useField, 10 | } from 'uniforms'; 11 | 12 | export type ListDelFieldProps = FieldProps< 13 | unknown, 14 | ButtonProps, 15 | { icon?: ReactNode } 16 | >; 17 | 18 | function ListDel({ name, disabled, ...props }: ListDelFieldProps) { 19 | const nameParts = joinName(null, name); 20 | const nameIndex = +nameParts[nameParts.length - 1]; 21 | const parentName = joinName(nameParts.slice(0, -1)); 22 | const parent = useField<{ minCount?: number }, unknown[]>( 23 | parentName, 24 | {}, 25 | { absoluteName: true } 26 | )[0]; 27 | 28 | const limitNotReached = 29 | !disabled && !(parent.minCount! >= parent.value!.length); 30 | 31 | return ( 32 | 45 | ); 46 | } 47 | 48 | export default connectField(ListDel, { 49 | initialValue: false, 50 | kind: 'leaf', 51 | }); 52 | -------------------------------------------------------------------------------- /src/ListField.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Children, 3 | cloneElement, 4 | isValidElement, 5 | ReactNode, 6 | } from 'react'; 7 | import { Split, SplitItem, Tooltip } from '@patternfly/react-core'; 8 | import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; 9 | import { connectField, filterDOMProps, HTMLFieldProps } from 'uniforms'; 10 | 11 | import ListItemField from './ListItemField'; 12 | import ListAddField from './ListAddField'; 13 | 14 | export type ListFieldProps = HTMLFieldProps< 15 | unknown[], 16 | HTMLDivElement, 17 | { 18 | children?: ReactNode; 19 | info?: string; 20 | error?: boolean; 21 | initialCount?: number; 22 | itemProps?: object; 23 | showInlineError?: boolean; 24 | } 25 | >; 26 | 27 | declare module 'uniforms' { 28 | interface FilterDOMProps { 29 | wrapperCol: never; 30 | labelCol: never; 31 | } 32 | } 33 | 34 | filterDOMProps.register('minCount', 'wrapperCol', 'labelCol'); 35 | 36 | function ListField({ 37 | children = , 38 | error, 39 | errorMessage, 40 | info, 41 | initialCount, 42 | itemProps, 43 | label, 44 | name, 45 | value, 46 | showInlineError, 47 | ...props 48 | }: ListFieldProps) { 49 | return ( 50 |
51 | 52 | 53 | {label && ( 54 | 65 | )} 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 | {value?.map((item, itemIndex) => 75 | Children.map(children, (child, childIndex) => 76 | isValidElement(child) 77 | ? cloneElement(child, { 78 | key: `${itemIndex}-${childIndex}`, 79 | name: child.props.name?.replace('$', '' + itemIndex), 80 | ...itemProps, 81 | }) 82 | : child 83 | ) 84 | )} 85 |
86 |
87 | ); 88 | } 89 | 90 | export default connectField(ListField); 91 | -------------------------------------------------------------------------------- /src/ListItemField.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { connectField } from 'uniforms'; 3 | 4 | import AutoField from './AutoField'; 5 | import ListDelField from './ListDelField'; 6 | 7 | export type ListItemFieldProps = { 8 | children?: ReactNode; 9 | value?: unknown; 10 | }; 11 | 12 | function ListItem({ 13 | children = , 14 | }: ListItemFieldProps) { 15 | return ( 16 |
23 |
{children}
24 |
25 | 26 |
27 |
28 | ); 29 | } 30 | 31 | export default connectField(ListItem, { 32 | initialValue: false, 33 | }); 34 | -------------------------------------------------------------------------------- /src/LongTextField.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextArea } from '@patternfly/react-core'; 3 | import { connectField, filterDOMProps, HTMLFieldProps } from 'uniforms'; 4 | 5 | export type LongTextFieldProps = HTMLFieldProps< 6 | string, 7 | HTMLDivElement, 8 | { 9 | inputRef: React.RefObject; 10 | onChange: ( 11 | value: string, 12 | event: React.ChangeEvent 13 | ) => void; 14 | value?: string; 15 | prefix?: string; 16 | } 17 | >; 18 | 19 | const LongText = ({ 20 | disabled, 21 | id, 22 | inputRef, 23 | label, 24 | name, 25 | onChange, 26 | placeholder, 27 | value, 28 | ...props 29 | }: LongTextFieldProps) => ( 30 |
31 | {label && } 32 |