├── .browserslistrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .gitlab-ci.yml ├── .gitlab └── issue_templates │ ├── Bug.md │ ├── Feature Proposal.md │ └── Research Proposal.md ├── .npmignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── playground ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── App.vue │ ├── components │ │ ├── Playground.vue │ │ └── PlaygroundPanel.vue │ ├── libs │ │ └── Prism.js │ ├── main.js │ ├── schema │ │ ├── array.json │ │ ├── dependencies.json │ │ ├── facebook.signup.descriptor.json │ │ ├── facebook.signup.schema.json │ │ ├── input.hidden.descriptor.json │ │ ├── input.hidden.schema.json │ │ ├── nestedarray.descriptor.js │ │ ├── nestedarray.model.js │ │ ├── nestedarray.schema.json │ │ ├── newsletter.descriptor.js │ │ ├── newsletter.schema.json │ │ ├── number.json │ │ ├── object.recursive.descriptor.json │ │ ├── object.recursive.schema.json │ │ └── personalinfo.json │ └── styles │ │ └── Prism.css └── vue.config.js ├── rollup.config.js ├── src ├── components │ ├── ArrayButtonElement.ts │ ├── ArrayElement.ts │ ├── FieldElement.ts │ ├── FieldsetElement.ts │ ├── FormSchema.ts │ ├── HelperElement.ts │ ├── InputElement.ts │ ├── ListElement.ts │ ├── MessageElement.ts │ ├── StateElement.ts │ └── TextareaElement.ts ├── descriptors │ ├── ArrayUIDescriptor.ts │ ├── EnumUIDescriptor.ts │ ├── ItemUIDescriptor.ts │ ├── ListUIDescriptor.ts │ ├── ObjectUIDescriptor.ts │ ├── ScalarUIDescriptor.ts │ ├── UIDescriptor.ts │ └── index.ts ├── lib │ ├── Arrays.ts │ ├── Components.ts │ ├── CreateInput.ts │ ├── Field.ts │ ├── Fieldset.ts │ ├── NativeComponents.ts │ ├── NativeElements.ts │ ├── Objects.ts │ ├── Pattern.ts │ ├── Schema.ts │ ├── UniqueId.ts │ └── Value.ts └── parsers │ ├── ArrayParser.ts │ ├── BooleanParser.ts │ ├── EnumParser.ts │ ├── IntegerParser.ts │ ├── ListParser.ts │ ├── NullParser.ts │ ├── NumberParser.ts │ ├── ObjectParser.ts │ ├── Parser.ts │ ├── ScalarParser.ts │ ├── SetParser.ts │ ├── StringParser.ts │ └── index.ts ├── tests ├── .eslintrc.js ├── lib │ ├── Options.ts │ └── TestParser.ts └── specs │ ├── components │ ├── ArrayElement.spec.ts │ ├── FieldElement.spec.ts │ ├── FieldsetElement.spec.ts │ ├── FormSchema.spec.ts │ ├── HelperElement.spec.ts │ ├── InputElement.spec.ts │ ├── ListElement.spec.ts │ ├── TextareaElement.spec.ts │ └── __snapshots__ │ │ ├── ArrayElement.spec.ts.snap │ │ ├── FieldElement.spec.ts.snap │ │ ├── FieldsetElement.spec.ts.snap │ │ ├── FormSchema.spec.ts.snap │ │ ├── HelperElement.spec.ts.snap │ │ ├── InputElement.spec.ts.snap │ │ ├── ListElement.spec.ts.snap │ │ └── TextareaElement.spec.ts.snap │ ├── descriptors │ ├── ObjectDescriptor.spec.ts │ └── ScalarUIDescriptor.spec.ts │ ├── lib │ ├── Arrays.spec.ts │ ├── Components.spec.ts │ ├── CreateInput.spec.ts │ ├── NativeElements.spec.ts │ ├── Objects.spec.ts │ ├── Pattern.spec.ts │ ├── Schema.spec.ts │ └── UniqueId.spec.ts │ └── parsers │ ├── ArrayParser.spec.ts │ ├── BooleanParser.spec.ts │ ├── EnumParser.spec.ts │ ├── IntegerParser.spec.ts │ ├── ListParser.spec.ts │ ├── NullParser.spec.ts │ ├── NumberParser.spec.ts │ ├── ObjectParser.spec.ts │ ├── Parser.spec.ts │ ├── ScalarParser.spec.ts │ └── StringParser.spec.ts ├── tsconfig.json ├── types ├── index.d.ts └── jsonschema.d.ts └── vuedoc.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | playground/ 2 | .eslintrc.js 3 | vuedoc.config.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable quote-props */ 2 | 3 | module.exports = { 4 | root: true, 5 | env: { 6 | browser: true 7 | }, 8 | parser: '@typescript-eslint/parser', 9 | plugins: [ 10 | 'import', 11 | '@typescript-eslint' 12 | ], 13 | extends: [ 14 | 'eslint:recommended', 15 | 'airbnb-base', 16 | 'plugin:import/errors', 17 | 'plugin:import/warnings', 18 | 'plugin:import/typescript', 19 | 'plugin:@typescript-eslint/eslint-recommended', 20 | 'plugin:@typescript-eslint/recommended' 21 | ], 22 | settings: { 23 | 'import/resolver': { 24 | typescript: {} 25 | } 26 | }, 27 | rules: { 28 | 'import/no-unresolved': 'error', 29 | 'import/extensions': 'off', 30 | '@typescript-eslint/indent': ['error', 2], 31 | '@typescript-eslint/explicit-function-return-type': 'off', 32 | '@typescript-eslint/no-explicit-any': 'off', 33 | '@typescript-eslint/explicit-member-accessibility': 'off', 34 | '@typescript-eslint/interface-name-prefix': 'off', 35 | '@typescript-eslint/no-empty-interface': 'off', 36 | '@typescript-eslint/no-this-alias': 'off', 37 | '@typescript-eslint/ban-types': 'off', 38 | 'import/prefer-default-export': 'off', 39 | 'import/no-extraneous-dependencies': 'off', 40 | 'import/no-cycle': 'off', 41 | 'comma-dangle': ['error', 'never'], 42 | 'prefer-destructuring': 'off', 43 | 'semi': ['error', 'always'], 44 | 'class-methods-use-this': 'error', 45 | 'block-scoped-var': 'error', 46 | 'no-console': 'error', 47 | 'no-debugger': 'error', 48 | 'no-lonely-if': 'error', 49 | 'no-plusplus': 'off', 50 | 'no-continue': 'off', 51 | 'lines-between-class-members': 'off', 52 | 'class-methods-use-this': ['error', { 53 | exceptMethods: ['type', 'kind', 'parseValue'] 54 | }], 55 | 'max-len': [ 56 | 'error', 57 | { 58 | code: 125, 59 | comments: 125, 60 | tabWidth: 2, 61 | ignoreUrls: true, 62 | ignoreRegExpLiterals: true, 63 | ignoreTemplateLiterals: true, 64 | ignorePattern: '(@returns|@param)' 65 | } 66 | ], 67 | 'complexity': [ 'error', { max: 40 } ], 68 | 'no-use-before-define': [ 69 | 'error', 70 | { 71 | classes: false, 72 | functions: false 73 | } 74 | ], 75 | 'arrow-parens': [ 'error', 'always' ], 76 | 'arrow-body-style': 'off', 77 | 'object-shorthand': 'off', 78 | 'guard-for-in': 'off', 79 | 'no-nested-ternary': 'off', 80 | 'object-curly-newline': 'off', 81 | 'array-bracket-spacing': [ 'error', 'always' ], 82 | 'no-param-reassign': 'off', 83 | 'default-case': 'off', 84 | 'no-shadow': 'off', 85 | 'no-restricted-syntax': 'off', 86 | 'no-prototype-builtins': 'off', 87 | 'space-before-function-paren': 'off', 88 | 'no-var': 'error', 89 | 'padding-line-between-statements': [ 90 | 'error', 91 | { 'blankLine': 'always', 'prev': 'block-like', 'next': '*' }, 92 | { 'blankLine': 'always', 'prev': '*', 'next': 'block-like' }, 93 | { 'blankLine': 'any', 'prev': 'block-like', 'next': ['block-like', 'break'] }, 94 | // require blank lines before all return statements 95 | { 'blankLine': 'always', 'prev': '*', 'next': 'return' }, 96 | // require blank lines after every sequence of variable declarations 97 | { 'blankLine': 'always', 'prev': ['const', 'let', 'var'], 'next': '*'}, 98 | { 'blankLine': 'any', 'prev': ['const', 'let', 'var'], 'next': ['const', 'let', 'var']}, 99 | // require blank lines after all directive prologues 100 | { 'blankLine': 'always', 'prev': 'directive', 'next': '*' }, 101 | { 'blankLine': 'any', 'prev': 'directive', 'next': 'directive' } 102 | ] 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | *.tgz 40 | *~ 41 | *.kate-swp 42 | .directory 43 | dist 44 | *.kdev4 45 | .codeline 46 | .vscode 47 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:lts-alpine 2 | 3 | stages: 4 | - test 5 | - dependencies 6 | - build 7 | - publish 8 | 9 | code quality: 10 | stage: test 11 | script: 12 | - npm ci 13 | - npm install --save-dev eslint eslint-formatter-gitlab 14 | - npm run lint -- --format gitlab . 15 | artifacts: 16 | reports: 17 | codequality: gl-codequality.json 18 | 19 | test & coverage: 20 | stage: test 21 | script: 22 | - npm ci 23 | - npm i -D jest-junit 24 | - npm test -- --ci --reporters=default --reporters=jest-junit 25 | coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/' 26 | artifacts: 27 | when: always 28 | reports: 29 | junit: 30 | - junit.xml 31 | 32 | outdated: 33 | stage: dependencies 34 | script: 35 | - npm outdated 36 | allow_failure: true 37 | 38 | security scan: 39 | stage: dependencies 40 | script: 41 | # upgrade the NPM version to the latest to be sure 42 | # the `npm audit` supports the --production flag 43 | - npm i -g npm 44 | - npm audit --production 45 | allow_failure: false 46 | 47 | rollup build: 48 | stage: build 49 | script: 50 | - npm ci 51 | - npm run build 52 | artifacts: 53 | paths: 54 | - dist 55 | expire_in: 4 weeks 56 | 57 | package: 58 | stage: build 59 | only: 60 | - tags 61 | script: 62 | - npm pack 63 | artifacts: 64 | paths: 65 | - ./*.tgz 66 | 67 | publish on npm: 68 | stage: publish 69 | needs: 70 | - package 71 | - rollup build 72 | only: 73 | - tags 74 | - triggers 75 | script: 76 | - echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc 77 | - npm publish 78 | environment: 79 | name: npm 80 | url: https://www.npmjs.com/package/@formschema/native 81 | -------------------------------------------------------------------------------- /.gitlab/issue_templates/Bug.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Summary 10 | 11 | (Summarize the bug encountered concisely) 12 | 13 | ### Steps to reproduce 14 | 15 | (How one can reproduce the issue - this is very important) 16 | 17 | ### Example Project 18 | 19 | (If possible, please create an example project here on [CodeSandbox](https://codesandbox.io/) or GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report) 20 | 21 | (If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version) 22 | 23 | ### What is the current *bug* behavior? 24 | 25 | (What actually happens) 26 | 27 | ### What is the expected *correct* behavior? 28 | 29 | (What you should see instead) 30 | 31 | ### Possible fixes 32 | 33 | (If you can, link to the line of code that might be responsible for the problem) 34 | 35 | /label ~bug 36 | -------------------------------------------------------------------------------- /.gitlab/issue_templates/Feature Proposal.md: -------------------------------------------------------------------------------- 1 | ### Problem to solve 2 | 3 | ### Further details 4 | 5 | (Include use cases, benefits, and/or goals) 6 | 7 | ### Proposal 8 | 9 | ### What does success look like, and how can we measure that? 10 | 11 | (If no way to measure success, link to an issue that will implement a way to measure this) 12 | 13 | ### Links / references 14 | 15 | /label ~"feature proposal" 16 | -------------------------------------------------------------------------------- /.gitlab/issue_templates/Research Proposal.md: -------------------------------------------------------------------------------- 1 | ### Background: 2 | 3 | (Include problem, use cases, benefits, and/or goals) 4 | 5 | **What questions are you trying to answer?** 6 | 7 | **Are you looking to verify an existing hypothesis or uncover new issues you should be exploring?** 8 | 9 | **What is the backstory of this project and how does it impact the approach?** 10 | 11 | **What do you already know about the areas you are exploring?** 12 | 13 | **What does success look like at the end of the project?** 14 | 15 | ### Links / references: 16 | 17 | /label ~"UX research" 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests 2 | src 3 | dist 4 | playground 5 | coverage 6 | tsconfig.json 7 | .browserslistrc 8 | .directory 9 | .eslintignore 10 | .gitignore 11 | *.tgz 12 | /*.js 13 | *.yml 14 | .gitlab 15 | .codeline 16 | .vscode 17 | *.png 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting merge requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct. 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are 25 | not aligned to this Code of Conduct. 26 | 27 | By adopting this Code of Conduct, project maintainers commit themselves to 28 | fairly and consistently applying these principles to every aspect of managing 29 | this project. 30 | 31 | Project maintainers who do not follow or enforce the Code of Conduct may be 32 | permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by opening an issue or contacting one or more of the project maintainers. 39 | 40 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, 41 | available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sébastien Demanou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env' 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | expand: true, 3 | notify: true, 4 | collectCoverage: true, 5 | collectCoverageFrom: [ 6 | 'src/**/*.ts' 7 | ], 8 | moduleFileExtensions: [ 9 | 'js', 'json', 'ts' 10 | ], 11 | transform: { 12 | '^.+\\.ts$': 'ts-jest' 13 | }, 14 | transformIgnorePatterns: [ 15 | '/node_modules/' 16 | ], 17 | moduleNameMapper: { 18 | '^@/types': '/src/types/index.d.ts', 19 | '^@/(.*)$': '/src/$1' 20 | }, 21 | snapshotSerializers: [ 22 | '/node_modules/jest-serializer-vue' 23 | ], 24 | testMatch: [ 25 | '**/tests/specs/**/*.spec.ts' 26 | ], 27 | testURL: 'http://localhost/', 28 | watchPlugins: [ 29 | 'jest-watch-typeahead/filename', 30 | 'jest-watch-typeahead/testname' 31 | ], 32 | watchPathIgnorePatterns: [ 33 | '/dist', 34 | '/coverage', 35 | '/playground', 36 | '/node_modules' 37 | ], 38 | globals: { 39 | describe: true, 40 | it: true, 41 | expect: true, 42 | 'ts-jest': { 43 | babelConfig: true 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@formschema/native", 3 | "version": "2.0.0-beta.6", 4 | "description": "Vue component form based on JSON Schema", 5 | "main": "dist/FormSchema.esm.min.js", 6 | "browser": "dist/FormSchema.umd.min.js", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "playground": "cd playground && npm run serve", 10 | "playground:install": "cd playground && npm i", 11 | "test": "vue-cli-service test:unit", 12 | "testdebug": "node --inspect-brk ./node_modules/@vue/cli-service/bin/vue-cli-service.js test:unit --runInBand", 13 | "lint": "eslint src/**/* tests/**/*.ts tests/**/specs/**/*.ts", 14 | "lint:fix": "eslint --fix src/**/* tests/**/*.ts tests/**/specs/**/*.ts", 15 | "gimtoc": "gimtoc -f README.md -s 'Table of Contents' -o README.md", 16 | "readme": "vuedoc.md -c && npm run gimtoc && git diff README.md", 17 | "build": "rollup -c" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://gitlab.com/formschema/native.git" 22 | }, 23 | "keywords": [ 24 | "vue", 25 | "form", 26 | "json", 27 | "schema", 28 | "jsonschema" 29 | ], 30 | "author": "Sébastien Demanou", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://gitlab.com/formschema/native/issues" 34 | }, 35 | "homepage": "https://gitlab.com/formschema/native", 36 | "devDependencies": { 37 | "@babel/core": "^7.11.1", 38 | "@babel/preset-env": "^7.11.0", 39 | "@types/jest": "^26.0.10", 40 | "@types/node": "^15.0.1", 41 | "@types/sinon": "^10.0.0", 42 | "@typescript-eslint/eslint-plugin": "^3.9.1", 43 | "@typescript-eslint/parser": "^3.9.1", 44 | "@vue/cli-plugin-unit-jest": "^4.5.4", 45 | "@vue/cli-service": "^4.5.4", 46 | "@vue/test-utils": "^1.0.4", 47 | "@vuedoc/md": "^3.0.0-beta2", 48 | "@vuedoc/parser": "^3.0.0-beta2", 49 | "babel-jest": "^26.3.0", 50 | "eslint": "^7.7.0", 51 | "eslint-config-airbnb": "^18.2.0", 52 | "eslint-import-resolver-typescript": "^2.2.1", 53 | "eslint-plugin-import": "^2.22.0", 54 | "gimtoc": "^1.3.3", 55 | "jest-serializer-vue": "^2.0.2", 56 | "rollup": "^2.26.4", 57 | "rollup-plugin-node-resolve": "^5.2.0", 58 | "rollup-plugin-terser": "^7.0.2", 59 | "rollup-plugin-typescript": "^1.0.1", 60 | "ts-jest": "^26.2.0", 61 | "tslib": "^2.0.1", 62 | "typescript": "^4.2.4", 63 | "typescript-eslint-parser": "^22.0.0", 64 | "vue": "^2.6.11", 65 | "vue-template-compiler": "^2.6.11" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --port 8183" 7 | }, 8 | "dependencies": { 9 | "ajv": "^6.10.2", 10 | "json-schema-ref-parser": "^7.1.1", 11 | "vue": "^2.6.10", 12 | "vue-prism-editor": "^0.3.0" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "^3.11.0", 16 | "@vue/cli-service": "^3.11.0", 17 | "stylus": "^0.54.7", 18 | "stylus-loader": "^3.0.2" 19 | }, 20 | "babel": { 21 | "presets": [ 22 | "@vue/app" 23 | ] 24 | }, 25 | "postcss": { 26 | "plugins": { 27 | "autoprefixer": {} 28 | } 29 | }, 30 | "browserslist": [ 31 | "> 1%", 32 | "last 2 versions", 33 | "not ie <= 8" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /playground/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | FormSchema Playground 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 60 | -------------------------------------------------------------------------------- /playground/src/components/PlaygroundPanel.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | -------------------------------------------------------------------------------- /playground/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App) 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /playground/src/schema/array.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "title": "Array fields", 4 | "properties": { 5 | "listOfStrings": { 6 | "type": "array", 7 | "title": "A list of strings", 8 | "minItems": 3, 9 | "maxItems": 3, 10 | "items": { 11 | "type": "string" 12 | }, 13 | "additionalItems": { 14 | "type": "number", 15 | "default": "" 16 | }, 17 | "default": ["1", "22", "333"] 18 | }, 19 | "addr": { 20 | "type": "array", 21 | "title": "With additional item", 22 | "items": [ 23 | { 24 | "type": "number" 25 | }, 26 | { 27 | "type": "string" 28 | }, 29 | { 30 | "type": "string", 31 | "enum": ["Street", "Avenue", "Boulevard"] 32 | }, 33 | { 34 | "type": "string", 35 | "enum": ["NW", "NE", "SW", "SE"] 36 | } 37 | ], 38 | "additionalItems": { 39 | "type": "number", 40 | "default": "" 41 | } 42 | }, 43 | "address": { 44 | "type": "array", 45 | "title": "Address", 46 | "items": [ 47 | { 48 | "type": "number" 49 | }, 50 | { 51 | "type": "string" 52 | }, 53 | { 54 | "type": "string", 55 | "enum": ["Street", "Avenue", "Boulevard"] 56 | }, 57 | { 58 | "type": "string", 59 | "enum": ["NW", "NE", "SW", "SE"] 60 | } 61 | ] 62 | }, 63 | "unique": { 64 | "type": "array", 65 | "title": "Unique items", 66 | "uniqueItems": true, 67 | "maxItems": 3 68 | }, 69 | "multipleChoicesList": { 70 | "type": "array", 71 | "title": "A multiple choices list", 72 | "items": { 73 | "type": "string", 74 | "enum": [ 75 | "foo", 76 | "bar", 77 | "fuzz", 78 | "qux" 79 | ] 80 | }, 81 | "default": ["fuzz"], 82 | "uniqueItems": true 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /playground/src/schema/dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Property dependencies", 3 | "description": "These samples are best viewed without live validation.", 4 | "type": "object", 5 | "properties": { 6 | "schemaDependencies": { 7 | "type": "object", 8 | "title": "Schema dependencies", 9 | 10 | "properties": { 11 | "name": { "type": "string", "title": "Name" }, 12 | "credit_card": { "type": "number", "title": "Credit Card" } 13 | }, 14 | 15 | "required": ["name"], 16 | 17 | "dependencies": { 18 | "credit_card": { 19 | "properties": { 20 | "billing_address": { "type": "string", "title": "Billing Address" } 21 | }, 22 | "required": ["billing_address"] 23 | } 24 | } 25 | }, 26 | "unidirectional": { 27 | "title": "Unidirectional", 28 | "src": "https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies", 29 | "type": "object", 30 | "properties": { 31 | "name": { 32 | "type": "string", 33 | "title": "Name" 34 | }, 35 | "credit_card": { 36 | "type": "number", 37 | "title": "Credit Card", 38 | "description": "If you enter anything here then billing_address will become required" 39 | }, 40 | "billing_address": { 41 | "type": "string", 42 | "title": "Billing Address", 43 | "description": "It’s okay to have a billing address without a credit card number" 44 | } 45 | }, 46 | "required": [ 47 | "name" 48 | ], 49 | "dependencies": { 50 | "credit_card": [ 51 | "billing_address" 52 | ] 53 | } 54 | }, 55 | "bidirectional": { 56 | "title": "Bidirectional", 57 | "src": "https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies", 58 | "description": "Dependencies are not bidirectional, you can, of course, define the bidirectional dependencies explicitly.", 59 | "type": "object", 60 | "properties": { 61 | "name": { 62 | "type": "string", 63 | "title": "Name" 64 | }, 65 | "credit_card": { 66 | "type": "number", 67 | "title": "Credit Card", 68 | "description": "If you enter anything here then billing_address will become required" 69 | }, 70 | "billing_address": { 71 | "type": "string", 72 | "title": "Billing Address", 73 | "description": "If you enter anything here then credit_card will become required" 74 | } 75 | }, 76 | "required": [ 77 | "name" 78 | ], 79 | "dependencies": { 80 | "credit_card": [ 81 | "billing_address" 82 | ], 83 | "billing_address": [ 84 | "credit_card" 85 | ] 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /playground/src/schema/facebook.signup.descriptor.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Sign Up", 3 | "helper": "It’s free and always will be.", 4 | "groups": { 5 | "birthday": { 6 | "label": "Birthday", 7 | "properties": [ "birthday_month", "birthday_day", "birthday_year" ] 8 | }, 9 | "custom_gender": { 10 | "properties": [ "preferred_pronoun", "custom_gender" ] 11 | } 12 | }, 13 | "properties": { 14 | "firstname": { 15 | "attrs": { 16 | "placeholder": "First name", 17 | "title": "What's your name?" 18 | } 19 | }, 20 | "lastname": { 21 | "attrs": { 22 | "placeholder": "Last name", 23 | "title": "What's your name?" 24 | } 25 | }, 26 | "reg_email__": { 27 | "attrs": { 28 | "placeholder": "Mobile number or email", 29 | "title": "You'll use this when you log in and if you ever need to reset your password" 30 | } 31 | }, 32 | "reg_email_confirmation__": { 33 | "attrs": { 34 | "placeholder": "Re-enter email", 35 | "title": "Please re-enter your email address." 36 | } 37 | }, 38 | "password": { 39 | "attrs": { 40 | "type": "password", 41 | "placeholder": "New password", 42 | "title": "Enter a combination of at least six numbers, letters and punctuation marks (like ! and &)." 43 | } 44 | }, 45 | "birthday_month": { 46 | "kind": "list", 47 | "options": { 48 | "generateEnumItems": true 49 | }, 50 | "props": { 51 | "placeholder": "Month" 52 | }, 53 | "items": { 54 | "1": { "label": "Jan" }, 55 | "2": { "label": "Feb" }, 56 | "3": { "label": "Mar" }, 57 | "4": { "label": "Apr" }, 58 | "5": { "label": "May" }, 59 | "6": { "label": "Jun" }, 60 | "7": { "label": "Jul" }, 61 | "8": { "label": "Aug" }, 62 | "9": { "label": "Sep" }, 63 | "10": { "label": "Oct" }, 64 | "11": { "label": "Nov" }, 65 | "12": { "label": "Dec" } 66 | } 67 | }, 68 | "birthday_day": { 69 | "kind": "list", 70 | "props": { 71 | "placeholder": "Day" 72 | } 73 | }, 74 | "birthday_year": { 75 | "kind": "list", 76 | "props": { 77 | "placeholder": "Year" 78 | } 79 | }, 80 | "sex": { 81 | "items": { 82 | "1": { "label": "Female" }, 83 | "2": { "label": "Male" }, 84 | "-1": { "label": "Custom" } 85 | } 86 | }, 87 | "preferred_pronoun": { 88 | "kind": "list", 89 | "attrs": { 90 | "title": "" 91 | }, 92 | "props": { 93 | "placeholder": "Select your pronoun" 94 | }, 95 | "items": { 96 | "1": { "label": "She: \"Wish her a happy birthday!\"" }, 97 | "2": { "label": "He: \"Wish him a happy birthday!\"" }, 98 | "6": { "label": "They: \"Wish them a happy birthday!\"" } 99 | }, 100 | "helper": "Your pronoun is visible to everyone." 101 | }, 102 | "custom_gender": { 103 | "attrs": { 104 | "placeholder": "Enter your gender (optional)" 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /playground/src/schema/facebook.signup.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "firstname": { 5 | "type": "string" 6 | }, 7 | "lastname": { 8 | "type": "string" 9 | }, 10 | "reg_email__": { 11 | "type": "string" 12 | }, 13 | "password": { 14 | "type": "string", 15 | "minLength": 6 16 | }, 17 | "birthday_month": { 18 | "type": "integer", 19 | "enum": [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ], 20 | "default": 0 21 | }, 22 | "birthday_day": { 23 | "type": "integer", 24 | "enum": [ 25 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 26 | 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 27 | ], 28 | "default": 0 29 | }, 30 | "birthday_year": { 31 | "type": "integer", 32 | "enum": [ 33 | 2019,2018,2017,2016,2015,2014,2013,2012,2011,2010,2009,2008,2007,2006, 34 | 2005,2004,2003,2002,2001,2000,1999,1998,1997,1996,1995,1994,1993,1992, 35 | 1991,1990,1989,1988,1987,1986,1985,1984,1983,1982,1981,1980,1979,1978, 36 | 1977,1976,1975,1974,1973,1972,1971,1970,1969,1968,1967,1966,1965,1964, 37 | 1963,1962,1961,1960,1959,1958,1957,1956,1955,1954,1953,1952,1951,1950, 38 | 1949,1948,1947,1946,1945,1944,1943,1942,1941,1940,1939,1938,1937,1936, 39 | 1935,1934,1933,1932,1931,1930,1929,1928,1927,1926,1925,1924,1923,1922, 40 | 1921,1920,1919,1918,1917,1916,1915,1914,1913,1912,1911,1910,1909,1908, 41 | 1907,1906,1905 42 | ], 43 | "default": 0 44 | }, 45 | "sex": { 46 | "type": "number", 47 | "enum": [ 1, 2, -1 ] 48 | } 49 | }, 50 | "dependencies": { 51 | "reg_email__": { 52 | "properties": { 53 | "reg_email_confirmation__": { 54 | "type": "string" 55 | } 56 | }, 57 | "required": [ "reg_email_confirmation__" ] 58 | }, 59 | "sex": { 60 | "properties": { 61 | "preferred_pronoun": { 62 | "type": "integer", 63 | "enum": [ 1, 2, 6 ] 64 | }, 65 | "custom_gender": { 66 | "type": "string" 67 | } 68 | }, 69 | "required": [ "preferred_pronoun" ] 70 | } 71 | }, 72 | "required": [ 73 | "firstname", 74 | "lastname", 75 | "reg_email__", 76 | "password", 77 | "birthday_month", 78 | "birthday_day", 79 | "birthday_year", 80 | "sex" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /playground/src/schema/input.hidden.descriptor.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /playground/src/schema/input.hidden.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "hidden": { 5 | "type": "string", 6 | "const": "value of the hidden input" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /playground/src/schema/nestedarray.descriptor.js: -------------------------------------------------------------------------------- 1 | export default { 2 | label: 'Customers List', 3 | helper: 'Describe your relations', 4 | properties: { 5 | contacts: { 6 | items: { 7 | properties: { 8 | name: { 9 | label: 'Customer Name', 10 | attrs: { 11 | placeholder: 'Type a name' 12 | } 13 | }, 14 | // since schema.contacts.items is not an array, 15 | // descriptor.contacts.items must be a Descriptor 16 | // instead of an array of descriptors 17 | kind: { 18 | kind: 'list', 19 | label: 'Type Customer', 20 | // since the native Main Hero 10 | 11 | 12 |
13 |
14 |
Main Monster 15 |
16 |
17 | 18 | `; 19 | 20 | exports[`components/FieldsetElement should successfully render component without schema title and description 1`] = ` 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | `; 30 | -------------------------------------------------------------------------------- /tests/specs/components/__snapshots__/FormSchema.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/FormSchema should successfully render component after the nextTick 1`] = ` 4 |
5 |
6 |
7 |
8 |

A String

9 |
10 |
11 |
12 | `; 13 | 14 | exports[`components/FormSchema should successfully render component with { schema, disabled } 1`] = ` 15 |
16 |
17 |
18 |
19 |
A String 20 |
21 |
22 |
23 |
24 | `; 25 | 26 | exports[`components/FormSchema should successfully render component with a default slot 1`] = ` 27 |
28 |
29 |
30 |
31 |

A String

32 |
33 |
checking... 34 |
35 | `; 36 | 37 | exports[`components/FormSchema should successfully render component with an object schema by using default schema ordering 1`] = ` 38 |
39 |
40 |
41 |
42 |
A String 43 |
44 |
45 |
46 |
47 |
A String 48 |
49 |
50 |
51 |
52 | `; 53 | 54 | exports[`components/FormSchema should successfully render component with an object schema by using explicit ordering fields 1`] = ` 55 |
56 |
57 |
58 |
59 |
A String 60 |
61 |
62 |
63 |
64 |
A String 65 |
66 |
67 |
68 |
69 | `; 70 | 71 | exports[`components/FormSchema should successfully render component with an object schema with groups 1`] = ` 72 |
73 |
74 |
75 |
Your name
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
Your location
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | `; 96 | 97 | exports[`components/FormSchema should successfully render component with only a scalar schema 1`] = ` 98 |
99 |
100 |
101 |
102 |

A String

103 |
104 |
105 |
106 | `; 107 | -------------------------------------------------------------------------------- /tests/specs/components/__snapshots__/HelperElement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/HelperElement should successfully render component 1`] = `

Your full name

`; 4 | 5 | exports[`components/HelperElement should successfully render component for non root field 1`] = `Your full name`; 6 | -------------------------------------------------------------------------------- /tests/specs/components/__snapshots__/InputElement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/InputElement should successfully render component 1`] = ` 4 |
5 |
6 |
7 |

Your full name

8 |
9 |
10 | `; 11 | -------------------------------------------------------------------------------- /tests/specs/components/__snapshots__/ListElement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/ListElement should successfully render component 1`] = ` 4 |
5 |
6 |
11 |

Your character

12 |
13 |
14 | `; 15 | -------------------------------------------------------------------------------- /tests/specs/components/__snapshots__/TextareaElement.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`components/TextareaElement should successfully render component 1`] = ` 4 |
5 |
6 |
7 |

Tell us about yourself

8 |
9 |
10 | `; 11 | -------------------------------------------------------------------------------- /tests/specs/descriptors/ObjectDescriptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { ObjectParser } from '@/parsers/ObjectParser'; 2 | import { TestParser, Scope } from '../../lib/TestParser'; 3 | 4 | describe('descriptors/ObjectDescriptor', () => { 5 | TestParser.Case({ 6 | case: '1.0', 7 | description: 'basic parsing', 8 | given: { 9 | parser: new ObjectParser({ 10 | schema: { 11 | type: 'object', 12 | properties: { 13 | lastname: { type: 'string' }, 14 | city: { type: 'string' }, 15 | firstname: { type: 'string' } 16 | }, 17 | required: [ 'firstname' ] 18 | }, 19 | model: { firstname: 'Jon', lastname: 'Snow', city: '' }, 20 | name: 'profile', 21 | descriptor: { 22 | order: [ 'firstname' ] 23 | } 24 | }) 25 | }, 26 | expected: { 27 | parser: { 28 | kind: ({ value }: Scope) => expect(value).toBe('object'), 29 | fields({ value }: Scope) { 30 | for (const key in value) { 31 | expect(value[key].property).toBe(key); 32 | expect(value[key].deep).toBe(1); 33 | } 34 | }, 35 | field: { 36 | value: ({ value }: Scope) => expect(value).toEqual({ firstname: 'Jon', lastname: 'Snow', city: '' }), 37 | attrs: { 38 | name: ({ value }: Scope) => expect(value).toBeUndefined(), 39 | required: ({ value }: Scope) => expect(value).toBeUndefined() 40 | }, 41 | deep: ({ value }: Scope) => expect(value).toBe(0), 42 | fields({ value }: Scope) { 43 | for (const key in value) { 44 | expect(value[key].property).toBe(key); 45 | expect(value[key].deep).toBe(1); 46 | } 47 | }, 48 | descriptor: { 49 | orderedProperties: [ 'firstname', 'lastname', 'city' ], 50 | childrenGroups: [ 51 | { 52 | children({ value }: Scope) { 53 | const expectedFieldOrders = [ 'firstname', 'lastname', 'city' ]; 54 | 55 | value.forEach((item: any, index: number) => expect(item.property).toBe(expectedFieldOrders[index])); 56 | } 57 | } 58 | ] 59 | } 60 | } 61 | } 62 | } 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/specs/descriptors/ScalarUIDescriptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from '@/parsers/Parser'; 2 | import { NativeElements } from '@/lib/NativeElements'; 3 | import { InputElement } from '@/components/InputElement'; 4 | import { TestParser, Scope } from '../../lib/TestParser'; 5 | 6 | describe('descriptors/ScalarUIDescriptor', () => { 7 | TestParser.Case({ 8 | case: '1.0', 9 | description: 'generic scalar field', 10 | given: { 11 | parser: Parser.get({ 12 | schema: { 13 | type: 'string' 14 | }, 15 | model: 'Jon Snow', 16 | name: 'name', 17 | required: true, 18 | descriptor: { 19 | label: 'Name', 20 | helper: 'Your Name', 21 | props: {}, 22 | attrs: { 23 | id: 'name' 24 | } 25 | } 26 | }) 27 | }, 28 | expected: { 29 | parser: { 30 | field: { 31 | kind: ({ value }: Scope) => expect(value).toBe('string'), 32 | value: ({ value, parser: { options } }: Scope) => expect(value).toEqual(options.model), 33 | attrs: { 34 | id: ({ value, options }: Scope) => expect(value.startsWith(`${options.name}-`)).toBeTruthy(), 35 | type: ({ value }: Scope) => expect(value).toBe('text'), 36 | name: ({ value, options }: Scope) => expect(value).toBe(options.name), 37 | required: ({ value, options }: Scope) => expect(value).toBe(options.required) 38 | }, 39 | descriptor: { 40 | kind: ({ value, parser }: Scope) => expect(value).toBe(parser.field.kind), 41 | label: ({ value, options }: Scope) => expect(value).toBe(options.descriptor.label), 42 | helper: ({ value, options }: Scope) => expect(value).toBe(options.descriptor.helper), 43 | components: ({ value }: Scope) => expect(value).toBe(NativeElements), 44 | component: ({ value }: Scope) => expect(value).toBe(InputElement), 45 | props: ({ value, options }: Scope) => expect(value).toEqual(options.descriptor.props), 46 | attrs({ value, field, options, descriptor }: any) { 47 | expect(value).not.toEqual(field.attrs); 48 | expect(value.id).toBe(options.descriptor.attrs.id); 49 | expect(value['aria-labelledby']).toBe(descriptor.labelAttrs.id); 50 | expect(value['aria-describedby']).toBe(descriptor.helperAttrs.id); 51 | }, 52 | labelAttrs: { 53 | id: ({ value }: Scope) => expect(value).toBeDefined(), 54 | for: ({ value }: Scope) => expect(value).toBeDefined() 55 | }, 56 | helperAttrs: { 57 | id: ({ value }: Scope) => expect(value).toBeDefined() 58 | } 59 | } 60 | } 61 | } 62 | } 63 | }); 64 | 65 | TestParser.Case({ 66 | case: '2.0', 67 | description: 'hidden field', 68 | given: { 69 | parser: Parser.get({ 70 | schema: { 71 | type: 'string' 72 | }, 73 | model: 'Jon Snow', 74 | name: 'name', 75 | required: true, 76 | descriptor: { 77 | kind: 'hidden', 78 | label: 'Name', 79 | helper: 'Your Name', 80 | props: {}, 81 | attrs: { 82 | id: 'name' 83 | } 84 | } 85 | }) 86 | }, 87 | expected: { 88 | parser: { 89 | field: { 90 | attrs: { 91 | id: ({ value, options }: Scope) => expect(value.startsWith(`${options.name}-`)).toBeTruthy(), 92 | type: ({ value }: Scope) => expect(value).toBe('hidden'), 93 | name: ({ value, options }: Scope) => expect(value).toBe(options.name), 94 | required: ({ value, options }: Scope) => expect(value).toBe(options.required) 95 | }, 96 | descriptor: { 97 | attrs: { 98 | id: ({ value, options }: Scope) => expect(value).toBe(options.descriptor.attrs.id), 99 | 'aria-labelledby': ({ value, descriptor }: Scope) => expect(value).toBe(descriptor.labelAttrs.id), 100 | 'aria-describedby': ({ value, descriptor }: Scope) => expect(value).toBe(descriptor.helperAttrs.id) 101 | }, 102 | kind: ({ value, parser }: Scope) => expect(value).toBe(parser.field.kind), 103 | label: ({ value, options }: Scope) => expect(value).toBe(options.descriptor.label), 104 | helper: ({ value, options }: Scope) => expect(value).toBe(options.descriptor.helper), 105 | component: ({ value }: Scope) => expect(value).toBe('input') 106 | } 107 | } 108 | } 109 | } 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /tests/specs/lib/Arrays.spec.ts: -------------------------------------------------------------------------------- 1 | import { Arrays } from '@/lib/Arrays'; 2 | 3 | describe('lib/Arrays', () => { 4 | describe('Arrays.index(items, item)', () => { 5 | it('should successfully return the item\'s index', () => { 6 | const result = Arrays.index([ 2 ], 2); 7 | const expected = 0; 8 | 9 | expect(result).toEqual(expected); 10 | }); 11 | 12 | it('should return -1 for unexistance item', () => { 13 | const result = Arrays.index([ 2 ], 1); 14 | const expected = -1; 15 | 16 | expect(result).toEqual(expected); 17 | }); 18 | }); 19 | 20 | describe('Arrays.swap(items, from, to)', () => { 21 | it('should successfully swap items', () => { 22 | const items = [ 0, 1, 2 ]; 23 | const expected = [ 2, 1, 0 ]; 24 | const movedItem = Arrays.swap(items, 0, 2); 25 | 26 | expect(items).toEqual(expected); 27 | expect(movedItem).toEqual(0); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/specs/lib/Components.spec.ts: -------------------------------------------------------------------------------- 1 | import { Components } from '@/lib/Components'; 2 | 3 | describe('lib/Components', () => { 4 | it('should have predefined items', () => { 5 | const components = new Components(); 6 | const expected = { 7 | form: 'form', 8 | default: 'input', 9 | message: 'div' 10 | }; 11 | 12 | expect(components.$).toEqual(expected); 13 | }); 14 | 15 | it('should successfully set and get a custom kind', () => { 16 | const components = new Components(); 17 | 18 | components.set('enum', 'enumc'); 19 | expect(components.get('enum')).toEqual('enumc'); 20 | }); 21 | 22 | it('should successfully get unknown kind', () => { 23 | const components = new Components(); 24 | 25 | expect(components.get('enum')).toEqual('input'); 26 | }); 27 | 28 | it('should successfully get fallback component with unknown kind', () => { 29 | const components = new Components(); 30 | 31 | expect(components.get('enum', 'span')).toEqual('span'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/specs/lib/CreateInput.spec.ts: -------------------------------------------------------------------------------- 1 | import { CreateInput } from '@/lib/CreateInput'; 2 | 3 | const MockData = { 4 | props: { 5 | field: { 6 | attrs: {}, 7 | setValue: jest.fn(), 8 | descriptor: { 9 | attrs: {} 10 | } 11 | } 12 | } 13 | }; 14 | 15 | describe('lib/CreateInput', () => { 16 | describe('CreateInput(h, tag, data, children = [])', () => { 17 | it('should create an input with required args', () => { 18 | const h: any = jest.fn(); 19 | 20 | CreateInput(h, 'input', MockData); 21 | 22 | const [ [ tag, data, children ] ] = h.mock.calls; 23 | 24 | expect(tag).toBe('input'); 25 | 26 | expect(Object.keys(data)).toEqual([ 'key', 'attrs', 'on' ]); 27 | expect(Object.keys(data.on)).toEqual([ 'input' ]); 28 | expect(typeof data.on.input).toBe('function'); 29 | 30 | expect(children).toEqual([]); 31 | }); 32 | 33 | it('should create an input with children', () => { 34 | const h: any = jest.fn(); 35 | 36 | CreateInput(h, 'input', MockData, 'hello'); 37 | 38 | const [ [ tag, data, children ] ] = h.mock.calls; 39 | 40 | expect(tag).toBe('input'); 41 | 42 | expect(Object.keys(data)).toEqual([ 'key', 'attrs', 'on' ]); 43 | expect(Object.keys(data.on)).toEqual([ 'input' ]); 44 | expect(typeof data.on.input).toBe('function'); 45 | 46 | expect(children).toEqual('hello'); 47 | }); 48 | 49 | it('should set the field value on on.input() call', () => { 50 | const h: any = jest.fn(); 51 | 52 | CreateInput(h, 'input', MockData); 53 | 54 | const [ [ /* tag */, data ] ] = h.mock.calls; 55 | 56 | data.on.input({ 57 | target: { 58 | value: 'Hello, World!' 59 | } 60 | }); 61 | 62 | const [ [ value ] ] = MockData.props.field.setValue.mock.calls; 63 | 64 | expect(value).toEqual('Hello, World!'); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tests/specs/lib/NativeElements.spec.ts: -------------------------------------------------------------------------------- 1 | import { NativeElements } from '@/lib/NativeElements'; 2 | 3 | const items: any = { 4 | array: 'ArrayElement', 5 | boolean: 'StateElement', 6 | string: 'InputElement', 7 | password: 'InputElement', 8 | file: 'InputElement', 9 | image: 'InputElement', 10 | radio: 'StateElement', 11 | checkbox: 'StateElement', 12 | enum: 'FieldsetElement', 13 | number: 'InputElement', 14 | integer: 'InputElement', 15 | object: 'FieldsetElement', 16 | list: 'ListElement', 17 | textarea: 'TextareaElement', 18 | message: 'MessageElement', 19 | button: 'ArrayButtonElement', 20 | helper: 'HelperElement' 21 | }; 22 | 23 | describe('lib/NativeElements', () => { 24 | Object.keys(NativeElements.$).forEach((kind: any) => { 25 | const name = items[kind]; 26 | const component: any = NativeElements.get(kind); 27 | 28 | it(`component for kind '${kind}' should be equal to '${name}'`, () => { 29 | expect(component.name).toBe(name); 30 | }); 31 | }); 32 | 33 | it('component for kind form should have native form element', () => { 34 | expect(NativeElements.get('form')).toBe('form'); 35 | }); 36 | 37 | it('getting an unknow kind should return the native input element', () => { 38 | expect(NativeElements.get('unknow' as any)).toBe('input'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/specs/lib/Objects.spec.ts: -------------------------------------------------------------------------------- 1 | import { Objects } from '@/lib/Objects'; 2 | 3 | describe('lib/Objects', () => { 4 | describe('Objects.isObject(value)', () => { 5 | [ 'hello', 123, true, undefined, null, [], () => { return 1; } ].forEach((value) => { 6 | it(`should return false for '${value}' as scalar value`, () => { 7 | expect(Objects.isObject(value)).toBeFalsy(); 8 | }); 9 | }); 10 | 11 | [ {} ].forEach((value) => { 12 | it(`should return true for '${JSON.stringify(value)}' as non scalar value`, () => { 13 | expect(Objects.isObject(value)).toBeTruthy(); 14 | }); 15 | }); 16 | }); 17 | 18 | describe('Objects.assign(dest, src)', () => { 19 | it('should successfully merge src object to the dest object', () => { 20 | const F = function F() { return 0; }; 21 | 22 | const src = { 23 | a: 1, c: 2, e: { x: 1 }, d: [ 1 ], f: { g: 1 }, F 24 | }; 25 | const dest = { 26 | a: 0, b: 1, e: { y: 2 }, d: [ 2, 3 ] 27 | }; 28 | const expected = { 29 | a: 1, b: 1, c: 2, e: { x: 1, y: 2 }, d: [ 1 ], f: { g: 1 }, F 30 | }; 31 | 32 | Objects.assign(dest, src); 33 | 34 | expect(dest).toEqual(expected); 35 | }); 36 | }); 37 | 38 | describe('Objects.clone(object)', () => { 39 | it('should successfully clone object', () => { 40 | const F = function F() { return 0; }; 41 | 42 | const src = { 43 | a: 1, c: 2, e: { x: 1 }, d: [ 1 ], f: { g: 1 }, F 44 | }; 45 | 46 | const expected = { 47 | a: 1, c: 2, e: { x: 1 }, d: [ 1 ], f: { g: 1 }, F 48 | }; 49 | 50 | const result = Objects.clone(src); 51 | 52 | expect(result).not.toBe(expected); 53 | expect(result).toEqual(expected); 54 | }); 55 | 56 | it('should successfully clone array', () => { 57 | const src = [ 58 | { x: 1 }, 59 | { y: 2 } 60 | ]; 61 | 62 | const expected = [ 63 | { x: 1 }, 64 | { y: 2 } 65 | ]; 66 | 67 | const result = Objects.clone(src); 68 | 69 | expect(result).not.toBe(expected); 70 | expect(result).toEqual(expected); 71 | }); 72 | 73 | it('should successfully clone scalar value', () => { 74 | expect(Objects.clone(12)).toBe(12); 75 | }); 76 | }); 77 | 78 | describe('Objects.isEmpty(object)', () => { 79 | it('should return true with an empty object', () => { 80 | expect(Objects.isEmpty({})).toBeTruthy(); 81 | }); 82 | 83 | it('should return false with a non empty object', () => { 84 | const F = function F() { return 0; }; 85 | const object = { 86 | a: 1, c: 2, e: { x: 1 }, d: [ 1 ], f: { g: 1 }, F 87 | }; 88 | 89 | expect(Objects.isEmpty(object)).toBe(false); 90 | }); 91 | }); 92 | 93 | describe('Objects.clear(object)', () => { 94 | it('should clear an empty object', () => { 95 | const object = {}; 96 | 97 | Objects.clear(object); 98 | 99 | expect(object).toEqual({}); 100 | }); 101 | 102 | it('should delete all properties', () => { 103 | const F = function F() { return 0; }; 104 | const object = { 105 | a: 1, c: 2, e: { x: 1 }, d: [ 1 ], f: { g: 1 }, F 106 | }; 107 | 108 | Objects.clear(object); 109 | 110 | expect(object).toEqual({}); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /tests/specs/lib/Pattern.spec.ts: -------------------------------------------------------------------------------- 1 | import { Pattern } from '@/lib/Pattern'; 2 | 3 | describe('lib/Pattern', () => { 4 | describe('Pattern.escape(str)', () => { 5 | it('should successfully escape a cleaned string', () => { 6 | expect(Pattern.escape('arya')).toBe('arya'); 7 | }); 8 | 9 | it('should successfully escape a ugly string', () => { 10 | const string = 'f(x) = ax + b; a = { 1, 2 }'; 11 | const expected = 'f\\(x\\) = ax \\+ b; a = \\{ 1, 2 \\}'; 12 | 13 | expect(Pattern.escape(string)).toBe(expected); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/specs/lib/Schema.spec.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '@/lib/Schema'; 2 | import { JsonSchema } from '../../../types/jsonschema'; 3 | 4 | describe('lib/Schema', () => { 5 | describe('Schema.isScalar(schema)', () => { 6 | [ 'boolean', 'integer', 'null', 'number', 'string' ].forEach((type) => { 7 | const schema = { type } as JsonSchema; 8 | 9 | it(`should validate { type: '${type}' } as scalar schema`, () => { 10 | expect(Schema.isScalar(schema)).toBeTruthy(); 11 | }); 12 | }); 13 | 14 | [ 'array', 'object' ].forEach((type) => { 15 | const schema = { type } as JsonSchema; 16 | 17 | it(`should validate { type: '${type}' } as non scalar schema`, () => { 18 | expect(Schema.isScalar(schema)).toBe(false); 19 | }); 20 | }); 21 | 22 | it('should validate empty schema {} as non scalar schema', () => { 23 | expect(Schema.isScalar({} as any)).toBe(false); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/specs/lib/UniqueId.spec.ts: -------------------------------------------------------------------------------- 1 | import { UniqueId } from '@/lib/UniqueId'; 2 | 3 | describe('lib/UniqueId', () => { 4 | describe('UniqueId.get(prefix = "", delimiter = "-")', () => { 5 | it('should successfully generate ID', () => { 6 | expect(UniqueId.get().length).toEqual(8); 7 | }); 8 | 9 | it('should successfully generate ID with a prefix', () => { 10 | expect(UniqueId.get('id').startsWith('id-')).toBeTruthy(); 11 | }); 12 | 13 | it('should successfully generate ID with a prefix and delimiter', () => { 14 | expect(UniqueId.get('id', '.').startsWith('id.')).toBeTruthy(); 15 | }); 16 | }); 17 | describe('UniqueId.parse(str)', () => { 18 | it('should successfully parse input string', () => { 19 | expect(UniqueId.parse('Hello World')).toBe('hello-world'); 20 | }); 21 | 22 | it('should successfully parse number input', () => { 23 | expect(UniqueId.parse(12 as any)).toBe('12'); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/specs/parsers/BooleanParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { BooleanParser } from '@/parsers/BooleanParser'; 2 | import { TestParser, Scope } from '../../lib/TestParser'; 3 | 4 | describe('parsers/BooleanParser', () => { 5 | TestParser.Case({ 6 | case: '0.0', 7 | given: { 8 | parser: new BooleanParser({ 9 | schema: { type: 'boolean' }, 10 | model: undefined 11 | }) 12 | }, 13 | expected: { 14 | parser: { 15 | field: { 16 | attrs: { 17 | type: ({ value }: Scope) => expect(value).toBe('checkbox'), 18 | checked: ({ value }: Scope) => expect(value).toBeFalsy() 19 | }, 20 | value: ({ value }: Scope) => expect(value).toBeFalsy() 21 | } 22 | } 23 | } 24 | }); 25 | 26 | it('should successfully parse default truthy boolean value', () => { 27 | const parser = new BooleanParser({ 28 | schema: { type: 'boolean' }, 29 | model: true 30 | }); 31 | 32 | parser.parse(); 33 | 34 | expect(parser.field.value).toBeTruthy(); 35 | }); 36 | 37 | it('field.value should successfully parse default falsy boolean value', () => { 38 | const parser = new BooleanParser({ 39 | schema: { type: 'boolean' }, 40 | model: false 41 | }); 42 | 43 | parser.parse(); 44 | 45 | expect(parser.field.value).toBeFalsy(); 46 | }); 47 | 48 | it('field.value should parse default non boolean value as a falsy model', () => { 49 | const parser = new BooleanParser({ 50 | schema: { type: 'boolean' }, 51 | model: 12 as any 52 | }); 53 | 54 | parser.parse(); 55 | 56 | expect(parser.field.value).toBeFalsy(); 57 | }); 58 | 59 | TestParser.Case({ 60 | case: '1.0', 61 | description: 'isEmpty() with non boolean', 62 | given: { 63 | parser: new BooleanParser({ 64 | schema: { type: 'boolean' } 65 | }) 66 | }, 67 | expected: { 68 | parser: { 69 | isEmpty: ({ parser }: Scope) => expect(parser.isEmpty('false')).toBeTruthy() 70 | } 71 | } 72 | }); 73 | 74 | TestParser.Case({ 75 | case: '1.1', 76 | description: 'isEmpty() with a falsy boolean', 77 | given: { 78 | parser: new BooleanParser({ 79 | schema: { type: 'boolean' } 80 | }) 81 | }, 82 | expected: { 83 | parser: { 84 | isEmpty: ({ parser }: Scope) => expect(parser.isEmpty(false)).toBeFalsy() 85 | } 86 | } 87 | }); 88 | 89 | TestParser.Case({ 90 | case: '1.2', 91 | description: 'isEmpty() with a truthy boolean', 92 | given: { 93 | parser: new BooleanParser({ 94 | schema: { type: 'boolean' } 95 | }) 96 | }, 97 | expected: { 98 | parser: { 99 | isEmpty: ({ parser }: Scope) => expect(parser.isEmpty(true)).toBeFalsy() 100 | } 101 | } 102 | }); 103 | 104 | TestParser.Case({ 105 | case: '1.3', 106 | description: 'isEmpty() with default value', 107 | given: { 108 | parser: new BooleanParser({ 109 | schema: { type: 'boolean', default: true } 110 | }) 111 | }, 112 | expected: { 113 | parser: { 114 | isEmpty: ({ parser }: Scope) => expect(parser.isEmpty()).toBeFalsy() 115 | } 116 | } 117 | }); 118 | 119 | TestParser.Case({ 120 | case: '1.4', 121 | description: 'isEmpty() with non boolean value', 122 | given: { 123 | parser: new BooleanParser({ 124 | schema: { type: 'boolean' } 125 | }) 126 | }, 127 | expected: { 128 | parser: { 129 | isEmpty: ({ parser }: Scope) => expect(parser.isEmpty(null)).toBeTruthy() 130 | } 131 | } 132 | }); 133 | 134 | TestParser.Case({ 135 | case: '2.0', 136 | description: 'parser.reset()', 137 | given: { 138 | parser: new BooleanParser({ 139 | schema: { type: 'boolean' }, 140 | model: true, 141 | onChange: jest.fn() 142 | }) 143 | }, 144 | expected: { 145 | parser: { 146 | reset({ parser }: Scope) { 147 | expect(parser.rawValue).toBe(true); 148 | expect(parser.model).toBe(true); 149 | 150 | parser.field.setValue(false); 151 | 152 | expect(parser.rawValue).toBe(false); 153 | expect(parser.model).toBe(false); 154 | 155 | parser.reset(); // reset without calling onChange() 156 | 157 | expect(parser.rawValue).toBe(true); 158 | expect(parser.model).toBe(true); 159 | 160 | parser.field.reset(); // reset with calling onChange() 161 | 162 | const onChange = parser.options.onChange; 163 | const result = onChange.mock.calls.map(([ value ]: any) => value); 164 | 165 | expect(result).toEqual([ true, false, true ]); 166 | } 167 | } 168 | } 169 | }); 170 | 171 | TestParser.Case({ 172 | case: '3.0', 173 | description: 'parser.clear()', 174 | given: { 175 | parser: new BooleanParser({ 176 | schema: { type: 'boolean' }, 177 | model: false, 178 | onChange: jest.fn() 179 | }) 180 | }, 181 | expected: { 182 | parser: { 183 | clear({ parser }: Scope) { 184 | expect(parser.rawValue).toBe(false); 185 | expect(parser.model).toBe(false); 186 | 187 | parser.field.setValue(true); 188 | 189 | expect(parser.rawValue).toBe(true); 190 | expect(parser.model).toBe(true); 191 | 192 | parser.clear(); // clear without calling onChange() 193 | 194 | expect(parser.rawValue).toBeFalsy(); 195 | expect(parser.model).toBeFalsy(); 196 | 197 | parser.field.clear(); // clear with calling onChange() 198 | 199 | const onChange = parser.options.onChange; 200 | const result = onChange.mock.calls.map(([ value ]: any) => value); 201 | 202 | expect(result).toEqual([ false, true, false ]); 203 | } 204 | } 205 | } 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /tests/specs/parsers/EnumParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { EnumParser } from '@/parsers/EnumParser'; 2 | import { TestParser, Scope } from '../../lib/TestParser'; 3 | 4 | import '@/parsers'; 5 | 6 | describe('parsers/EnumParser', () => { 7 | TestParser.Case({ 8 | case: '1.0', 9 | description: 'parser.reset()', 10 | given: { 11 | parser: new EnumParser({ 12 | schema: { 13 | type: 'string', 14 | enum: [ 'jon', 'arya', 'bran', 'ned' ] 15 | }, 16 | model: 'jon' 17 | }) 18 | }, 19 | expected: { 20 | parser: { 21 | kind: ({ value }: Scope) => expect(value).toBe('enum'), 22 | field: { 23 | value: 'jon', 24 | fields({ parser }: Scope) { 25 | // fields should be defined 26 | const models = Object.keys(parser.field.fields); 27 | 28 | expect(models).toEqual([ 'jon', 'arya', 'bran', 'ned' ]); 29 | 30 | // fields's field.attrs.checked should be defined 31 | const checkStates = models.map((key) => parser.field.fields[key].attrs.checked); 32 | 33 | expect(checkStates).toEqual([ true, false, false, false ]); 34 | }, 35 | setValue({ parser }: Scope) { 36 | // field.value should be equal to the updated value using field.setValue() 37 | parser.field.setValue('arya'); 38 | expect(parser.field.value).toBe('arya'); 39 | 40 | // field.attrs.checked should be updated when using field.setValue() 41 | parser.field.setValue('bran'); 42 | 43 | const models = Object.keys(parser.field.fields); 44 | const checkStates = models.map((key) => parser.field.fields[key].attrs.checked); 45 | 46 | expect(checkStates).toEqual([ false, false, true, false ]); 47 | 48 | // field.value should be updated when a child is checked 49 | const childField: any = parser.field.fields[models.slice(-1).pop() as any]; 50 | 51 | childField.setValue(childField.value); 52 | expect(parser.field.value).toBe('ned'); 53 | } 54 | } 55 | } 56 | } 57 | }); 58 | 59 | TestParser.Case({ 60 | case: '2.0', 61 | description: 'should successfully parse default value', 62 | given: { 63 | parser: new EnumParser({ 64 | schema: { 65 | type: 'string', 66 | enum: [ 'jon', 'arya', 'tyrion' ], 67 | default: 'arya' 68 | }, 69 | model: undefined 70 | }) 71 | }, 72 | expected: { 73 | parser: { 74 | model: ({ value }: Scope) => expect(value).toBe('arya'), 75 | field: { 76 | value: ({ value }: Scope) => expect(value).toBe('arya') 77 | } 78 | } 79 | } 80 | }); 81 | 82 | TestParser.Case({ 83 | case: '2.1', 84 | description: 'field.value should parse default undefined as an undefined model', 85 | given: { 86 | parser: new EnumParser({ 87 | schema: { type: 'string', enum: [ 'jon', 'arya' ] }, 88 | model: undefined 89 | }) 90 | }, 91 | expected: { 92 | parser: { 93 | field: { 94 | value: ({ value }: Scope) => expect(value).toBeUndefined() 95 | } 96 | } 97 | } 98 | }); 99 | 100 | TestParser.Case({ 101 | case: '2.2', 102 | description: 'field.fields should be empty with missing schema.enum', 103 | given: { 104 | parser: new EnumParser({ 105 | schema: { type: 'string' }, 106 | model: undefined 107 | }) 108 | }, 109 | expected: { 110 | parser: { 111 | field: { 112 | fields: ({ value }: Scope) => expect(value).toEqual({}) 113 | } 114 | } 115 | } 116 | }); 117 | 118 | TestParser.Case({ 119 | case: '2.3', 120 | description: 'field.fields should be defined with provided field.descriptor.items', 121 | given: { 122 | parser: new EnumParser({ 123 | schema: { 124 | type: 'string', 125 | enum: [ 'jon', 'arya' ] 126 | }, 127 | model: 'jon', 128 | descriptor: { 129 | items: { 130 | jon: { 131 | label: 'Jon Snow' 132 | }, 133 | arya: { 134 | label: 'Arya Stark' 135 | } 136 | } 137 | } 138 | }) 139 | }, 140 | expected: { 141 | parser: { 142 | fields: { 143 | jon: { 144 | value: ({ value }: Scope) => expect(value).toBe('jon') 145 | }, 146 | arya: { 147 | value: ({ value }: Scope) => expect(value).toBe('arya') 148 | } 149 | }, 150 | field: { 151 | fields: { 152 | jon: { 153 | value: ({ value }: Scope) => expect(value).toBe('jon') 154 | }, 155 | arya: { 156 | value: ({ value }: Scope) => expect(value).toBe('arya') 157 | } 158 | }, 159 | descriptor: { 160 | children: [ 161 | { 162 | label: ({ value }: Scope) => expect(value).toBe('Jon Snow') 163 | }, 164 | { 165 | label: ({ value }: Scope) => expect(value).toBe('Arya Stark') 166 | } 167 | ] 168 | } 169 | } 170 | } 171 | } 172 | }); 173 | 174 | TestParser.Case({ 175 | case: '3.0', 176 | description: 'parser.reset()', 177 | given: { 178 | parser: new EnumParser({ 179 | schema: { 180 | type: 'string', 181 | enum: [ 'jon', 'arya', 'bran', 'ned' ] 182 | }, 183 | model: 'arya', 184 | onChange: jest.fn() 185 | }) 186 | }, 187 | expected: { 188 | parser: { 189 | reset({ parser }: Scope) { 190 | const onChange = parser.options.onChange; 191 | const expected = [ 192 | 'arya', 193 | 'jon' 194 | ]; 195 | 196 | expect(parser.rawValue).toEqual('arya'); 197 | expect(parser.model).toEqual('arya'); 198 | expect(onChange.mock.calls.length).toBe(1); 199 | expect(onChange.mock.calls[0][0]).toEqual(expected[0]); 200 | 201 | parser.field.fields.jon.setValue(true); 202 | 203 | expect(onChange.mock.calls.length).toBe(2); 204 | expect(onChange.mock.calls[1][0]).toEqual(expected[1]); 205 | expect(parser.rawValue).toEqual('jon'); 206 | expect(parser.model).toEqual('jon'); 207 | 208 | parser.reset(); // reset without calling onChange 209 | 210 | expect(onChange.mock.calls.length).toBe(2); 211 | expect(parser.initialValue).toEqual('arya'); 212 | expect(parser.rawValue).toEqual('arya'); 213 | expect(parser.model).toEqual('arya'); 214 | 215 | parser.field.reset(); // reset with calling onChange 216 | 217 | expect(onChange.mock.calls.length).toBe(3); 218 | expect(onChange.mock.calls[2][0]).toEqual(expected[0]); 219 | } 220 | } 221 | } 222 | }); 223 | 224 | TestParser.Case({ 225 | case: '4.0', 226 | description: 'parser.clear()', 227 | given: { 228 | parser: new EnumParser({ 229 | schema: { 230 | type: 'string', 231 | enum: [ 'jon', 'arya', 'bran', 'ned' ] 232 | }, 233 | model: 'arya', 234 | onChange: jest.fn() 235 | }) 236 | }, 237 | expected: { 238 | parser: { 239 | clear({ parser }: Scope) { 240 | const onChange = parser.options.onChange; 241 | 242 | expect(parser.rawValue).toEqual('arya'); 243 | expect(parser.model).toEqual('arya'); 244 | expect(onChange.mock.calls.length).toBe(1); 245 | expect(onChange.mock.calls[0][0]).toEqual('arya'); 246 | 247 | parser.field.fields.jon.setValue(true); 248 | 249 | expect(onChange.mock.calls.length).toBe(2); 250 | expect(onChange.mock.calls[1][0]).toEqual('jon'); 251 | expect(parser.rawValue).toEqual('jon'); 252 | expect(parser.model).toEqual('jon'); 253 | 254 | parser.clear(); // clear without calling onChange 255 | 256 | expect(onChange.mock.calls.length).toBe(2); 257 | expect(parser.rawValue).toEqual(undefined); 258 | expect(parser.model).toEqual(undefined); 259 | 260 | parser.field.clear(); // clear with calling onChange 261 | 262 | expect(onChange.mock.calls.length).toBe(3); 263 | expect(onChange.mock.calls[2][0]).toEqual(undefined); 264 | } 265 | } 266 | } 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /tests/specs/parsers/IntegerParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { IntegerParser } from '@/parsers/IntegerParser'; 2 | import { TestParser, Scope } from '../../lib/TestParser'; 3 | 4 | describe('parsers/IntegerParser', () => { 5 | TestParser.Case({ 6 | case: '0.0', 7 | given: { 8 | parser: new IntegerParser({ 9 | schema: { 10 | type: 'integer', 11 | minimum: 0, 12 | maximum: 10, 13 | multipleOf: 2 14 | }, 15 | model: 2 16 | }) 17 | }, 18 | expected: { 19 | parser: { 20 | kind: ({ value }: Scope) => expect(value).toBe('integer'), 21 | field: { 22 | attrs: { 23 | type: ({ value }: Scope) => expect(value).toBe('number'), 24 | min: ({ value, options }: Scope) => expect(value).toBe(options.schema.minimum), 25 | max: ({ value, options }: Scope) => expect(value).toBe(options.schema.maximum), 26 | value: ({ value, options }: Scope) => expect(value).toBe(`${options.model}`) 27 | }, 28 | value: ({ value, options }: Scope) => expect(value).toBe(options.model) 29 | } 30 | } 31 | } 32 | }); 33 | 34 | it('should successfully parse default integer value', () => { 35 | const parser = new IntegerParser({ 36 | schema: { type: 'integer' }, 37 | model: 3 38 | }); 39 | 40 | parser.parse(); 41 | 42 | expect(parser.field.value).toBe(3); 43 | }); 44 | 45 | it('field.value should parse default non integer value as an undefined model', () => { 46 | const parser = new IntegerParser({ 47 | schema: { type: 'integer' }, 48 | model: undefined 49 | }); 50 | 51 | parser.parse(); 52 | 53 | expect(parser.field.value).toBeUndefined(); 54 | }); 55 | 56 | describe('exclusiveMinimum/exclusiveMaximum', () => { 57 | const parser = new IntegerParser({ 58 | schema: { 59 | type: 'integer', 60 | exclusiveMinimum: 0, 61 | exclusiveMaximum: 10 62 | }, 63 | model: 0 64 | }); 65 | 66 | parser.parse(); 67 | 68 | it('field.attrs.min should equal define using schema.exclusiveMinimum', () => { 69 | expect(parser.field.attrs.min).toBe(1); 70 | }); 71 | 72 | it('field.attrs.max should equal define using schema.exclusiveMaximum', () => { 73 | expect(parser.field.attrs.max).toBe(9); 74 | }); 75 | }); 76 | 77 | TestParser.Case({ 78 | case: '1.0', 79 | description: 'parser.reset()', 80 | given: { 81 | parser: new IntegerParser({ 82 | schema: { 83 | type: 'integer', 84 | minimum: 0, 85 | maximum: 10, 86 | multipleOf: 2 87 | }, 88 | model: 2, 89 | onChange: jest.fn() 90 | }) 91 | }, 92 | expected: { 93 | parser: { 94 | reset({ parser }: Scope) { 95 | expect(parser.rawValue).toBe(2); 96 | expect(parser.model).toBe(2); 97 | 98 | parser.field.setValue(1); 99 | 100 | expect(parser.rawValue).toBe(1); 101 | expect(parser.model).toBe(1); 102 | 103 | parser.reset(); // reset without calling onChange 104 | 105 | expect(parser.rawValue).toBe(2); 106 | expect(parser.model).toBe(2); 107 | 108 | parser.field.reset(); // reset with calling onChange 109 | 110 | const onChange = parser.options.onChange; 111 | const result = onChange.mock.calls.map(([ value ]: any) => value); 112 | 113 | expect(result).toEqual([ 2, 1, 2 ]); 114 | } 115 | } 116 | } 117 | }); 118 | 119 | TestParser.Case({ 120 | case: '2.0', 121 | description: 'parser.clear()', 122 | given: { 123 | parser: new IntegerParser({ 124 | schema: { 125 | type: 'integer', 126 | minimum: 0, 127 | maximum: 10, 128 | multipleOf: 2 129 | }, 130 | model: 2, 131 | onChange: jest.fn() 132 | }) 133 | }, 134 | expected: { 135 | parser: { 136 | clear({ parser }: Scope) { 137 | expect(parser.rawValue).toBe(2); 138 | expect(parser.model).toBe(2); 139 | 140 | parser.field.setValue(1); 141 | 142 | expect(parser.rawValue).toBe(1); 143 | expect(parser.model).toBe(1); 144 | 145 | parser.clear(); // clear without calling onChange 146 | 147 | expect(parser.rawValue).toBeUndefined(); 148 | expect(parser.model).toBeUndefined(); 149 | 150 | parser.field.clear(); // clear with calling onChange 151 | 152 | const onChange = parser.options.onChange; 153 | const result = onChange.mock.calls.map(([ value ]: any) => value); 154 | 155 | expect(result).toEqual([ 2, 1, undefined ]); 156 | } 157 | } 158 | } 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /tests/specs/parsers/ListParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { ListParser } from '@/parsers/ListParser'; 2 | import { Options } from '../../lib/Options'; 3 | import { TestParser, Scope } from '../../lib/TestParser'; 4 | 5 | describe('parsers/ListParser', () => { 6 | TestParser.Case({ 7 | case: '1.0', 8 | description: 'with a string schema with default descriptor', 9 | given: { 10 | parser: new ListParser({ 11 | schema: { 12 | type: 'string', 13 | enum: [ 'jon', 'arya' ] 14 | }, 15 | model: 'arya' 16 | }) 17 | }, 18 | expected: { 19 | parser: { 20 | kind: ({ value }: Scope) => expect(value).toBe('list'), 21 | items: ({ value }: Scope) => expect(value).toEqual([ 22 | { 23 | value: 'jon', 24 | selected: false 25 | }, 26 | { 27 | value: 'arya', 28 | selected: true 29 | } 30 | ]), 31 | field: { 32 | value: ({ value }: Scope) => expect(value).toBe('arya'), 33 | items: ({ value, parser }: Scope) => expect(value).toEqual(parser.items), 34 | descriptor: { 35 | kind: ({ value }: Scope) => expect(value).toBe('list'), 36 | options: ({ value }: Scope) => expect(value).toEqual([ 37 | { 38 | value: '', 39 | selected: false, 40 | label: undefined 41 | }, 42 | { 43 | value: 'jon', 44 | selected: false, 45 | label: 'jon' 46 | }, 47 | { 48 | value: 'arya', 49 | selected: true, 50 | label: 'arya' 51 | } 52 | ]) 53 | } 54 | } 55 | } 56 | } 57 | }); 58 | 59 | TestParser.Case({ 60 | case: '1.1', 61 | description: 'with a string schema with custom descriptor kind', 62 | given: Options.get({ 63 | schema: { 64 | type: 'string', 65 | enum: [ 'jon', 'arya' ] 66 | }, 67 | model: 'arya', 68 | descriptor: { 69 | kind: 'enum' 70 | } 71 | }), 72 | expected: { 73 | parser: { 74 | field: { 75 | descriptor: { 76 | kind: ({ value }: Scope) => expect(value).toBe('enum') 77 | } 78 | } 79 | } 80 | } 81 | }); 82 | 83 | TestParser.Case({ 84 | case: '1.2', 85 | description: 'with a boolean schema', 86 | given: { 87 | parser: new ListParser({ 88 | schema: { 89 | type: 'boolean', 90 | enum: [ true, false ] 91 | }, 92 | model: true 93 | }) 94 | }, 95 | expected: { 96 | parser: { 97 | items: ({ value }: Scope) => expect(value).toEqual([ 98 | { 99 | value: 'true', 100 | selected: true 101 | }, 102 | { 103 | value: 'false', 104 | selected: false 105 | } 106 | ]), 107 | field: { 108 | value: ({ value }: Scope) => expect(value).toBe(true), 109 | descriptor: { 110 | options: ({ value }: Scope) => expect(value).toEqual([ 111 | { 112 | value: '', 113 | selected: false, 114 | label: undefined 115 | }, 116 | { 117 | value: 'true', 118 | selected: true, 119 | label: 'true' 120 | }, 121 | { 122 | value: 'false', 123 | selected: false, 124 | label: 'false' 125 | } 126 | ]) 127 | } 128 | } 129 | } 130 | } 131 | }); 132 | 133 | TestParser.Case({ 134 | case: '1.3', 135 | description: 'with a null schema', 136 | given: { 137 | parser: new ListParser({ 138 | schema: { 139 | type: 'null', 140 | enum: [ null, null ] 141 | }, 142 | model: null 143 | }) 144 | }, 145 | expected: { 146 | parser: { 147 | items: ({ value }: Scope) => expect(value).toEqual([ 148 | { 149 | value: 'null', 150 | selected: true 151 | }, 152 | { 153 | value: 'null', 154 | selected: true 155 | } 156 | ]), 157 | field: { 158 | value: ({ value }: Scope) => expect(value).toBe(null), 159 | descriptor: { 160 | options: ({ value }: Scope) => expect(value).toEqual([ 161 | { 162 | value: '', 163 | selected: false, 164 | label: undefined 165 | }, 166 | { 167 | value: 'null', 168 | selected: true, 169 | label: 'null' 170 | }, 171 | { 172 | value: 'null', 173 | selected: true, 174 | label: 'null' 175 | } 176 | ]) 177 | } 178 | } 179 | } 180 | } 181 | }); 182 | 183 | TestParser.Case({ 184 | case: '1.4', 185 | description: 'with defined descriptor', 186 | given: { 187 | parser: new ListParser({ 188 | schema: { 189 | type: 'string', 190 | enum: [ 'jon', 'arya' ] 191 | }, 192 | model: undefined, 193 | descriptor: { 194 | kind: 'list', 195 | items: { 196 | jon: { label: 'Jon Snow' }, 197 | arya: { label: 'Arya Stark' } 198 | } 199 | } 200 | }) 201 | }, 202 | expected: { 203 | parser: { 204 | items: ({ value }: Scope) => expect(value).toEqual([ 205 | { 206 | value: 'jon', 207 | selected: false 208 | }, 209 | { 210 | value: 'arya', 211 | selected: false 212 | } 213 | ]), 214 | field: { 215 | value: ({ value }: Scope) => expect(value).toBe(undefined), 216 | descriptor: { 217 | options: ({ value }: Scope) => expect(value).toEqual([ 218 | { 219 | value: '', 220 | selected: false, 221 | label: undefined 222 | }, 223 | { 224 | value: 'jon', 225 | selected: false, 226 | label: 'Jon Snow' 227 | }, 228 | { 229 | value: 'arya', 230 | selected: false, 231 | label: 'Arya Stark' 232 | } 233 | ]) 234 | } 235 | } 236 | } 237 | } 238 | }); 239 | 240 | TestParser.Case({ 241 | case: '1.5', 242 | description: 'field.items should be empty with missing schema.enum', 243 | given: { 244 | parser: new ListParser({ 245 | schema: { type: 'string' }, 246 | model: undefined 247 | }) 248 | }, 249 | expected: { 250 | parser: { 251 | items: ({ value }: Scope) => expect(value).toEqual([]) 252 | } 253 | } 254 | }); 255 | 256 | TestParser.Case({ 257 | case: '2.0', 258 | description: 'parseValue(data) with truthy boolean', 259 | given: { 260 | parser: new ListParser({ 261 | schema: { type: 'boolean' }, 262 | model: 'true' 263 | }) 264 | }, 265 | expected: { 266 | parser: { 267 | model: ({ value }: Scope) => expect(value).toBe(true) 268 | } 269 | } 270 | }); 271 | 272 | TestParser.Case({ 273 | case: '2.1', 274 | description: 'parseValue(data) with falsy boolean', 275 | given: { 276 | parser: new ListParser({ 277 | schema: { type: 'boolean' }, 278 | model: 'false' 279 | }) 280 | }, 281 | expected: { 282 | parser: { 283 | model: ({ value }: Scope) => expect(value).toBe(false) 284 | } 285 | } 286 | }); 287 | 288 | TestParser.Case({ 289 | case: '2.2', 290 | description: 'parseValue(data) with invalid boolean', 291 | given: { 292 | parser: new ListParser({ 293 | schema: { type: 'boolean' }, 294 | model: 'invalid boolean value' 295 | }) 296 | }, 297 | expected: { 298 | parser: { 299 | model: ({ value }: Scope) => expect(value).toBe(false) 300 | } 301 | } 302 | }); 303 | 304 | TestParser.Case({ 305 | case: '2.3', 306 | description: 'parseValue(data) with integer', 307 | given: { 308 | parser: new ListParser({ 309 | schema: { type: 'integer' }, 310 | model: '12' 311 | }) 312 | }, 313 | expected: { 314 | parser: { 315 | model: ({ value }: Scope) => expect(value).toBe(12) 316 | } 317 | } 318 | }); 319 | 320 | TestParser.Case({ 321 | case: '2.4', 322 | description: 'parseValue(data) with number', 323 | given: { 324 | parser: new ListParser({ 325 | schema: { type: 'number' }, 326 | model: '12.2' 327 | }) 328 | }, 329 | expected: { 330 | parser: { 331 | model: ({ value }: Scope) => expect(value).toBe(12.2) 332 | } 333 | } 334 | }); 335 | 336 | TestParser.Case({ 337 | case: '2.5', 338 | description: 'parseValue(data) with unknown schema type', 339 | given: { 340 | parser: new ListParser({ 341 | schema: { type: 'unknown' } as any, 342 | model: '12.5' 343 | }) 344 | }, 345 | expected: { 346 | parser: { 347 | model: ({ value }: Scope) => expect(value).toBe('12.5') 348 | } 349 | } 350 | }); 351 | 352 | TestParser.Case({ 353 | case: '3.0', 354 | description: 'should successfully parse default value', 355 | given: { 356 | parser: new ListParser({ 357 | schema: { 358 | type: 'string', 359 | enum: [ 'jon', 'arya' ], 360 | default: 'jon' 361 | }, 362 | model: undefined 363 | }) 364 | }, 365 | expected: { 366 | parser: { 367 | model: ({ value }: Scope) => expect(value).toBe('jon') 368 | } 369 | } 370 | }); 371 | 372 | TestParser.Case({ 373 | case: '3.1', 374 | description: 'field.value should parse default undefined as an undefined model', 375 | given: { 376 | parser: new ListParser({ 377 | schema: { type: 'string', enum: [ 'jon', 'arya' ] }, 378 | model: undefined 379 | }) 380 | }, 381 | expected: { 382 | parser: { 383 | field: { 384 | value: ({ value }: Scope) => expect(value).toBeUndefined() 385 | } 386 | } 387 | } 388 | }); 389 | }); 390 | -------------------------------------------------------------------------------- /tests/specs/parsers/NullParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { NullParser } from '@/parsers/NullParser'; 2 | import { TestParser, Scope } from '../../lib/TestParser'; 3 | 4 | describe('parsers/NullParser', () => { 5 | TestParser.Case({ 6 | case: '0.0', 7 | given: { 8 | parser: new NullParser({ 9 | schema: { type: 'null' }, 10 | model: undefined 11 | }) 12 | }, 13 | expected: { 14 | parser: { 15 | field: { 16 | attrs: { 17 | type: ({ value }: Scope) => expect(value).toBe('hidden'), 18 | value: ({ value }: Scope) => expect(value).toBe('\u0000') 19 | }, 20 | value: ({ value }: Scope) => expect(value).toBeNull() 21 | } 22 | } 23 | } 24 | }); 25 | 26 | TestParser.Case({ 27 | case: '1.0', 28 | description: 'parser.reset()', 29 | given: { 30 | parser: new NullParser({ 31 | schema: { type: 'null' }, 32 | model: null, 33 | onChange: jest.fn() 34 | }) 35 | }, 36 | expected: { 37 | parser: { 38 | reset({ parser }: Scope) { 39 | expect(parser.rawValue).toBe(null); 40 | expect(parser.model).toBe(null); 41 | 42 | parser.reset(); // reset without calling onChange 43 | 44 | expect(parser.rawValue).toBe(null); 45 | expect(parser.model).toBe(null); 46 | 47 | parser.field.reset(); // reset with calling onChange 48 | 49 | const onChange = parser.options.onChange; 50 | const result = onChange.mock.calls.map(([ value ]: any) => value); 51 | 52 | expect(result).toEqual([ null, null ]); 53 | } 54 | } 55 | } 56 | }); 57 | 58 | TestParser.Case({ 59 | case: '2.0', 60 | description: 'parser.clear()', 61 | given: { 62 | parser: new NullParser({ 63 | schema: { type: 'null' }, 64 | model: null, 65 | onChange: jest.fn() 66 | }) 67 | }, 68 | expected: { 69 | parser: { 70 | clear({ parser }: Scope) { 71 | expect(parser.rawValue).toBe(null); 72 | expect(parser.model).toBe(null); 73 | 74 | parser.clear(); // clear without calling onChange 75 | 76 | expect(parser.rawValue).toBe(null); 77 | expect(parser.model).toBe(null); 78 | 79 | parser.field.clear(); // clear with calling onChange 80 | 81 | const onChange = parser.options.onChange; 82 | const result = onChange.mock.calls.map(([ value ]: any) => value); 83 | 84 | expect(result).toEqual([ null, null ]); 85 | } 86 | } 87 | } 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/specs/parsers/NumberParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { NumberParser } from '@/parsers/NumberParser'; 2 | import { TestParser, Scope } from '../../lib/TestParser'; 3 | 4 | describe('parsers/NumberParser', () => { 5 | TestParser.Case({ 6 | case: '0.0', 7 | description: 'parser.reset()', 8 | given: { 9 | parser: new NumberParser({ 10 | schema: { 11 | type: 'number', 12 | minimum: 0, 13 | maximum: 10, 14 | multipleOf: 2 15 | }, 16 | model: 2.0 17 | }) 18 | }, 19 | expected: { 20 | parser: { 21 | kind: ({ value }: Scope) => expect(value).toBe('number'), 22 | field: { 23 | attrs: { 24 | type: ({ value }: Scope) => expect(value).toBe('number'), 25 | min: ({ value, options }: Scope) => expect(value).toBe(options.schema.minimum), 26 | max: ({ value, options }: Scope) => expect(value).toBe(options.schema.maximum), 27 | step: ({ value, options }: Scope) => expect(value).toBe(options.schema.multipleOf), 28 | value: ({ value, options }: Scope) => expect(value).toBe(`${options.model}`) 29 | }, 30 | value: ({ value, options }: Scope) => expect(value).toBe(options.model) 31 | } 32 | } 33 | } 34 | }); 35 | 36 | it('should successfully parse default number value', () => { 37 | const parser = new NumberParser({ 38 | schema: { type: 'number' }, 39 | model: 3.1 40 | }); 41 | 42 | parser.parse(); 43 | 44 | expect(parser.field.value).toBe(3.1); 45 | }); 46 | 47 | it('field.value should parse default non number value as an undefined model', () => { 48 | const parser = new NumberParser({ 49 | schema: { type: 'number' }, 50 | model: undefined 51 | }); 52 | 53 | parser.parse(); 54 | 55 | expect(parser.field.value).toBeUndefined(); 56 | }); 57 | 58 | describe('exclusiveMinimum/exclusiveMaximum', () => { 59 | const parser = new NumberParser({ 60 | schema: { 61 | type: 'number', 62 | exclusiveMinimum: 0, 63 | exclusiveMaximum: 10 64 | }, 65 | model: 0 66 | }); 67 | 68 | parser.parse(); 69 | 70 | it('field.attrs.min should equal define using schema.exclusiveMinimum', () => { 71 | expect(parser.field.attrs.min).toBe(0.1); 72 | }); 73 | 74 | it('field.attrs.max should equal define using schema.exclusiveMaximum', () => { 75 | expect(parser.field.attrs.max).toBe(9.9); 76 | }); 77 | }); 78 | 79 | TestParser.Case({ 80 | case: '1.0', 81 | description: 'parser.reset()', 82 | given: { 83 | parser: new NumberParser({ 84 | schema: { 85 | type: 'number', 86 | minimum: 0, 87 | maximum: 10, 88 | multipleOf: 2 89 | }, 90 | model: 2.1, 91 | onChange: jest.fn() 92 | }) 93 | }, 94 | expected: { 95 | parser: { 96 | reset({ parser }: Scope) { 97 | expect(parser.rawValue).toBe(2.1); 98 | expect(parser.model).toBe(2.1); 99 | 100 | parser.field.setValue(1.1); 101 | 102 | expect(parser.rawValue).toBe(1.1); 103 | expect(parser.model).toBe(1.1); 104 | 105 | parser.reset(); // reset without calling onChange 106 | 107 | expect(parser.rawValue).toBe(2.1); 108 | expect(parser.model).toBe(2.1); 109 | 110 | parser.field.reset(); // reset with calling onChange 111 | 112 | const onChange = parser.options.onChange; 113 | const result = onChange.mock.calls.map(([ value ]: any) => value); 114 | 115 | expect(result).toEqual([ 2.1, 1.1, 2.1 ]); 116 | } 117 | } 118 | } 119 | }); 120 | 121 | TestParser.Case({ 122 | case: '2.0', 123 | description: 'parser.clear()', 124 | given: { 125 | parser: new NumberParser({ 126 | schema: { 127 | type: 'number', 128 | minimum: 0, 129 | maximum: 10, 130 | multipleOf: 2 131 | }, 132 | model: 2.1, 133 | onChange: jest.fn() 134 | }) 135 | }, 136 | expected: { 137 | parser: { 138 | clear({ parser }: Scope) { 139 | expect(parser.rawValue).toBe(2.1); 140 | expect(parser.model).toBe(2.1); 141 | 142 | parser.field.setValue(1.1); 143 | 144 | expect(parser.rawValue).toBe(1.1); 145 | expect(parser.model).toBe(1.1); 146 | 147 | parser.clear(); // clear without calling onChange 148 | 149 | expect(parser.rawValue).toBeUndefined(); 150 | expect(parser.model).toBeUndefined(); 151 | 152 | parser.field.clear(); // clear with calling onChange 153 | 154 | const onChange = parser.options.onChange; 155 | const result = onChange.mock.calls.map(([ value ]: any) => value); 156 | 157 | expect(result).toEqual([ 2.1, 1.1, undefined ]); 158 | } 159 | } 160 | } 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /tests/specs/parsers/ScalarParser.spec.ts: -------------------------------------------------------------------------------- 1 | import { Options } from '../../lib/Options'; 2 | import { TestParser, Scope } from '../../lib/TestParser'; 3 | 4 | describe('parsers/ScalarParser', () => { 5 | TestParser.Case({ 6 | case: '1.0: ScalarParser.getKind()', 7 | description: 'should return kind `radio` with schema.enum', 8 | given: Options.get({ 9 | schema: { 10 | type: 'string', 11 | enum: [ 'arya' ] 12 | }, 13 | model: undefined as any 14 | }), 15 | expected: { 16 | parser: { 17 | kind: ({ value }: Scope) => expect(value).toBe('enum'), 18 | field: { 19 | kind: ({ value }: Scope) => expect(value).toBe('enum'), 20 | fields: { 21 | arya: { 22 | kind: ({ value }: Scope) => expect(value).toBe('radio') 23 | } 24 | }, 25 | descriptor: { 26 | kind({ value, parser: { field } }: Scope) { 27 | expect(value).toBe(field.kind); 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }); 34 | 35 | TestParser.Case({ 36 | case: '1.2: ScalarParser.getKind()', 37 | description: 'should convert schema.const to field.attrs.pattern', 38 | given: Options.get({ 39 | schema: { type: 'string', const: 'hello?' }, 40 | model: undefined as any 41 | }), 42 | expected: { 43 | parser: { 44 | kind: ({ value }: Scope) => expect(value).toBe('string'), 45 | field: { 46 | kind: ({ value }: Scope) => expect(value).toBe('string'), 47 | attrs: { 48 | type: ({ value }: Scope) => expect(value).toBe('text'), 49 | pattern: ({ value }: Scope) => expect(value).toBe('hello\\?') 50 | }, 51 | descriptor: { 52 | kind({ value, parser: { field } }: Scope) { 53 | expect(value).toBe(field.kind); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }); 60 | 61 | TestParser.Case({ 62 | case: '1.3: ScalarParser.getKind()', 63 | description: 'should return kind `null` for unknow kind', 64 | given: Options.get({ 65 | schema: { type: 'string' }, 66 | model: undefined as any 67 | }), 68 | expected: { 69 | parser: { 70 | kind: ({ value }: Scope) => expect(value).toBe('string'), 71 | field: { 72 | kind: ({ value }: Scope) => expect(value).toBe('string'), 73 | descriptor: { 74 | kind({ value, parser: { field } }: Scope) { 75 | expect(value).toBe(field.kind); 76 | } 77 | } 78 | } 79 | } 80 | } 81 | }); 82 | 83 | TestParser.Case({ 84 | case: '2.0: ScalarParser.getType()', 85 | description: 'should return type `radio` with schema.enum', 86 | given: Options.get({ 87 | schema: { 88 | type: 'string', 89 | enum: [ 'arya' ] 90 | }, 91 | model: undefined as any 92 | }), 93 | expected: { 94 | parser: { 95 | field: { 96 | fields: { 97 | arya: { 98 | attrs: { 99 | type: ({ value }: Scope) => expect(value).toBe('radio') 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | }); 107 | 108 | TestParser.Case({ 109 | case: '2.1: ScalarParser.getType()', 110 | description: 'should return type `null` with schema.enum', 111 | given: Options.get({ 112 | schema: { type: 'string' }, 113 | model: undefined as any 114 | }), 115 | expected: { 116 | parser: { 117 | field: { 118 | attrs: { 119 | type: ({ value }: Scope) => expect(value).toBe('text') 120 | } 121 | } 122 | } 123 | } 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "es5", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "experimentalDecorators": false, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "sourceMap": true, 14 | "baseUrl": ".", 15 | "types": [ 16 | "@types/jest", 17 | "./types" 18 | ], 19 | "paths": { 20 | "@/*": [ 21 | "src/*" 22 | ] 23 | }, 24 | "lib": [ 25 | "esnext", 26 | "dom", 27 | "dom.iterable", 28 | "scripthost" 29 | ] 30 | }, 31 | "include": [ 32 | "src/**/*.ts", 33 | "tests/**/*.ts", 34 | "tests/specs/**/*.ts" 35 | ], 36 | "exclude": [ 37 | "dist", 38 | "coverage", 39 | "playground", 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /types/jsonschema.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MIT License 3 | * 4 | * Copyright (c) 2016 Richard Adams (https://github.com/enriched) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | export interface JsonSchema { 26 | [key: string]: any; 27 | $ref?: string; 28 | // /////////////////////////////////////////////// 29 | // Schema Metadata 30 | // /////////////////////////////////////////////// 31 | /** 32 | * This is important because it tells refs where 33 | * the root of the document is located 34 | */ 35 | id?: string; 36 | /** 37 | * It is recommended that the meta-schema is 38 | * included in the root of any JSON Schema 39 | */ 40 | $schema?: JsonSchema; 41 | /** 42 | * Title of the schema 43 | */ 44 | title?: string; 45 | /** 46 | * Schema description 47 | */ 48 | description?: string; 49 | /** 50 | * Default json for the object represented by 51 | * this schema 52 | */ 53 | 'default'?: any; 54 | 55 | /** 56 | * The value of this keyword MAY be of any type, 57 | * including null. 58 | * An instance validates successfully against this 59 | * keyword if its value is equal to the value of 60 | * the keyword. 61 | */ 62 | const?: any; 63 | 64 | // /////////////////////////////////////////////// 65 | // Number Validation 66 | // /////////////////////////////////////////////// 67 | /** 68 | * The value must be a multiple of the number 69 | * (e.g. 10 is a multiple of 5) 70 | */ 71 | multipleOf?: number; 72 | maximum?: number; 73 | /** 74 | * If true maximum must be > value, >= otherwise 75 | */ 76 | exclusiveMaximum?: number; 77 | minimum?: number; 78 | /** 79 | * If true minimum must be < value, <= otherwise 80 | */ 81 | exclusiveMinimum?: number; 82 | 83 | // /////////////////////////////////////////////// 84 | // String Validation 85 | // /////////////////////////////////////////////// 86 | maxLength?: number; 87 | minLength?: number; 88 | /** 89 | * This is a regex string that the value must 90 | * conform to 91 | */ 92 | pattern?: string; 93 | 94 | // /////////////////////////////////////////////// 95 | // Array Validation 96 | // /////////////////////////////////////////////// 97 | additionalItems?: JsonSchema; 98 | items?: JsonSchema | JsonSchema[]; 99 | maxItems?: number; 100 | minItems?: number; 101 | uniqueItems?: boolean; 102 | 103 | // /////////////////////////////////////////////// 104 | // Object Validation 105 | // /////////////////////////////////////////////// 106 | maxProperties?: number; 107 | minProperties?: number; 108 | required?: string[]; 109 | additionalProperties?: boolean | JsonSchema; 110 | /** 111 | * Holds simple JSON Schema definitions for 112 | * referencing from elsewhere. 113 | */ 114 | definitions?: {[key: string]: JsonSchema}; 115 | /** 116 | * The keys that can exist on the object with the 117 | * json schema that should validate their value 118 | */ 119 | properties?: {[property: string]: JsonSchema}; 120 | /** 121 | * The key of this object is a regex for which 122 | * properties the schema applies to 123 | */ 124 | patternProperties?: {[pattern: string]: JsonSchema}; 125 | /** 126 | * If the key is present as a property then the 127 | * string of properties must also be present. 128 | * If the value is a JSON Schema then it must 129 | * also be valid for the object if the key is 130 | * present. 131 | */ 132 | dependencies?: {[key: string]: JsonSchema | string[]}; 133 | 134 | // /////////////////////////////////////////////// 135 | // Generic 136 | // /////////////////////////////////////////////// 137 | /** 138 | * Enumerates the values that this schema can be 139 | * e.g. 140 | * {"type": "string", 141 | * "enum": ["red", "green", "blue"]} 142 | */ 143 | 'enum'?: unknown[]; 144 | /** 145 | * The basic type of this schema, can be one of 146 | * [string, number, object, array, boolean, null] 147 | */ 148 | type: 'object' | 'array' | 'string' | 'number' | 'integer' | 'boolean' | 'null'; 149 | format?: string; 150 | 151 | // /////////////////////////////////////////////// 152 | // Combining Schemas 153 | // /////////////////////////////////////////////// 154 | allOf?: JsonSchema[]; 155 | anyOf?: JsonSchema[]; 156 | oneOf?: JsonSchema[]; 157 | /** 158 | * The entity being validated must not match this schema 159 | */ 160 | not?: JsonSchema; 161 | /** 162 | * Draft 7 163 | * @see https://json-schema.org/latest/json-schema-validation.html 164 | */ 165 | readOnly?: boolean; 166 | writeOnly?: boolean; 167 | 168 | contentMediaType?: string; 169 | } 170 | -------------------------------------------------------------------------------- /vuedoc.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | section: 'FormSchema API', 3 | output: 'README.md', 4 | filenames: [ 5 | 'src/components/FormSchema.ts', 6 | ], 7 | parsing: { 8 | features: [ 9 | 'description', 10 | 'keywords', 11 | 'slots', 12 | 'props', 13 | 'events', 14 | 'methods' 15 | ], 16 | }, 17 | }; 18 | --------------------------------------------------------------------------------