├── .nvmrc
├── docs
├── .nojekyll
├── .DS_Store
├── _assets
│ ├── logo.png
│ ├── favicon.ico
│ ├── stylesheet.css
│ └── index.js
├── getting_started
│ ├── installation.md
│ ├── writing_tests.md
│ ├── callbacks.md
│ └── result.md
├── _sidebar.md
├── compatability
│ └── assertions.md
├── index.html
├── test
│ ├── warn_only_tests.md
│ ├── how_to_fail.md
│ ├── index.md
│ ├── async.md
│ └── specific.md
├── utilities
│ └── README.md
├── enforce.md.bak
├── README.md
└── enforce.md
├── .github
├── CODEOWNERS
├── PULL_REQUEST_TEMPLATE.md
├── CODE_OF_CONDUCT.md
└── CONTRIBUTING.md
├── .eslintignore
├── config
├── babel-register.js
├── test-setup.js
├── babel.config.js
└── rollup.js
├── scripts
├── prepare_next.sh
├── get_next_version.js
├── prep_docs.js
├── handle_changes.sh
├── constants.js
├── determine_change_level.js
├── push_tag_to_master.sh
├── get_diff.js
├── create_release.js
└── update_changelog.js
├── src
├── core
│ ├── test
│ │ ├── lib
│ │ │ ├── index.js
│ │ │ ├── isTestFn
│ │ │ │ ├── index.js
│ │ │ │ └── spec.js
│ │ │ └── TestObject
│ │ │ │ ├── index.js
│ │ │ │ └── spec.js
│ │ ├── index.js
│ │ └── spec.js
│ ├── draft
│ │ ├── constants.js
│ │ ├── index.js
│ │ └── spec.js
│ ├── Context
│ │ ├── index.js
│ │ └── spec.js
│ ├── passable
│ │ ├── index.js
│ │ └── spec.js
│ ├── Specific
│ │ ├── index.js
│ │ └── spec.js
│ └── passableResult
│ │ └── index.js
├── lib
│ ├── index.js
│ ├── globalObject
│ │ └── index.js
│ └── singleton
│ │ ├── constants.js
│ │ ├── index.js
│ │ └── spec.js
├── spec
│ ├── fail.d.ts
│ ├── ts.spec.js
│ ├── react.elements.support.spec.js
│ ├── passable.api.severity.spec.js
│ ├── passable.exports.spec.js
│ └── passable.api.specific.spec.js
├── constants.js
├── utilities
│ └── validate
│ │ ├── index.js
│ │ └── spec.js
└── index.js
├── .npmignore
├── .gitignore
├── playground
└── index.js
├── LICENSE
├── .travis.yml
├── package.json
├── .eslintrc
├── README.md
├── CHANGELOG.md
├── dist
└── passable.min.js
└── index.d.ts
/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/*
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fiverr/passable/HEAD/docs/.DS_Store
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Owners for everything
2 | * @raphaelboukara @fiverr/fe-team
3 |
--------------------------------------------------------------------------------
/docs/_assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fiverr/passable/HEAD/docs/_assets/logo.png
--------------------------------------------------------------------------------
/docs/_assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fiverr/passable/HEAD/docs/_assets/favicon.ico
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | ./dist/*
2 | ./dev/*
3 | ./node_modules/*
4 | ./flow-typed/*
5 | ./docs
6 | webpack.config.js
--------------------------------------------------------------------------------
/config/babel-register.js:
--------------------------------------------------------------------------------
1 | require("@babel/register")({
2 | configFile: './config/babel.config.js'
3 | });
--------------------------------------------------------------------------------
/scripts/prepare_next.sh:
--------------------------------------------------------------------------------
1 | echo "Preparing next tag"
2 |
3 | npm version "${NEXT_VERSION}-next-${TRAVIS_COMMIT:(0):6}" --no-git-tag
4 |
--------------------------------------------------------------------------------
/src/core/test/lib/index.js:
--------------------------------------------------------------------------------
1 | export { default as isTestFn } from './isTestFn';
2 | export { default as TestObject } from './TestObject';
3 |
--------------------------------------------------------------------------------
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | export { default as globalObject } from './globalObject';
2 | export { default as singleton } from './singleton';
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .eslintignore
2 | .eslintrc
3 | .travis.yml
4 | .github
5 | yarn.lock
6 | docs
7 | scripts
8 | config
9 | playground
10 | src
11 |
--------------------------------------------------------------------------------
/src/spec/fail.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is should fail in tests because of invalid typings.
3 | */
4 | interface FailureTypes {
5 | name: typeNotExist
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/globalObject/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {Object} Reference to global object.
3 | */
4 | const globalObject = Function('return this')();
5 |
6 | export default globalObject;
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vscode
3 | .sass-cache
4 | dev_docpress
5 | _docpress
6 | dev
7 | version.json
8 | version.txt
9 | .version
10 | documentation/MAIN.md
11 | documentation/assets/**/*.css
12 | .DS_Store
--------------------------------------------------------------------------------
/config/test-setup.js:
--------------------------------------------------------------------------------
1 | const regeneratorRuntime = require('regenerator-runtime');
2 | const chai = require('chai');
3 |
4 | global.expect = chai.expect;
5 | global.PASSABLE_VERSION = require('../package.json').version;
6 |
7 | require('../src');
8 |
--------------------------------------------------------------------------------
/src/core/draft/constants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {String} Error thrown when draft gets called without an active Passable context.
3 | */
4 | export const ERROR_NO_CONTEXT = '[Passable]: Draft was called outside of the context of a running suite. Please make sure you call it only from your Passable suite.';
5 |
--------------------------------------------------------------------------------
/src/lib/singleton/constants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {String} Passable's major version.
3 | */
4 | const PASSABLE_MAJOR = PASSABLE_VERSION.split('.')[0];
5 |
6 | /**
7 | * @type {Symbol} Used to store a global instance of Passable.
8 | */
9 | export const SYMBOL_PASSABLE = Symbol.for(`PASSABLE#${PASSABLE_MAJOR}`);
10 |
--------------------------------------------------------------------------------
/scripts/get_next_version.js:
--------------------------------------------------------------------------------
1 | const { version } = require('../package.json');
2 | const semver = require('semver');
3 | const determineLevel = require('./determine_change_level');
4 |
5 | const changeLevel = determineLevel(process.argv[2] || '');
6 |
7 | const nextVersion = semver.inc(version, changeLevel);
8 |
9 | console.log(nextVersion);
10 |
--------------------------------------------------------------------------------
/scripts/prep_docs.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const n4sRules = fs.readFileSync('./node_modules/n4s/docs/rules.md', 'utf8').replace('\n#', '\n##');
4 | const enforceDoc = fs.readFileSync('./docs/enforce.md.bak', 'utf8');
5 |
6 | const nextDoc = enforceDoc.replace('{{LIST_OF_ENFORCE_RULES}}', n4sRules);
7 |
8 | fs.writeFileSync('./docs/enforce.md', nextDoc);
9 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {String} Version number derived from current tag.
3 | */
4 | export const VERSION = PASSABLE_VERSION;
5 |
6 | /**
7 | * @type {String} Keyword used for marking non failing tests.
8 | */
9 | export const WARN = 'warn';
10 |
11 | /**
12 | * @type {String} Keyword used for marking failing tests.
13 | */
14 | export const FAIL = 'fail';
15 |
--------------------------------------------------------------------------------
/scripts/handle_changes.sh:
--------------------------------------------------------------------------------
1 | echo "Script: handle_changes"
2 |
3 | echo "Getting diff"
4 | export COMMIT_MESSAGES=$(node ./scripts/get_diff.js)
5 |
6 | echo "Commit message is:"
7 | echo $COMMIT_MESSAGES
8 |
9 | echo "Getting next version"
10 | export NEXT_VERSION=$(node ./scripts/get_next_version.js "$COMMIT_MESSAGES")
11 |
12 | echo "Next version is:"
13 | echo $NEXT_VERSION
14 |
--------------------------------------------------------------------------------
/src/core/test/lib/isTestFn/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Checks that a given argument qualifies as a test function
3 | * @param {*} testFn
4 | * @return {Boolean}
5 | */
6 | const isTestFn = (testFn) => {
7 | if (!testFn) {
8 | return false;
9 | }
10 |
11 | return typeof testFn.then === 'function' || typeof testFn === 'function';
12 | };
13 |
14 | export default isTestFn;
15 |
--------------------------------------------------------------------------------
/docs/getting_started/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 | You can use npm to install Passable as a package and include it in your app
3 |
4 | ```shell
5 | npm install passable --save
6 | ```
7 |
8 | Or import Passable as a script tag to your page:
9 | ```html
10 |
11 | ```
12 |
13 | To view the currently running version, you can use:
14 |
15 | ```js
16 | console.log(passable.VERSION) // 7.1.0
17 | ```
18 |
--------------------------------------------------------------------------------
/src/core/draft/index.js:
--------------------------------------------------------------------------------
1 | import { singleton } from '../../lib';
2 | import { ERROR_NO_CONTEXT } from './constants';
3 |
4 | /**
5 | * @return {Object} Current draft.
6 | */
7 | const draft = () => {
8 |
9 | const ctx = singleton.use().ctx;
10 |
11 | if (ctx) {
12 | return ctx.result.output;
13 | }
14 |
15 | setTimeout(() => {
16 | throw new Error(ERROR_NO_CONTEXT);
17 | });
18 | };
19 |
20 | export default draft;
21 |
--------------------------------------------------------------------------------
/docs/_assets/stylesheet.css:
--------------------------------------------------------------------------------
1 | .markdown-section code {
2 | font-size: 85%;
3 | color: #26B4AD;
4 | }
5 |
6 | .app-name {
7 | padding: 10px;
8 | }
9 |
10 | .sidebar-nav {
11 | padding-left: 10px;
12 | }
13 |
14 | body, .anchor span, a.anchor {
15 | color: #3C4464;
16 | }
17 |
18 | .token.function,.token.keyword {
19 | color: #26B4AD
20 | }
21 |
22 | .token.boolean, .token.number {
23 | color: #59d4e8;
24 | }
25 |
26 | .markdown-section a:hover {
27 | color: #3a6b96;
28 | }
--------------------------------------------------------------------------------
/config/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 |
3 | if (api) {
4 | api.cache(true);
5 | }
6 |
7 | const presets = [
8 | '@babel/preset-env'
9 | ];
10 |
11 | const plugins = [
12 | 'babel-plugin-add-module-exports',
13 | '@babel/plugin-proposal-class-properties',
14 | '@babel/plugin-transform-object-assign'
15 | ];
16 |
17 | return {
18 | include: [/src/, /node_modules/],
19 | presets,
20 | plugins
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/src/utilities/validate/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Run tests and catch errors
3 | *
4 | * @param {function} callback The test content
5 | * @return {boolean}
6 | */
7 | function validate(test) {
8 |
9 | if (typeof test !== 'function' && !(test instanceof Promise)) {
10 | throw new TypeError(`[Validate]: expected ${typeof test} to be a function.`);
11 | }
12 |
13 | try {
14 | return test() !== false;
15 | } catch (_) {
16 | return false;
17 | }
18 | }
19 |
20 | export default validate;
21 |
--------------------------------------------------------------------------------
/src/core/Context/index.js:
--------------------------------------------------------------------------------
1 | import { singleton } from '../../lib';
2 |
3 | /**
4 | * Creates a new context object, and assigns it as a static property on Passable's singleton.
5 | * @param {Object} parent Parent context.
6 | */
7 | const Context = function(parent) {
8 | singleton.use().ctx = this;
9 | Object.assign(this, parent);
10 | };
11 |
12 | /**
13 | * Clears stored instance from constructor function.
14 | */
15 | Context.clear = function() {
16 | singleton.use().ctx = null;
17 | };
18 |
19 | export default Context;
20 |
--------------------------------------------------------------------------------
/scripts/constants.js:
--------------------------------------------------------------------------------
1 | const PATCH_KEYWORD = 'patch';
2 | const MINOR_KEYWORD = 'minor';
3 | const MAJOR_KEYWORD = 'major';
4 |
5 | const CHANGELOG_TITLES = {
6 | [MAJOR_KEYWORD]: 'Breaking changes',
7 | [MINOR_KEYWORD]: 'Additions',
8 | [PATCH_KEYWORD]: 'Fixes and non breaking changes'
9 | };
10 |
11 | const EXCLUDED_WORDS = [
12 | 'dependabot',
13 | '[config]'
14 | ];
15 |
16 | module.exports = {
17 | PATCH_KEYWORD,
18 | MINOR_KEYWORD,
19 | MAJOR_KEYWORD,
20 | CHANGELOG_TITLES,
21 | EXCLUDED_WORDS
22 | };
23 |
--------------------------------------------------------------------------------
/src/spec/ts.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | const { execSync } = require('child_process');
3 |
4 | describe('TypeScript Typings', () => {
5 | it('Should pass the typings check', () => {
6 | expect(() => {
7 | execSync('node_modules/.bin/tsc index.d.ts');
8 | }).to.not.throw();
9 | }).timeout(0);
10 |
11 | it('Should fail the typings check with fail stub', () => {
12 | expect(() => {
13 | execSync('node_modules/.bin/tsc src/spec/fail.d.ts');
14 | }).to.throw();
15 | }).timeout(0);
16 | });
17 |
--------------------------------------------------------------------------------
/playground/index.js:
--------------------------------------------------------------------------------
1 | const passable = require('../dist/Passable');
2 | const enforce = passable.enforce;
3 |
4 | console.log('Playground. Lets play!');
5 | console.log(`passable version: ${passable.VERSION}`);
6 |
7 | passable('TestForm', (test) => {
8 | test('Field1', 'Should be valid', () => {
9 | enforce(1).isNumeric();
10 | });
11 |
12 | test('Field2', 'should wait some and pass', new Promise((resolve, reject) => setTimeout(resolve, 1000)));
13 | test('Field3', 'should wait some and fail', new Promise((resolve, reject) => setTimeout(reject, 3000)));
14 | }).done(console.log);
15 |
--------------------------------------------------------------------------------
/scripts/determine_change_level.js:
--------------------------------------------------------------------------------
1 | const { MAJOR_KEYWORD, MINOR_KEYWORD, PATCH_KEYWORD } = require('./constants');
2 |
3 | /**
4 | * Determines semver level
5 | * @param {String} message
6 | * @return {String} change level
7 | */
8 | const determineChangeLevel = (message) => {
9 | if (message.toLowerCase().includes(`[${MAJOR_KEYWORD}]`)) {
10 | return MAJOR_KEYWORD;
11 | }
12 |
13 | if (message.toLowerCase().includes(`[${MINOR_KEYWORD}]`)) {
14 | return MINOR_KEYWORD;
15 | }
16 |
17 | return PATCH_KEYWORD;
18 | };
19 |
20 | module.exports = determineChangeLevel;
21 |
--------------------------------------------------------------------------------
/docs/_assets/index.js:
--------------------------------------------------------------------------------
1 | Object.assign(window, passable);
2 |
3 | setTimeout(() => {
4 | console.log(`All passable functions are exposed globally,
5 | you can try passable in your console.`);
6 |
7 | console.log(`PASSABLE VERSION:`, passable.VERSION);
8 |
9 | console.table(Object.keys(passable).map((item) => [item, typeof passable[item]]));
10 |
11 | console.log(`You can try:
12 |
13 | const data = {
14 | useanme: 'example'
15 | };
16 |
17 | passable('FormName', () => {
18 | test('username', 'Must be at least 4 chars', () => {
19 | enforce(data.username).longerThanOrEquals(4);
20 | });
21 | });
22 | `)
23 | }, 1000);
--------------------------------------------------------------------------------
/docs/_sidebar.md:
--------------------------------------------------------------------------------
1 |
2 | * Getting Started
3 | * [Installation](./getting_started/installation.md)
4 | * [Writing tests](./getting_started/writing_tests.md)
5 | * [Passable callbacks](./getting_started/callbacks.md)
6 | * [The `result` object](./getting_started/result.md)
7 | * [The `test` Function](./test/index.md)
8 | * [How to fail a test](./test/how_to_fail.md)
9 | * [Warn only tests](./test/warn_only_tests.md)
10 | * [Skipping tests](./test/specific.md)
11 | * [Async tests](./test/async.md)
12 | * [Test Utilities](./utilities/README.md)
13 | * [Assertions with `enforce`](./enforce.md)
14 | * [Using with assertions libraries](./compatability/assertions.md)
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import enforce from 'n4s/dist/enforce.min';
2 | import any from 'anyone/any';
3 | import passable from './core/passable';
4 | import draft from './core/draft';
5 | import test from './core/test';
6 | import validate from './utilities/validate';
7 | import { singleton } from './lib';
8 | import { WARN, FAIL, VERSION } from './constants';
9 |
10 | passable.VERSION = VERSION;
11 | passable.enforce = enforce;
12 | passable.draft = draft;
13 | passable.Enforce = enforce.Enforce;
14 | passable.test = test;
15 | passable.validate = validate;
16 | passable.any = any;
17 | passable.WARN = WARN;
18 | passable.FAIL = FAIL;
19 |
20 | singleton.register(passable);
21 |
22 | export default passable;
23 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
10 |
11 | | Q | A
12 | | ---------------- | ---
13 | | Bug fix? | yes/no
14 | | New feature? | yes/no
15 | | Breaking change? | yes/no
16 | | Deprecations? | yes/no
17 | | Documentation? | yes/no
18 | | Tests added? | yes/no
19 | | Fixed issues | comma-separated list of issues fixed by the pull request, where applicable
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/utilities/validate/spec.js:
--------------------------------------------------------------------------------
1 | import validate from '.';
2 | import { enforce } from '../../index';
3 | import { expect } from 'chai';
4 |
5 | describe('Utilities: validate', () => {
6 | it('Should return `false` for a failing test', () => {
7 | expect(validate(() => {
8 | enforce(33).greaterThan(100);
9 | })).to.equal(false);
10 | });
11 |
12 | it('Should return `false` for a falsy statement', () => {
13 | expect(validate(() => false)).to.equal(false);
14 | });
15 |
16 | it('Should return `true` for a truthy statement', () => {
17 | expect(validate(() => true)).to.equal(true);
18 | });
19 |
20 | it('Should return `true` for an empty test', () => {
21 | expect(validate(() => undefined)).to.equal(true);
22 | });
23 |
24 | it('Should throw TypeError when no tests are present', () => {
25 | expect(() => validate()).to.throw(TypeError);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/core/test/lib/isTestFn/spec.js:
--------------------------------------------------------------------------------
1 | import faker from 'faker';
2 | import { expect } from 'chai';
3 | import isTestFn from '.';
4 |
5 | describe('isTestFn module', () => {
6 |
7 | describe('When argument is a function', () => {
8 | it('Should return true', () => {
9 | expect(isTestFn(Function.prototype)).to.equal(true);
10 | });
11 | });
12 |
13 | describe('When argument is a Promise', () => {
14 | it('Should return true', () => {
15 | expect(isTestFn(new Promise(() => null))).to.equal(true);
16 | });
17 | });
18 |
19 | describe('When argument is any other type', () => {
20 | expect([
21 | { [faker.lorem.word()]: faker.lorem.word() },
22 | [faker.lorem.word()],
23 | 1,
24 | faker.lorem.word(),
25 | true,
26 | null,
27 | NaN
28 | ].some(isTestFn)).to.equal(false);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/spec/react.elements.support.spec.js:
--------------------------------------------------------------------------------
1 | const runSpec = (passable) => {
2 | describe('React Elements Support', () => {
3 | let result;
4 |
5 | beforeEach(() => {
6 | result = passable('NewUserForm', (test) => {
7 | test('username', { a: 1 }, () => {
8 | passable.enforce(true).equals(false);
9 | });
10 |
11 | test('username', { a: 2 }, () => {
12 | enforce(true).equals(false);
13 | }, passable.WARN);
14 | });
15 | });
16 |
17 | it('should support objects in errors', () => {
18 | expect(result.errors.username).to.deep.equal([{ a: 1 }]);
19 | });
20 |
21 | it('should support objects in warnings', () => {
22 | expect(result.warnings.username).to.deep.equal([{ a: 2 }]);
23 | });
24 | });
25 | };
26 |
27 | runSpec(require('../'));
28 | runSpec(require('../../dist/passable'));
29 | runSpec(require('../../dist/passable.min.js'));
30 |
--------------------------------------------------------------------------------
/scripts/push_tag_to_master.sh:
--------------------------------------------------------------------------------
1 | git config --global user.email "${GIT_NAME}@users.noreply.github.com" --replace-all
2 | git config --global user.name $GIT_NAME
3 |
4 | echo "Removing old master"
5 | git branch -D master
6 |
7 | echo "Switching to new master"
8 | git checkout -b master
9 |
10 | echo "Bumping version"
11 | npm version $NEXT_VERSION --no-git-tag
12 |
13 | echo "Rebuilding with current tag"
14 | yarn build
15 |
16 | echo "Updating changelog"
17 | CHANGELOG=$(node ./scripts/update_changelog.js)
18 |
19 | EMOJIS=(🚀 🤘 ✨ 🔔 🌈 🤯)
20 | EMOJI=${EMOJIS[$RANDOM % ${#EMOJIS[@]}]}
21 |
22 | git add .
23 |
24 | if (( $(grep -c . <<<"$msg") > 1 )); then
25 | git commit -m "$EMOJI Passable cumulative update: $NEXT_VERSION" -m "$COMMIT_MESSAGES"
26 | else
27 | git commit -m "$EMOJI Passable update: $NEXT_VERSION" -m "$COMMIT_MESSAGES"
28 | fi
29 |
30 | echo "Pushing to master"
31 | git push https://${GITHUB_TOKEN}@github.com/$GITHUB_REPO.git master
32 |
33 | git tag $NEXT_VERSION
34 | git push origin $NEXT_VERSION
35 |
36 | echo "Publishing Release"
37 | node ./scripts/create_release.js "$CHANGELOG"
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Fiverr
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 |
--------------------------------------------------------------------------------
/src/core/Context/spec.js:
--------------------------------------------------------------------------------
1 | import faker from 'faker';
2 | import { singleton } from '../../lib';
3 | import Context from '.';
4 |
5 | describe('Context', () => {
6 | let parent, instance;
7 |
8 | beforeEach(() => {
9 |
10 | parent = {
11 | [faker.random.word()]: faker.random.word(),
12 | [faker.random.word()]: faker.random.word(),
13 | [faker.random.word()]: faker.random.word()
14 | };
15 |
16 | instance = new Context(parent);
17 | });
18 |
19 | it('Should assign all parent properties onto ctx instance', () => {
20 | Object.keys(parent).forEach((key) => {
21 | expect(instance[key]).to.equal(parent[key]);
22 | });
23 | });
24 |
25 | it('Should store instance on singleton', () => {
26 | expect(singleton.use().ctx).to.equal(instance);
27 | });
28 |
29 | describe('Context.clear', () => {
30 | it('Should nullify stored instance', () => {
31 | expect(singleton.use().ctx).to.equal(instance);
32 | Context.clear();
33 | expect(singleton.use().ctx).to.equal(null);
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/docs/compatability/assertions.md:
--------------------------------------------------------------------------------
1 | # Using with other assertion libraries
2 | Using other assertion libraries along with Passable can't be easier. Most popular assertion libraries are supported by default, and many others are supported as well. Basically, if it throws an error, it works.
3 |
4 | For example, let's say you want to use the popular v8n validation library, which can be configured to throw an exception for failed validations.
5 |
6 | ```js
7 | import passable, { enforce } 'passable';
8 | import v8n from 'v8n';
9 |
10 | // data = {
11 | // username: 'ealush',
12 | // age: 27
13 | // }
14 |
15 | passable('FormWithV8N', (test) => {
16 | test('username', 'Should be a string', () => {
17 | v8n()
18 | .string()
19 | .check(data.username);
20 | });
21 |
22 | test('age', 'Should be a number and larger than 18', () => {
23 | v8n()
24 | .number()
25 | .greaterThan(18)
26 | .check(data.age);
27 | });
28 | });
29 |
30 | ```
--------------------------------------------------------------------------------
/scripts/get_diff.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch');
2 |
3 | function compareUrl(repo, [commit1, commit2]) {
4 | return `https://api.github.com/repos/${repo}/compare/${commit1}...${commit2}`;
5 | }
6 |
7 | function listMessages(commits = []) {
8 | return commits.reduce((accumulator, { commit, author, sha }) => {
9 | const [message] = commit.message.split('\n');
10 | const name = author.login || commit.author.name;
11 | return `${accumulator}${sha.slice(0, 7)} ${message} (${name})\n`;
12 | }, '');
13 | }
14 |
15 | function getCommitDiff(repo, branches, token) {
16 | return fetch(compareUrl(repo, branches), { ...token && { headers: { Authorization: `token ${token}` } } })
17 | .then((res) => res.json())
18 | .catch(() => process.exit(1));
19 | }
20 |
21 | async function init({
22 | repo,
23 | branches,
24 | token
25 | } = {}) {
26 | const { commits } = await getCommitDiff(repo, branches, token);
27 | const messages = listMessages(commits);
28 | console.log(messages);
29 | }
30 |
31 | init({
32 | repo: process.env.GITHUB_REPO,
33 | branches: ['master', process.env.TRAVIS_BRANCH],
34 | token: process.env.GITHUB_TOKEN
35 | });
36 |
--------------------------------------------------------------------------------
/scripts/create_release.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch');
2 |
3 | const TITLE_DELIMITER = '\n\n';
4 |
5 | const changelog = process.argv[2] || '';
6 |
7 | function releaseUrl(repo) {
8 | return `https://api.github.com/repos/${repo}/releases`;
9 | };
10 |
11 | function postRelease({
12 | repo,
13 | token,
14 | tag,
15 | body
16 | }) {
17 | console.log('posting release');
18 | fetch(releaseUrl(repo), {
19 | method: 'POST',
20 | headers: { Authorization: `token ${token}` },
21 | body: JSON.stringify({
22 | tag_name: tag,
23 | name: tag,
24 | body
25 | })
26 | });
27 | }
28 |
29 | function release({ repo, token, tag }) {
30 | console.log('In release');
31 |
32 | if (!(changelog && token && repo)) {
33 | return;
34 | }
35 |
36 | const body = changelog.substr(changelog.indexOf(TITLE_DELIMITER) + TITLE_DELIMITER.length);
37 |
38 | postRelease({
39 | repo,
40 | token,
41 | tag,
42 | body
43 | });
44 | };
45 |
46 | release({
47 | repo: process.env.GITHUB_REPO,
48 | token: process.env.GITHUB_TOKEN,
49 | tag: process.env.NEXT_VERSION,
50 | changelog
51 | });
52 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | passable - Declarative validations
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/lib/singleton/index.js:
--------------------------------------------------------------------------------
1 | import go from '../globalObject';
2 | import { SYMBOL_PASSABLE } from './constants';
3 |
4 | /**
5 | * @param {String[]} versions List of passable versions.
6 | * @throws {Error}
7 | */
8 | const throwMultiplePassableError = (...versions) => {
9 | throw new Error(`[Passable]: Multiple versions of Passable detected: (${versions.join()}).
10 | Most features should work regularly, but for optimal feature compatibility, you should have all running instances use the same version.`);
11 | };
12 |
13 | /**
14 | * Registers current Passable instance on global object.
15 | * @param {Function} passable Reference to passable.
16 | * @return {Function} Global passable reference.
17 | */
18 | const register = (passable) => {
19 |
20 | const existing = go[SYMBOL_PASSABLE];
21 |
22 | if (existing) {
23 | if (existing.VERSION !== passable.VERSION) {
24 | setTimeout(() => throwMultiplePassableError(passable.VERSION, existing.VERSION));
25 | }
26 | } else {
27 | go[SYMBOL_PASSABLE] = passable;
28 | }
29 |
30 | return go[SYMBOL_PASSABLE];
31 | };
32 |
33 | const singletonExport = {
34 | use: () => go[SYMBOL_PASSABLE],
35 | register
36 | };
37 |
38 | export default singletonExport;
39 |
--------------------------------------------------------------------------------
/config/rollup.js:
--------------------------------------------------------------------------------
1 | import commonjs from 'rollup-plugin-commonjs';
2 | import resolve from 'rollup-plugin-node-resolve';
3 | import babel from 'rollup-plugin-babel';
4 | import replace from 'rollup-plugin-replace';
5 | import { terser } from 'rollup-plugin-terser';
6 | const { version } = require('../package.json');
7 |
8 | const DEFAULT_FORMAT = 'umd';
9 | const LIBRARY_NAME = 'passable';
10 |
11 | const PLUGINS = [
12 | resolve(),
13 | commonjs({
14 | include: /node_modules\/(anyone|n4s)/
15 | }),
16 | babel({
17 | babelrc: false,
18 | ...require('./babel.config')()
19 | }),
20 | replace({
21 | PASSABLE_VERSION: JSON.stringify(version)
22 | })
23 | ];
24 |
25 | const buildConfig = ({ format = DEFAULT_FORMAT, min = false } = {}) => ({
26 | input: 'src/index.js',
27 | output: {
28 | file: [
29 | `dist/${LIBRARY_NAME}`,
30 | min && 'min',
31 | format !== DEFAULT_FORMAT && format,
32 | 'js'
33 | ].filter(Boolean).join('.'),
34 | name: LIBRARY_NAME,
35 | format
36 | },
37 | plugins: min
38 | ? [ ...PLUGINS, terser() ]
39 | : PLUGINS
40 |
41 | });
42 |
43 | export default [
44 | buildConfig({ min: true }),
45 | buildConfig()
46 | ];
47 |
--------------------------------------------------------------------------------
/src/core/passable/index.js:
--------------------------------------------------------------------------------
1 | import Context from '../Context';
2 | import test, { runAsync } from '../test';
3 | import passableResult from '../passableResult';
4 | import Specific from '../Specific';
5 | import { singleton } from '../../lib';
6 |
7 | const initError = (name, value, doc) => `[Passable]: failed during suite initialization. Unexpected '${typeof value}' for '${name}' argument.
8 | See: ${doc ? doc : 'https://fiverr.github.io/passable/getting_started/writing_tests.html'}`;
9 |
10 | const passable = (name, tests, specific) => {
11 | if (typeof name !== 'string') {
12 | throw new TypeError(initError('suite name', name));
13 | }
14 |
15 | if (typeof tests !== 'function') {
16 | throw new TypeError(initError('tests', tests));
17 | }
18 |
19 | if (specific && !Specific.is(specific)) {
20 | throw new TypeError(initError('specific', tests, 'https://fiverr.github.io/passable/test/specific.html'));
21 | }
22 |
23 | const result = passableResult(name);
24 |
25 | const pending = [];
26 |
27 | new Context({
28 | specific: new Specific(specific),
29 | result,
30 | pending
31 | });
32 |
33 | tests(test, result.output);
34 |
35 | Context.clear();
36 |
37 | [...pending].forEach(runAsync);
38 |
39 | return result.output;
40 | };
41 |
42 | export default passable;
43 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | cache: npm
3 |
4 | env:
5 | global:
6 | - GIT_NAME: A1vy
7 | - GIT_EMAIL: opensource@fiverr.com
8 | - GITHUB_REPO: fiverr/passable
9 | - GIT_SOURCE: docs
10 |
11 | before_script:
12 | - npm run build
13 | - npm run eslint
14 |
15 | after_success:
16 | - chmod +x ./scripts/handle_changes.sh
17 | - source ./scripts/handle_changes.sh
18 | - node ./scripts/prep_docs.js
19 | - chmod +x ./scripts/push_tag_to_master.sh
20 | - if [ "$TRAVIS_BRANCH" = "release" ]; then ./scripts/push_tag_to_master.sh; fi
21 | - chmod +x ./scripts/prepare_next.sh
22 | - if [ "$TRAVIS_BRANCH" != "master" ]; then ./scripts/prepare_next.sh; fi
23 |
24 | deploy:
25 | - provider: npm
26 | email: it@fiverr.com
27 | skip_cleanup: true
28 | api_key: $NPM_TOKEN
29 | on:
30 | repo: fiverr/passable
31 | branch: master
32 | - provider: npm
33 | email: it@fiverr.com
34 | skip_cleanup: true
35 | api_key: $NPM_TOKEN
36 | tag: "next"
37 | on:
38 | all_branches: true
39 | condition: $TRAVIS_BRANCH = "next"
40 | repo: fiverr/passable
41 | - provider: npm
42 | email: it@fiverr.com
43 | skip_cleanup: true
44 | api_key: $NPM_TOKEN
45 | tag: "DEVELOPMENT"
46 | on:
47 | all_branches: true
48 | condition: $TRAVIS_BRANCH != "next" && $TRAVIS_BRANCH != "master" && $TRAVIS_BRANCH != "release"
49 | repo: fiverr/passable
50 |
--------------------------------------------------------------------------------
/src/core/passable/spec.js:
--------------------------------------------------------------------------------
1 | import Passable from '.';
2 | import faker from 'faker';
3 | import { noop } from 'lodash';
4 | import sinon from 'sinon';
5 | import { expect } from 'chai';
6 |
7 | describe('Test passable suite wrapper', () => {
8 | const passable = (...args) => new Passable(...args);
9 |
10 | describe('Test arguments', () => {
11 | it('Should throw a TypeError for a non-string name', () => {
12 |
13 | expect(() => passable(1, noop))
14 | .to.throw(TypeError);
15 | expect(() => passable({}, noop))
16 | .to.throw(TypeError);
17 | expect(() => passable(noop, noop))
18 | .to.throw(TypeError);
19 | });
20 |
21 | it('Should throw TypeError if `tests` is not a function', () => {
22 | expect(() => passable('MyForm', 'noop', null))
23 | .to.throw(TypeError);
24 | });
25 |
26 | it('Should throw an exception when specific does not follow convention', () => {
27 | expect(() => passable('FormName', noop, noop)).to.throw(TypeError);
28 | expect(() => passable('FormName', noop, true)).to.throw(TypeError);
29 | });
30 | });
31 |
32 | it('Should pass down `test` function to `tests` callback', () => {
33 | const p = new Passable('name', (test) => {
34 | expect(test).to.be.a('function');
35 | });
36 | });
37 |
38 | it('Calls `tests` argument', (done) => {
39 | new Passable('FormName', () => done());
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/core/test/lib/TestObject/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Describes a test call inside a passable suite.
3 | * @param {Object} ctx Parent context.
4 | * @param {String} fieldName Name of the field being tested.
5 | * @param {String} statement The message returned when failing.
6 | * @param {Promise|Function} testFn The actual test callbrack or promise.
7 | * @param {String} [severity] Indicates whether the test should fail or warn.
8 | */
9 | function TestObject(ctx, fieldName, statement, testFn, severity) {
10 | Object.assign(this, {
11 | ctx,
12 | testFn,
13 | fieldName,
14 | statement,
15 | severity,
16 | failed: false
17 | });
18 | };
19 |
20 | /**
21 | * @returns Current validity status of a test.
22 | */
23 | TestObject.prototype.valueOf = function() {
24 | return this.failed !== true;
25 | };
26 |
27 | /**
28 | * Sets a field to failed.
29 | * @returns {TestObject} Current instance.
30 | */
31 | TestObject.prototype.fail = function() {
32 |
33 | this.ctx.result.fail(
34 | this.fieldName,
35 | this.statement,
36 | this.severity
37 | );
38 |
39 | this.failed = true;
40 | return this;
41 | };
42 |
43 | /**
44 | * Adds current test to pending list.
45 | */
46 | TestObject.prototype.setPending = function() {
47 | this.ctx.pending.push(this);
48 | };
49 |
50 | /**
51 | * Removes test from pending list.
52 | */
53 | TestObject.prototype.clearPending = function() {
54 | this.ctx.pending = this.ctx.pending.filter((t) => t !== this);
55 | };
56 |
57 | export default TestObject;
58 |
--------------------------------------------------------------------------------
/docs/test/warn_only_tests.md:
--------------------------------------------------------------------------------
1 | # Warn only `test`
2 | By default, a failing `test` will set `.hasErrors()` to `true`. Sometimes you need to set a warn-only validation test (password strength, for example). In this case, you would add the `WARN` flag to your test function as the last argument.
3 | WARN validations work exactly the same. The only thing different is that the result will be stored under `warnings` instead of `errors`, and `.hasWarnings()` will return true.
4 |
5 | If no flag is added, your test function will default to `FAIL`. The `WARN` and `FAIL` flags are constants exported from passable.
6 |
7 | ```js
8 | // es6 imports
9 | import passable, { WARN } from 'passable';
10 |
11 | // es5
12 | const passable = require('passable');
13 | const WARN = passable.WARN;
14 | ```
15 |
16 | Use it like this:
17 |
18 | ```js
19 | import passable, { WARN, enforce } from 'passable';
20 |
21 | const result = passable('WarnAndPass', (test) => {
22 | test('WarnMe', 'Should warn and not fail', () => {
23 | enforce(5).greaterThan(500);
24 | }, WARN);
25 | });
26 |
27 | result.hasWarnings(); // true
28 | result.hasWarnings('WarnMe'); // true
29 | result.hasErrors(); // false
30 | ```
31 |
32 | You may also use the values directly from the result object.
33 | ```js
34 | {
35 | name: 'WarnAndPass',
36 | skipped: [],
37 | testsPerformed: {
38 | WarnMe: {
39 | testCount: 1,
40 | failCount: 0,
41 | warnCount: 1
42 | }
43 | },
44 | errors: {},
45 | warnings: {
46 | WarnMe: [
47 | 'Should warn and not fail'
48 | ]
49 | },
50 | failCount: 0,
51 | warnCount: 1,
52 | testCount: 1
53 | }
54 | ```
--------------------------------------------------------------------------------
/docs/test/how_to_fail.md:
--------------------------------------------------------------------------------
1 | # All ways to fail a `test`
2 | With the exclusion of async tests, there are two ways of failing a test, and marking it as having a validation error:
3 |
4 | ## Throwing an Error
5 |
6 | Throwing an error from within the `test`. Thrown errors within the `test` function are caught and handled to mark the test as failing.
7 | ```js
8 | test('field', 'should fail by a thrown error', () => { throw new Error(); });
9 | ```
10 | This also includes `enforce` failures, which throws when the validation criteria are not met:
11 |
12 | ```js
13 | test('field', 'should fail by enforce', () => {
14 | enforce(1).greaterThan(5);
15 | });
16 | ```
17 |
18 | ## Explicitly returning `false`
19 |
20 | Explicitly returning `false` from the `test` itself (running some logic that returns false) will fail your test. This is good especially for migration periods in which your previous validations relied on a boolean flag, and you want to quickly just copy things over.
21 |
22 | ```js
23 | test('field', 'should explicitly fail by returning `false`', () => false);
24 | test('field', 'should explicitly fail by returning `false`', () => data.value !== 1);
25 | ```
26 |
27 | ## Promise rejection (Async)
28 |
29 | Since [async tests](./test/async.html) rely on Promise, the way to fail the test is simply to reject it:
30 |
31 | ```js
32 | test('Field3', 'should wait some and fail',
33 | new Promise((resolve, reject) => setTimeout(reject, 3000))
34 |
35 | );
36 | test('Field4', 'should wait some and fail',
37 | () => (
38 | new Promise((resolve, reject) => setTimeout(reject, 3000))
39 | )
40 | );
41 | ```
--------------------------------------------------------------------------------
/scripts/update_changelog.js:
--------------------------------------------------------------------------------
1 |
2 | const fs = require('fs');
3 | const { format } = require('date-fns');
4 | const determineLevel = require('./determine_change_level');
5 | const { MAJOR_KEYWORD, MINOR_KEYWORD, PATCH_KEYWORD, CHANGELOG_TITLES, EXCLUDED_WORDS } = require('./constants');
6 |
7 | const version = process.env.NEXT_VERSION;
8 | const gitLog = process.env.COMMIT_MESSAGES;
9 |
10 | /**
11 | * Takes commit history and groups messages by change level
12 | * @param {String} gitLog commit history
13 | * @return {Object} an object with keys matching the semver levels
14 | */
15 | const groupMessages = (gitLog) => gitLog.split('\n').reduce((accumulator, current) => {
16 | const level = determineLevel(current);
17 |
18 | if (EXCLUDED_WORDS.some((word) => current.toLowerCase().includes(word.toLowerCase()))) {
19 | return accumulator;
20 | }
21 |
22 | if (!accumulator[level]) {
23 | accumulator[level] = `### ${CHANGELOG_TITLES[level]}\n`;
24 | }
25 |
26 | return Object.assign(accumulator, {
27 | [level]: `${accumulator[level]}- ${current}\n`
28 | });
29 | }, {});
30 |
31 | const updateChangelog = () => {
32 | const groupedMessages = groupMessages(gitLog);
33 |
34 | const changelogTitle = `## [${version}] - ${format(new Date(), 'yyyy-MM-dd')}\n`;
35 |
36 | const versionLog = [
37 | changelogTitle,
38 | groupedMessages[MAJOR_KEYWORD],
39 | groupedMessages[MINOR_KEYWORD],
40 | groupedMessages[PATCH_KEYWORD]
41 | ].filter(Boolean).join('\n');
42 |
43 | const changelog = fs.readFileSync('./CHANGELOG.md', 'utf8').split('\n');
44 | changelog.splice(6, 0, versionLog);
45 |
46 | fs.writeFileSync('./CHANGELOG.md', changelog.join('\n'));
47 |
48 | console.log(versionLog);
49 | };
50 |
51 | updateChangelog();
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "passable",
3 | "version": "8.0.0",
4 | "description": "Isomorphic Data Model Validations.",
5 | "main": "./dist/passable.min.js",
6 | "typings": "index.d.ts",
7 | "author": "Evyatar ",
8 | "license": "MIT",
9 | "scripts": {
10 | "build": "rollup --config config/rollup.js -m",
11 | "pretest": "npm run build",
12 | "test": "mocha --file ./config/test-setup.js --require ./config/babel-register.js --colors \"./src/**/*spec.js\"",
13 | "eslint": "eslint -c .eslintrc \"./src/**/*.js\"; exit 0"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/fiverr/passable.git"
18 | },
19 | "bugs": {
20 | "url": "https://github.com/fiverr/passable/issues"
21 | },
22 | "homepage": "https://fiverr.github.io/passable/",
23 | "devDependencies": {
24 | "@babel/cli": "^7.5.5",
25 | "@babel/core": "^7.5.5",
26 | "@babel/plugin-proposal-class-properties": "^7.5.5",
27 | "@babel/plugin-transform-object-assign": "^7.2.0",
28 | "@babel/preset-env": "^7.5.5",
29 | "@babel/register": "^7.5.5",
30 | "anyone": "0.0.5",
31 | "babel-eslint": "^10.0.1",
32 | "babel-loader": "^8.0.6",
33 | "babel-plugin-add-module-exports": "^1.0.2",
34 | "chai": "^4.2.0",
35 | "cross-env": "^7.0.3",
36 | "date-fns": "^2.0.1",
37 | "eslint": "^7.29.0",
38 | "eslint-loader": "^4.0.2",
39 | "faker": "^5.5.3",
40 | "lodash": "^4.17.15",
41 | "mocha": "^9.0.3",
42 | "n4s": "0.4.1",
43 | "node-fetch": "^2.6.0",
44 | "regenerator-runtime": "^0.13.3",
45 | "rollup": "^2.55.1",
46 | "rollup-plugin-babel": "^4.3.2",
47 | "rollup-plugin-commonjs": "^10.0.2",
48 | "rollup-plugin-node-resolve": "^5.2.0",
49 | "rollup-plugin-replace": "^2.2.0",
50 | "rollup-plugin-terser": "^5.0.0",
51 | "semver": "^7.3.5",
52 | "sinon": "^11.1.1",
53 | "typescript": "^4.3.5"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/docs/test/index.md:
--------------------------------------------------------------------------------
1 | # The `test` function
2 | The test function is a single test in your validations. It is similar to unit tests `it` or `test` function. `test` accepts the name of the test, the error message and the actual test logic. Tests can either be sync or async.
3 |
4 | You may have multipe `test` functions validating different aspects each field, each with a different error.
5 |
6 | ```js
7 | import passable, { enforce } from 'passable';
8 |
9 | passable('MyForm', (test) => {
10 |
11 | test('name', 'should be at least 3 chars', () => {
12 | enforce(data.name).longerThanOrEquals(3);
13 | });
14 |
15 | test('name', 'must be unique', new Promise((resolve, reject) => {
16 | fetch(`/userExists?name=${name}`)
17 | .then(res => res.json)
18 | .then(data => {
19 | if (data.exists) {
20 | reject();
21 | } else {
22 | resolve();
23 | }
24 | }
25 | }));
26 |
27 | test('age', 'must be at least 18', () => {
28 | enforce(data.age).greaterThanOrEquals(18);
29 | });
30 | });
31 | ```
32 |
33 | ## Synchronous Tests
34 | In most cases, you would want to test some already existing data value against predefined rules, for example - input field length or email regex. These kinds of validations can be performed synchronously, as they only rely on information already present for you when you initialize the test suite.
35 |
36 | The synchronous test is simply a callback function passed as the third argument of the `test` function. If this function throws an exception or explicitly returns `false` the test is considered to be failing. Otherwise, it passes implicitly.
37 |
38 | ```js
39 | test('name', 'should be at least 3 chars', () => {
40 | enforce(data.name).longerThan(2);
41 | });
42 | ```
43 |
44 | ### Table of Contents
45 | * [Async tests](test/async.md)
46 | * [How to fail a test](test/how_to_fail.md)
47 | * [Warn only test](test/warn_only_tests.md)
48 | * [Running a specific tests](test/specific.md)
49 |
--------------------------------------------------------------------------------
/docs/utilities/README.md:
--------------------------------------------------------------------------------
1 | # Passable utilities and helpers
2 |
3 | Using Passable on its own usually covers most of your needs, but sometimes you could use the following utility functions to cover the less common use cases.
4 |
5 | ## `any()` for OR relationship tests
6 |
7 | > Since 7.1.0
8 |
9 | Sometimes you need to have `OR` (`||`) relationship in your validations, this is tricky to do on your own, and `any()` simplifies this process.
10 |
11 | The general rule for using `any()` in your validation is when you can say: "At least one of the following has to pass".
12 |
13 | A good example would be: When your validated field can be either empty (not required), but when field - has to pass some validation.
14 |
15 | ### Usage
16 | `any()` accepts an infinite number of arguments, all of which are functions. It returns a function, that when called - behaves just like a passable [`test`](../test/index.md) function callback, and it fails on [these conditions](../test/how_to_fail.md).
17 |
18 | The only difference is - if any of the supplied tests passes, the success condition is met and `any()` returns true.
19 |
20 | ```js
21 | import passable, { test, enforce, any } from 'passable';
22 |
23 | const validation = (data) => passable('Checkout', () => {
24 |
25 | test('coupon', 'When filled, must be at least 5 chars', any(
26 | () => enforce(data.coupon).isEmpty(),
27 | () => enforce(data.coupon).longerThanOrEquals(5)
28 | ));
29 | });
30 |
31 | ```
32 |
33 | ## `validate()` for leaner tests
34 |
35 | > Since 5.10.0
36 |
37 | In cases where you have only a handful of fields to validate, you might want to use `enforce`, but not wrap it with a whole passable suite. For these cases, you can use `valiate`, which can wrap enforce, and gives you back a boolean result.
38 |
39 | ### Usage
40 |
41 | `validate` accepts one argument:
42 |
43 | | Argument | Type | Required? | Description |
44 | |------------|------------|-----------|-------------|
45 | | `test` | `function` | Yes | The test function to run |
46 |
47 | Just like the [`test`](../test/index.md) function callback, `validate` will fail your tests on [these conditions](../test/how_to_fail.md).
48 |
49 | ```js
50 |
51 | import { validate, enforce } from 'passable';
52 |
53 | // name = 'Eve'
54 | const valid = validate(() => {
55 | enforce(name).longerThanOrEquals(5);
56 | });
57 |
58 | // valid = false;
59 | ```
--------------------------------------------------------------------------------
/docs/test/async.md:
--------------------------------------------------------------------------------
1 | # Asynchronous Tests
2 |
3 | Sometimes you would want to validate data with information from the server, for example - username availability. In these cases, you should add an async test to your suite, reaching the server before going in and performing the validation. An async test is a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) object. When it *resolves*, the test passes, and when it *rejects* the test fails. An async test will not complete unless either happens.
4 |
5 | There are two ways to perform an async test. One, is by passing a promise as your test, and the other by returning a promise from your test function.
6 |
7 | ## Passing a Promise directly
8 |
9 | ```js
10 | test('name', 'must be unique', new Promise((resolve, reject) => {
11 | fetch(`/userExists?name=${name}`)
12 | .then(res => res.json)
13 | .then(data => {
14 | if (data.exists) {
15 | reject(); // rejects and marks the test as failing
16 | } else {
17 | resolve(); // completes. doesn't mark the test as failing
18 | }
19 | });
20 | }));
21 | ```
22 |
23 | ## Returning a Promise / Async/Await
24 |
25 | ```js
26 | test('name', 'Should be unique', async () => {
27 | const res = await doesUserExist(user);
28 | return res;
29 | });
30 |
31 | test('name', 'I fail', async () => Promise.reject());
32 | ```
33 |
34 | ## Rejecting with rejection message
35 |
36 | What if your promise can reject with different messages? No problem!
37 | You can reject the promise with your own message by passing it to the
38 | rejection callback.
39 |
40 | Notice that when using rejection messages we do not need to pass `statement`
41 | argument to `test`. This means that the statement will always be inferred
42 | from the rejection message.
43 |
44 | In case you do pass `statement`, it will serve as a fallback message in any
45 | case that the rejection message is not provided.
46 |
47 | ```js
48 | test('name', new Promise((resolve, reject) => {
49 | fetch(`/checkUsername?name=${name}`)
50 | .then(res => res.json)
51 | .then(data => {
52 | if (data.status === 'fail') {
53 | reject(data.message); // rejects with message and marks the test as failing
54 | } else {
55 | resolve(); // completes. doesn't mark the test as failing
56 | }
57 | });
58 | }));
59 | ```
--------------------------------------------------------------------------------
/src/core/test/lib/TestObject/spec.js:
--------------------------------------------------------------------------------
1 | import faker from 'faker';
2 | import _ from 'lodash';
3 | import { expect } from 'chai';
4 | import { WARN, FAIL } from '../../../../constants';
5 | import passableResult from '../../../passableResult';
6 | import TestObject from '.';
7 |
8 | describe('TestObject module', () => {
9 | let testObject, ctx, testFn, fieldName, statement, severity;
10 |
11 | beforeEach(() => {
12 | ctx = {
13 | result: passableResult(faker.lorem.word()),
14 | pending: []
15 | };
16 | testFn = _.noop;
17 | fieldName = faker.lorem.word();
18 | statement = faker.lorem.sentence();
19 | severity = _.sample([WARN, FAIL]);
20 |
21 | testObject = new TestObject(
22 | ctx,
23 | testFn,
24 | fieldName,
25 | statement,
26 | severity
27 | );
28 | });
29 |
30 | describe('.fail() methoh', () => {
31 | it('Should set `failed` to true', () => {
32 | expect(testObject.failed).to.equal(false);
33 | testObject.fail();
34 | expect(testObject.failed).to.equal(true);
35 | });
36 | });
37 |
38 | describe('.setPending() method', () => {
39 | it('Should push current instance to pending array', () => {
40 | expect(ctx.pending).to.have.lengthOf(0);
41 | testObject.setPending();
42 | expect(ctx.pending).to.have.lengthOf(1);
43 | expect(ctx.pending).to.have.members([testObject]);
44 | });
45 | });
46 |
47 | describe('.clearPending() method', () => {
48 | beforeEach(() => {
49 | testObject.setPending();
50 | });
51 |
52 | it('Should push current instance to pending array', () => {
53 | expect(ctx.pending).to.have.members([testObject]);
54 | testObject.clearPending();
55 | expect(ctx.pending).not.to.have.members([testObject]);
56 | });
57 | });
58 |
59 | describe('.valueOf()', () => {
60 |
61 | describe('Default case', () => {
62 | it('Should return true', () => {
63 | expect(testObject == true).to.equal(true); // eslint-disable-line
64 | });
65 | });
66 |
67 | describe('When invalid', () => {
68 |
69 | beforeEach(() => {
70 | testObject.fail();
71 | });
72 |
73 | it('Should return false', () => {
74 | expect(testObject == false).to.equal(true); // eslint-disable-line
75 | });
76 | });
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/docs/enforce.md.bak:
--------------------------------------------------------------------------------
1 | # Enforce
2 | For assertions, Passable is bundled with [Enforce](npmjs.com/package/n4s). Enforce is a validation assertions library. It allows you to run your data against rules and conditions and test whether it passes your validations. It is intended for validation logic that gets repeated over and over again and should not be written manually. It comes with a wide-variety of pre-built rules, but it can also be extended to support your own repeated custom logic.
3 |
4 | The way Enforce operates is similar to most common assertion libraries. You pass it a value, and one or more rules to test your value against - if the validation fails, it throws an Error, otherwise - it will move on to the next rule rule in the chain.
5 |
6 | ```js
7 | import { passable } from 'passable'
8 |
9 | enforce(4)
10 | .isNumber();
11 | // passes
12 |
13 | enforce(4)
14 | .isNumber()
15 | .greaterThan(2);
16 | // passes
17 |
18 | enforce(4)
19 | .lessThan(2) // throws an error, will not carry on to the next rule
20 | .greaterThan(3);
21 | ```
22 |
23 | ## Content
24 | - [List of Enforce rules](#list-of-enforce-rules)
25 | - [Custom Enforce Rules](#custom-enforce-rules)
26 |
27 | Enforce exposes all predefined and custom rules. You may use chaining to make multiple enfocements for the same value.
28 |
29 | {{LIST_OF_ENFORCE_RULES}}
30 |
31 | # Custom enforce rules
32 | To make it easier to reuse logic across your application, sometimes you would want to encapsulate bits of logic in rules that you can use later on, for example, "what's considered a valid email".
33 |
34 | Your custom rules are essentially a single javascript object containing your rules.
35 | ```js
36 | const myCustomRules = {
37 | isValidEmail: (value) => value.indexOf('@') > -1,
38 | hasKey: (value, {key}) => value.hasOwnProperty(key),
39 | passwordsMatch: (passConfirm, options) => passConfirm === options.passConfirm && options.passIsValid
40 | }
41 | ```
42 | Just like the predefined rules, your custom rules can accepts two parameters:
43 | * `value` The actual value you are testing against.
44 | * `args` (optional) the arguments which you pass on when running your tests.
45 |
46 |
47 | You can extend enforce with your custom rules by creating a new instance of `Enforce` and adding the rules object as the argument.
48 |
49 | ```js
50 | import { Enforce } from 'passable';
51 |
52 | const myCustomRules = {
53 | isValidEmail: (value) => value.indexOf('@') > -1,
54 | hasKey: (value, key) => value.hasOwnProperty(key),
55 | passwordsMatch: (passConfirm, options) => passConfirm === options.passConfirm && options.passIsValid
56 | }
57 |
58 | const enforce = new Enforce(myCustomRules);
59 |
60 | enforce(user.email).isValidEmail();
61 | ```
62 |
--------------------------------------------------------------------------------
/src/core/Specific/index.js:
--------------------------------------------------------------------------------
1 | /** Class representing validation inclusion and exclusion groups */
2 | class Specific {
3 |
4 | /**
5 | * Initialize Specific object
6 | *
7 | * @param {String | Array | Object | undefined} specific
8 | */
9 | constructor(specific) {
10 |
11 | if (!specific) { return; }
12 |
13 | if (!Specific.is(specific)) {
14 | throw new TypeError();
15 | }
16 |
17 | if (typeof specific === 'string' || Array.isArray(specific)) {
18 | if (specific.length === 0) { return; }
19 | this.only = this.populateGroup(this.only, specific);
20 | return;
21 | }
22 |
23 | if (specific.only) {
24 | this.only = this.populateGroup(this.only, specific.only);
25 | }
26 |
27 | if (specific.not) {
28 | this.not = this.populateGroup(this.not, specific.not);
29 | }
30 | }
31 |
32 | /**
33 | * Populate inclusion and exclusion groups
34 | *
35 | * @param {Object} group - the group to populate.
36 | * @param {String | Array} field - the field to add to the group
37 | * @return {Object} modified group
38 | */
39 | populateGroup(group, field) {
40 | group = group || {};
41 |
42 | if (typeof field === 'string') {
43 | group[field] = true;
44 | } else if (Array.isArray(field)) {
45 | field.forEach((item) => group[item] = true);
46 | }
47 |
48 | return group;
49 | }
50 |
51 | /**
52 | * Checkes whether a given field name is in exclusion group
53 | * or not a member of inclusion group (when present)
54 | *
55 | * @param {String} fieldName
56 | * @return {Boolean}
57 | */
58 | excludes(fieldName) {
59 | if (this.only && !this.only[fieldName]) {
60 | return true;
61 | }
62 |
63 | if (this.not && this.not[fieldName]) {
64 | return true;
65 | }
66 |
67 | return false;
68 | }
69 |
70 | /**
71 | * Test whether a given argument matches
72 | * the `specific` filter convention
73 | *
74 | * @param {Any} item
75 | * @return {boolean}
76 | */
77 | static is(item) {
78 | if (Array.isArray(item)) {
79 | return item.every((item) => typeof item === 'string');
80 | }
81 |
82 | if (typeof item === 'string') { return true; }
83 |
84 | if (item !== null && typeof item === 'object' && (
85 | item.hasOwnProperty('only')
86 | || item.hasOwnProperty('not')
87 | )) {
88 | return true;
89 | }
90 |
91 | return false;
92 | }
93 | }
94 |
95 | export default Specific;
--------------------------------------------------------------------------------
/src/spec/passable.api.severity.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | const runSpec = (passable) => {
4 | const { WARN } = passable;
5 |
6 | describe('Test warn flag', () => {
7 | it('Should mark test with warning', () => {
8 | expect(warnPass.hasErrors()).to.equal(false);
9 | expect(warnPass.hasWarnings()).to.equal(true);
10 | });
11 |
12 | it('Should mark test with both warning and error', () => {
13 | expect(warnFail.hasErrors()).to.equal(true);
14 | expect(warnFail.hasWarnings()).to.equal(true);
15 | });
16 |
17 | it('Should only fail test', () => {
18 | expect(fail.hasErrors()).to.equal(true);
19 | expect(fail.hasWarnings()).to.equal(false);
20 | });
21 | });
22 |
23 | // Actual test data
24 |
25 | const warnPass = passable('WarnPass', (test) => {
26 | test('WarnPass', 'should warn', () => false, WARN);
27 | }),
28 | warnFail = passable('WarnFail', (test) => {
29 | test('Warn', 'should warn', () => false, WARN);
30 | test('Fail', 'should Fail', () => false);
31 | }),
32 | fail = passable('Fail', (test) => {
33 | test('Warn', 'should not warn', () => true, WARN);
34 | test('Fail', 'should Fail', () => false);
35 | });
36 |
37 | const warnPassExpected = {
38 | name: 'WarnPass',
39 | skipped: [],
40 | testsPerformed: {
41 | WarnPass: { testCount: 1, failCount: 0, warnCount: 1 }
42 | },
43 | errors: {},
44 | warnings: { WarnPass: ['should warn'] },
45 | failCount: 0,
46 | warnCount: 1,
47 | testCount: 1
48 | },
49 | warnFailExpected = {
50 | name: 'WarnFail',
51 | skipped: [],
52 | testsPerformed: {
53 | Warn: { testCount: 1, failCount: 0, warnCount: 1 },
54 | Fail: { testCount: 1, failCount: 1, warnCount: 0 }
55 | },
56 | errors: { Fail: ['should Fail'] },
57 | warnings: { Warn: ['should warn'] },
58 | failCount: 1,
59 | warnCount: 1,
60 | testCount: 2
61 | },
62 | failExpected = {
63 | name: 'Fail',
64 | skipped: [],
65 | testsPerformed: {
66 | Warn: { testCount: 1, failCount: 0, warnCount: 0 },
67 | Fail: { testCount: 1, failCount: 1, warnCount: 0 }
68 | },
69 | errors: { Fail: ['should Fail'] },
70 | warnings: {},
71 | failCount: 1,
72 | warnCount: 0,
73 | testCount: 2
74 | };
75 | };
76 |
77 | runSpec(require('../'));
78 | runSpec(require('../../dist/passable'));
79 | runSpec(require('../../dist/passable.min.js'));
80 |
--------------------------------------------------------------------------------
/docs/test/specific.md:
--------------------------------------------------------------------------------
1 | # Running or skipping `specific` tests
2 | Sometimes you want to test only a specific field out of the whole dataset. For example, when validating upon user interaction (such as input change), you probably do not need to validate all other fields as well. Similarly, you might want to **not** run the tests of specific fields, for example - fields that have not been touched by the user.
3 |
4 | To specify which fields should or should not run, use the `specific` param. It is the third argument in the `passable()` function, and it is optional.
5 |
6 | `specific` accepts any of the following:
7 |
8 | | Type | Description
9 | |-----------------|------------
10 | | `undefined` | No `test` will be skipped (same as empty array or empty string `[] | ''`)
11 | | `string` | The names of the `test` function that will run. All the rest will be skipped.
12 | | `Array` | Array with the names of the `test` functions that should run. All the rest will be skipped.
13 | | `Object` | Allows both setting `test` functions that should run, or not run
14 |
15 | **Remember** Using the array or string directly as your `specific` param is exactly the same as using `only`, and is provided as a shorthand to reduce clutter.
16 |
17 | ```js
18 | passable('formName', (test) => {...}, ['field_1', 'field_2'])
19 | // ------
20 | passable('formName', (test) => {...}, 'field_1')
21 | ```
22 |
23 | ## `specific` object structure:
24 | > Since 6.0.0
25 |
26 | As noted before, The `specific` object gives you more control on which fields should be tested and which should not. It may contain any of the following keys:
27 |
28 | | Name | Type | Description
29 | |--------|------------------|-----
30 | | `only` | `Array`/`String` | Only these `test` functions will run. The rest will be skipped
31 | | `not` | `Array`/`String` | These `test` functions will be skipped. The rest will run normally
32 |
33 | ```js
34 | passable('formName', (test) => {...}, {only: 'field_1'})
35 | // ------
36 | passable('formName', (test) => {...}, {only: ['field_1', 'field_2']})
37 | // ------
38 | passable('formName', (test) => {...}, {not: 'field_1'})
39 | // ------
40 | passable('formName', (test) => {...}, {not: ['field_1', 'field_2']})
41 | ```
42 |
43 | ## Production use
44 |
45 | The easiest way to use the `specific` argument in production, is to wrap your validation with a function that passes down the fields to include, only if needed.
46 |
47 | In the following example, only First test is going to run. Second will be skipped.
48 | ```js
49 | // app.js
50 | import validate from './validate.js';
51 | const result = validate(['First'], data);
52 |
53 | // validation.js
54 | function validate (specific) {
55 | return passable('MyForm', (test) => {
56 | test('First', 'should pass', () => {...});
57 | test('Second', 'should be skipped', () => {...});
58 | }, specific);
59 | };
60 | ```
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@fiverr.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "sourceType": "module",
4 | "ecmaFeatures": {
5 | "globalReturn": true,
6 | "modules": true
7 | }
8 | },
9 | "parser": "babel-eslint",
10 | "env": {
11 | "browser": true,
12 | "es6": true,
13 | "node": true,
14 | "mocha": true
15 | },
16 | "globals": {
17 | "document": false,
18 | "escape": false,
19 | "navigator": false,
20 | "unescape": false,
21 | "window": false,
22 | "describe": true,
23 | "before": true,
24 | "it": true,
25 | "sinon": true
26 | },
27 | "rules": {
28 | "eqeqeq": 2,
29 | "no-multi-spaces": [2, {
30 | "exceptions": {
31 | "VariableDeclarator": true,
32 | "Property": false
33 | }
34 | }],
35 | "comma-dangle": 2,
36 | "comma-spacing": 2,
37 | "comma-style": 2,
38 | "computed-property-spacing": 2,
39 | "dot-notation": 2,
40 | "func-call-spacing": 2,
41 | "indent": ["error", 4, {
42 | "SwitchCase": 1
43 | }],
44 | "jsx-quotes": 2,
45 | "keyword-spacing": 2,
46 | "lines-around-comment": ["error", {
47 | "beforeBlockComment": true,
48 | "allowBlockStart": true,
49 | "beforeLineComment": true
50 | }],
51 | "new-cap": 2,
52 | "no-lonely-if": 2,
53 | "no-case-declarations": 0,
54 | "no-multiple-empty-lines": 2,
55 | "no-trailing-spaces": 1,
56 | "no-unneeded-ternary": 2,
57 | "no-whitespace-before-property": 2,
58 | "quotes": ["error", "single", {
59 | "avoidEscape": true
60 | }],
61 | "semi-spacing": 2,
62 | "semi": ["error", "always"],
63 | "space-before-function-paren": ["error", "never"],
64 | "space-in-parens": 2,
65 | "spaced-comment": ["error", "always"],
66 | "vars-on-top": 2,
67 | "radix": 2,
68 | "no-nested-ternary": 2,
69 | "one-var-declaration-per-line": 2,
70 | "no-unsafe-negation": 2,
71 | "array-callback-return": 2,
72 | "block-scoped-var": 2,
73 | "curly": 2,
74 | "default-case": 2,
75 | "no-caller": 2,
76 | "no-eval": 2,
77 | "no-floating-decimal": 2,
78 | "no-global-assign": 2,
79 | "no-implied-eval": 2,
80 | "no-loop-func": 2,
81 | "no-empty-function": 2,
82 | "no-extra-bind": 2,
83 | "no-sequences": 2,
84 | "no-useless-call": 2,
85 | "no-useless-escape": 2,
86 | "no-useless-return": 2,
87 | "no-with": 2,
88 | "wrap-iife": [2, "inside"],
89 | "consistent-this": 2,
90 | "arrow-body-style": 2,
91 | "arrow-parens": 2,
92 | "arrow-spacing": 2,
93 | "no-duplicate-imports": 2,
94 | "no-template-curly-in-string": 2,
95 | "no-useless-computed-key": 2,
96 | "no-useless-constructor": 2,
97 | "no-useless-rename": 2,
98 | "no-var": 2,
99 | "object-shorthand": ["error", "properties"],
100 | "prefer-arrow-callback": 2,
101 | "prefer-const": ["error", {
102 | "destructuring": "all",
103 | "ignoreReadBeforeAssign": false
104 | }],
105 | "prefer-spread": 2,
106 | "prefer-template": 2,
107 | "rest-spread-spacing": 2,
108 | "template-curly-spacing": 2
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # 
2 |
3 | Declarative data validations.
4 |
5 | [](https://badge.fury.io/js/passable) [](https://travis-ci.org/fiverr/passable)
6 |
7 |
8 | - [Documentation homepage](https://fiverr.github.io/passable/)
9 | - [Try it live](https://stackblitz.com/edit/passable-example?file=validate.js)
10 | - [Getting started](getting_started/writing_tests).
11 |
12 | ## What is Passable?
13 | Passable is a library for JS applications for writing validations in a way that's structured and declarative.
14 |
15 | Inspired by the syntax of modern unit testing framework, passable validations are written as a spec or a contract, that reflects your form structure.
16 | Your validations run in production code, and you can use them in any framework (or without any framework at all).
17 |
18 | The idea behind passable is that you can easily adopt its very familiar syntax, and transfer your knowledge from the world of testing to your form validations.
19 |
20 | Much like most testing frameworks, Passable comes with its own assertion function, [enforce](./enforce.md), all error based assertion libraries are supported.
21 |
22 | ## Key features
23 | 1. [Non failing tests](test/warn_only_tests).
24 | 2. [Conditionally running tests](test/specific).
25 | 3. [Async validations](test/async).
26 | 4. [Test callbacks](getting_started/callbacks).
27 |
28 | ---
29 |
30 | ## Syntactic differences from testing frameworks
31 |
32 | Since Passable is running in production environment, and accommodates different needs, some changes to the basic unit test syntax have been made, to cover the main ones quickly:
33 |
34 | - Your test function is not available globally, it is an argument passed to your suite's callback.
35 | - Each test has two string values before its callback, one for the field name, and one for the error returned to the user.
36 | - Your suite accepts another argument after the callback - name (or array of names) of a field. This is so you can run tests selectively only for changed fields.
37 |
38 | ```js
39 | // validation.js
40 | import passable, { enforce } from 'passable';
41 |
42 | const validation = (data) => passable('NewUserForm', (test) => {
43 |
44 | test('username', 'Must be at least 3 chars', () => {
45 | enforce(data.username).longerThanOrEquals(3);
46 | });
47 |
48 | test('email', 'Is not a valid email address', () => {
49 | enforce(data.email)
50 | .isNotEmpty()
51 | .matches(/[^@]+@[^\.]+\..+/g);
52 | });
53 | });
54 |
55 | export default validation;
56 | ```
57 |
58 | ```js
59 | // myFeature.js
60 | import validation from './validation.js';
61 |
62 | const res = validation({
63 | username: 'example',
64 | email: 'email@example.com'
65 | });
66 |
67 | res.hasErrors() // returns whether the form has errors
68 | res.hasErrors('username') // returns whether the 'username' field has errors
69 | res.getErrors() // returns an object with an array of errors per field
70 | res.getErrors('username') // returns an array of errors for the `username` field
71 | ```
72 |
73 | ## "BUT HEY! I ALREADY USE X VALIDATION LIBRARY! CAN IT WORK WITH PASSABLE?"
74 | As a general rule, Passable works similarly to unit tests in term that if your test throws an exception, it is considered to be failing. Otherwise, it is considered to be passing.
75 |
76 | There are a [few more ways](test/how_to_fail) to handle failures in order to ease migration, and in most cases, you can move your validation logic directly to into Passable with only a few adjustments.
77 |
78 | For example, if you use a [different assertion libraries](compatability/assertions) such as `chai` (expect) or `v8n`, you can simply use it instead of enforce, and it should work straight out of the box.
79 |
--------------------------------------------------------------------------------
/src/spec/passable.exports.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import passable, { Enforce, enforce, validate } from '../index';
3 | const passableExports = require('../index');
4 | import { version } from '../../package.json';
5 |
6 | describe("Test Passable's exports", () => {
7 |
8 | it('Should expose all outward facing passable API functions', () => {
9 | const exportsArray = Object.keys(passableExports);
10 | const names = ['validate', 'enforce'];
11 | names.forEach((name) => {
12 | expect(passableExports[name]).to.be.a('function');
13 | });
14 | });
15 |
16 | describe("Test passable's default export", () => {
17 | it('Default export should output correct version number (es6 imports)', () => {
18 | expect(passable.VERSION).to.equal(version);
19 | });
20 |
21 | it('Default export should output correct version number (commonjs)', () => {
22 | expect(passableExports.VERSION).to.equal(version);
23 | });
24 |
25 | it('Default export name should be `passable` (commonjs)', () => {
26 | expect(passableExports.name).to.equal('passable');
27 | });
28 |
29 | it('Default export name should be `passable` (es6 imports)', () => {
30 | expect(passable.name).to.equal('passable');
31 | });
32 | });
33 |
34 | describe('Test Enforce import', () => {
35 | it('enforce instance should be on passable (commonjs)', () => {
36 | expect(passableExports.enforce).to.be.a('function');
37 | });
38 |
39 | it('enforce constructor should be assigned to passable (commonjs)', () => {
40 | expect(passableExports.Enforce).to.be.a('function');
41 | });
42 |
43 | it('enforce instance should be on passable (es6 imports)', () => {
44 | expect(passable.enforce).to.be.a('function');
45 | });
46 |
47 | it('enforce constructor should be on passable (es6 imports)', () => {
48 | expect(passable.Enforce).to.be.a('function');
49 | });
50 |
51 | it('enforce instance should be destructurable directly from passable', () => {
52 | expect(enforce).to.be.a('function');
53 | });
54 |
55 | it('enforce constructor should be destructurable directly from passable', () => {
56 | expect(Enforce).to.be.a('function');
57 | });
58 | });
59 |
60 | describe('Test validate import', () => {
61 | it('validate should be assigned to passable (commonjs)', () => {
62 | expect(passableExports.validate).to.be.a('function');
63 | });
64 |
65 | it('validate should be assigned to passable (es6 imports)', () => {
66 | expect(passable.validate).to.be.a('function');
67 | });
68 |
69 | it('validate should be destructurable directly from passable', () => {
70 | expect(validate).to.be.a('function');
71 | });
72 |
73 | it('validate function name should be validate (es6 imports)', () => {
74 | expect(passable.validate.name).to.equal('validate');
75 | });
76 |
77 | it('validate function name should be validate (commonjs)', () => {
78 | expect(passableExports.validate.name).to.equal('validate');
79 | });
80 | });
81 |
82 | describe('Test severity imports', () => {
83 | it('WARN should be assigned to passable (commonjs)', () => {
84 | expect(passableExports.WARN).to.equal('warn');
85 | });
86 |
87 | it('WARN should be assigned to passable (es6 imports)', () => {
88 | expect(passable.WARN).to.equal('warn');
89 | });
90 |
91 | it('FAIL should be assigned to passable (commonjs)', () => {
92 | expect(passableExports.FAIL).to.equal('fail');
93 | });
94 |
95 | it('FAIL should be assigned to passable (es6 imports)', () => {
96 | expect(passable.FAIL).to.equal('fail');
97 | });
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 
2 |
3 | Declarative data validations.
4 |
5 | [](https://badge.fury.io/js/passable) [](https://travis-ci.org/fiverr/passable)
6 |
7 |
8 | - [Documentation homepage](https://fiverr.github.io/passable/)
9 | - [Try it live](https://stackblitz.com/edit/passable-example?file=validate.js)
10 | - [Getting started](https://fiverr.github.io/passable/#/./getting_started/writing_tests).
11 |
12 | ## What is Passable?
13 | Passable is a library for JS applications for writing validations in a way that's structured and declarative.
14 |
15 | Inspired by the syntax of modern unit testing framework, passable validations are written as a spec or a contract, that reflects your form structure.
16 | Your validations run in production code, and you can use them in any framework (or without any framework at all).
17 |
18 | The idea behind passable is that you can easily adopt its very familiar syntax, and transfer your knowledge from the world of testing to your form validations.
19 |
20 | Much like most testing frameworks, Passable comes with its own assertion function, [enforce](https://fiverr.github.io/passable/#/./enforce.md), all error based assertion libraries are supported.
21 |
22 | ## Key features
23 | 1. [Non failing tests](https://fiverr.github.io/passable/#/test/warn_only_tests).
24 | 2. [Conditionally running tests](https://fiverr.github.io/passable/#/test/specific).
25 | 3. [Async validations](https://fiverr.github.io/passable/#/test/async).
26 | 4. [Test callbacks](https://fiverr.github.io/passable/#/getting_started/callbacks).
27 |
28 | ---
29 |
30 | ## Syntactic differences from testing frameworks
31 |
32 | Since Passable is running in production environment, and accommodates different needs, some changes to the basic unit test syntax have been made, to cover the main ones quickly:
33 |
34 | - Your test function is not available globally, it is an argument passed to your suite's callback.
35 | - Each test has two string values before its callback, one for the field name, and one for the error returned to the user.
36 | - Your suite accepts another argument after the callback - name (or array of names) of a field. This is so you can run tests selectively only for changed fields.
37 |
38 | ```js
39 | // validation.js
40 | import passable, { enforce } from 'passable';
41 |
42 | const validation = (data) => passable('NewUserForm', (test) => {
43 |
44 | test('username', 'Must be at least 3 chars', () => {
45 | enforce(data.username).longerThanOrEquals(3);
46 | });
47 |
48 | test('email', 'Is not a valid email address', () => {
49 | enforce(data.email)
50 | .isNotEmpty()
51 | .matches(/[^@]+@[^\.]+\..+/g);
52 | });
53 | });
54 |
55 | export default validation;
56 | ```
57 |
58 | ```js
59 | // myFeature.js
60 | import validation from './validation.js';
61 |
62 | const res = validation({
63 | username: 'example',
64 | email: 'email@example.com'
65 | });
66 |
67 | res.hasErrors() // returns whether the form has errors
68 | res.hasErrors('username') // returns whether the 'username' field has errors
69 | res.getErrors() // returns an object with an array of errors per field
70 | res.getErrors('username') // returns an array of errors for the `username` field
71 | ```
72 |
73 | ## "BUT HEY! I ALREADY USE X VALIDATION LIBRARY! CAN IT WORK WITH PASSABLE?"
74 | As a general rule, Passable works similarly to unit tests in term that if your test throws an exception, it is considered to be failing. Otherwise, it is considered to be passing.
75 |
76 | There are a [few more ways](https://fiverr.github.io/passable/#/./test/how_to_fail) to handle failures in order to ease migration, and in most cases, you can move your validation logic directly to into Passable with only a few adjustments.
77 |
78 | For example, if you use a [different assertion libraries](https://fiverr.github.io/passable/#/./compatability/assertions) such as `chai` (expect) or `v8n`, you can simply use it instead of enforce, and it should work straight out of the box.
79 |
--------------------------------------------------------------------------------
/src/lib/singleton/spec.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import faker from 'faker';
3 | import { test } from 'mocha';
4 | import passable from '../../';
5 | import Context from '../../core/Context';
6 | import singleton from '.';
7 | import go from '../globalObject';
8 | import { SYMBOL_PASSABLE } from './constants';
9 |
10 | describe('singleton', () => {
11 |
12 | after(() => {
13 | singleton.register(passable);
14 | });
15 |
16 | describe('Attaching to global scope', () => {
17 | beforeEach(() => {
18 | delete go[SYMBOL_PASSABLE];
19 | });
20 |
21 | afterEach(() => {
22 | delete go[SYMBOL_PASSABLE];
23 | });
24 |
25 | test('That global instance is not populated', () => {
26 | expect(go[SYMBOL_PASSABLE]).to.equal(undefined);
27 | });
28 |
29 | it('Should register passable on a global object', () => {
30 | singleton.register(passable, Context);
31 |
32 | expect(go[SYMBOL_PASSABLE]).to.equal(passable);
33 | });
34 |
35 | describe('When already registered', () => {
36 |
37 | beforeEach(() => {
38 | singleton.register(passable);
39 | });
40 |
41 | describe('When same version', () => {
42 | it('Should return silently', (done) => {
43 | const timeout = setTimeout(done, 300);
44 |
45 | process.on('uncaughtException', (err) => {
46 | clearTimeout(timeout);
47 | });
48 |
49 | });
50 | });
51 |
52 | describe('When different version', () => {
53 |
54 | let _uncaughtListeners;
55 |
56 | beforeEach(() => {
57 | _uncaughtListeners = process.listeners('uncaughtException');
58 | process.removeAllListeners('uncaughtException');
59 | });
60 |
61 | afterEach(() => {
62 | _uncaughtListeners.forEach((listener) => process.on('uncaughtException', listener));
63 | });
64 |
65 | it('Should throw an error', (done) => {
66 | const fn = () => null;
67 | fn.VERSION = Math.random();
68 |
69 | singleton.register(fn);
70 |
71 | process.on('uncaughtException', (err) => {
72 | expect(err.message).to.have.string('Multiple versions of Passable detected');
73 | process.removeAllListeners('uncaughtException');
74 | done();
75 | });
76 |
77 | });
78 | });
79 | });
80 |
81 | });
82 |
83 | describe('Make sure everything works together', () => {
84 |
85 | before(() => {
86 | singleton.register(passable);
87 | });
88 |
89 | const instances = [require('../../'), require('../../../dist/passable'), require('../../../dist/passable.min.js')];
90 | const pairs = instances.reduce((pairs, current) => (
91 | [...pairs, ...instances.map(({ test }) => [ current, test ])]
92 | ), []);
93 |
94 | pairs.forEach(([ passable, test ]) => {
95 | it('Should produce correct validation result', () => {
96 | const failCount = _.random(1, 10);
97 | const successCount = _.random(1, 10);
98 | const warnCount = _.random(1, 10);
99 | const output = passable(faker.random.word(), () => {
100 |
101 | Array.from({ length: warnCount }, () => test(faker.random.word(), faker.lorem.sentence(), () => false, passable.WARN));
102 | Array.from({ length: failCount }, () => test(faker.random.word(), faker.lorem.sentence(), () => false));
103 | Array.from({ length: successCount }, () => test(faker.random.word(), faker.lorem.sentence(), () => true));
104 | });
105 |
106 | expect(output.failCount).to.equal(failCount);
107 | expect(output.warnCount).to.equal(warnCount);
108 | expect(output.testCount).to.equal(warnCount + failCount + successCount);
109 | });
110 | });
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/src/core/test/index.js:
--------------------------------------------------------------------------------
1 | import { FAIL } from '../../constants';
2 | import { singleton } from '../../lib';
3 | import { isTestFn, TestObject } from './lib';
4 |
5 | /**
6 | * Run async test.
7 | * @param {TestObject} testObject A TestObject instance.
8 | */
9 | export const runAsync = (testObject) => {
10 | const { fieldName, testFn, statement, ctx } = testObject;
11 |
12 | ctx.result.markAsync(fieldName);
13 |
14 | const done = () => {
15 | testObject.clearPending();
16 |
17 | if (!hasRemainingPendingTests(ctx, fieldName)) {
18 | ctx.result.markAsDone(fieldName);
19 | }
20 |
21 | if (!hasRemainingPendingTests(ctx)) {
22 | ctx.result.markAsDone();
23 | }
24 | };
25 |
26 | const fail = (rejectionMessage) => {
27 | testObject.statement = typeof rejectionMessage === 'string'
28 | ? rejectionMessage
29 | : statement;
30 |
31 | if (ctx.pending.includes(testObject)) {
32 | testObject.fail();
33 | }
34 |
35 | done();
36 | };
37 |
38 | try {
39 | testFn.then(done, fail);
40 | } catch (e) {
41 | fail();
42 | }
43 | };
44 |
45 | /**
46 | * Checks if there still are remaining pending tests for given criteria
47 | * @param {Object} ctx Parent context
48 | * @param {String} [fieldName] Name of the field to test against
49 | * @return {Boolean}
50 | */
51 | const hasRemainingPendingTests = (ctx, fieldName) => {
52 | if (!ctx.pending.length) {
53 | return false;
54 | }
55 |
56 | if (fieldName) {
57 | return ctx.pending.some((testObject) => testObject.fieldName === fieldName);
58 | }
59 |
60 | return !!ctx.pending.length;
61 | };
62 |
63 | /**
64 | * Performs "shallow" run over test functions, assuming sync tests only.
65 | * @param {TestObject} testObject TestObject instance.
66 | * @return {*} Result from test function
67 | */
68 | const preRun = (testObject) => {
69 | let result;
70 | try {
71 | result = testObject.testFn();
72 | } catch (e) {
73 | result = false;
74 | }
75 |
76 | if (result === false) {
77 | testObject.fail();
78 | }
79 |
80 | return result;
81 | };
82 |
83 | /**
84 | * Registers test, if async - adds to pending array
85 | * @param {TestObject} testObject A TestObject Instance.
86 | */
87 | const register = (testObject) => {
88 | const { testFn, ctx, fieldName } = testObject;
89 | let pending = false;
90 | let result;
91 |
92 | if (ctx.specific.excludes(fieldName)) {
93 | ctx.result.addToSkipped(fieldName);
94 | return;
95 | }
96 |
97 | ctx.result.initFieldCounters(fieldName);
98 | ctx.result.bumpTestCounter(fieldName);
99 |
100 | if (testFn && typeof testFn.then === 'function') {
101 | pending = true;
102 | } else {
103 | result = preRun(testObject);
104 | }
105 |
106 | if (result && typeof result.then === 'function') {
107 | pending = true;
108 |
109 | testObject.testFn = result;
110 | }
111 |
112 | if (pending) {
113 | testObject.setPending();
114 | }
115 | };
116 |
117 | /**
118 | * Test function used by consumer to provide their own validations.
119 | * @param {String} fieldName Name of the field to test.
120 | * @param {String} [statement] The message returned in case of a failure.
121 | * @param {function | Promise} testFn The actual test callback or promise.
122 | * @param {String} [severity] Indicates whether the test should fail or warn.
123 | * @return {TestObject} A TestObject instance.
124 | */
125 | const test = (fieldName, ...args) => {
126 | let statement,
127 | testFn,
128 | severity;
129 |
130 | if (isTestFn(args[0])) {
131 | [testFn, severity] = args;
132 | } else if (['string', 'object'].some((type) => typeof args[0] === type)) {
133 | [statement, testFn, severity] = args;
134 | }
135 |
136 | if (!isTestFn(testFn)) {
137 | return;
138 | }
139 |
140 | const testObject = new TestObject(
141 | singleton.use().ctx,
142 | fieldName,
143 | statement,
144 | testFn,
145 | severity || FAIL
146 | );
147 |
148 | register(testObject);
149 |
150 | return testObject;
151 | };
152 |
153 | export default test;
154 |
--------------------------------------------------------------------------------
/docs/getting_started/writing_tests.md:
--------------------------------------------------------------------------------
1 | # Writing tests
2 | Much like when writing unit-tests, writing validations with Passable is all about knowing in advance which values you expect to get. The structure is very similar to the familiar unit test `describe/it/expect` combo, only that with Passable the functions you will mostly run are `Passable/test/enforce`.
3 |
4 | * `passable` - the wrapper for your form validation, much like the describe function in unit tests.
5 | * `test` - a single tests, most commonly a single field, much like the it function in unit tests. [More about test](../test/index.md)
6 | * `enforce` - the function which gets and enforces the data model compliance, similar to the expect function in unit tests. [More about enforce](../enforce.md);
7 |
8 | ## Passable Parameters
9 | The passable suite accepts three arguments:
10 |
11 | | Name | Optional? | Type | Description
12 | |------------|:---------:|:--------:|------------------------------------------------
13 | | `name` | No | String | A name for the group of tests. E.G - form name
14 | | `tests` | No | Function | A function containing the actual validation logic.
15 | | `only/not` | Yes | Array / Object | Whitelist or blacklist of tests to run/skip in the suite see: [Running a specific tests](../test/specific.md)
16 |
17 |
18 | The most basic test would look somewhat like this:
19 |
20 | ```js
21 | // data = {
22 | // username: 'ealush',
23 | // age: 27
24 | // }
25 | passable('NewUserForm', (test) => {
26 | test('username', 'Must be a string between 2 and 10 chars', () => {
27 | enforce(data.username).isString().longerThan(1).shorterThan(11);
28 | });
29 |
30 | test('username', 'already exists', fetch(`/check_availability?username=${data.username}`));
31 |
32 | test('age', 'Must be greater than 18', () => {
33 | enforce(data.age).greaterThan(18);
34 | });
35 | });
36 | ```
37 |
38 | In the example above, we tested a form named `NewUserForm`, and ran two tests on it. One of the `username` field, and one on the `age` field. When testing the username field, we made sure that **all** conditions are true, and when testing age, we made sure that **at least one** condition is true.
39 |
40 | If our validation fails, among other information, we would get the names of the fields, and an array of errors for each, so we can show them back to the user.
41 |
42 | ## Accessing the intermediate test result from within your suite
43 | // since 7.5.0
44 |
45 | In some cases, you might want to access the intermediate result, for example, if you'd like to stop a server call from happening for a field that you already know is invalid. To do that, you can access the draft of the result object from within the test suite.
46 |
47 | To use `draft`, import it from passable, and call it as a function. Its return value is the intermediate result of your test suite.
48 |
49 | **Note**
50 | * It is only possible to access intermediate test results for sync tests, and it is recommended to put all the async tests at the bottom of your suite so they have access to the result of all the sync tests.
51 | * You may not call `draft` from outside a running suite. Doing that will result in a thrown error.
52 |
53 | In the following example, we're preventing the async validation from running over the username field in case it already has errors.
54 |
55 | ```js
56 | import passable, { test, enforce, draft } from 'passable';
57 |
58 | passable('NewUserForm', () => {
59 | test('username', 'Must be a string between 2 and 10 chars', () => {
60 | enforce(data.username).isString().longerThan(1).shorterThan(11);
61 | });
62 |
63 | if (!draft().hasErrors('username')) {
64 | // if the username did not pass the previous test, the following test won't run
65 | test('username', 'already exists', fetch(`/check_availability?username=${data.username}`));
66 | }
67 | });
68 | ```
69 |
70 | You can also use `draft` as the second argument of the your passable suite tests callback. This api is likely to be deprecated in an upcoming major version. Using draft this way does not require you to call it as a function.
71 |
72 | ### Example:
73 |
74 | ```js
75 | import passable, { enforce } from 'passable';
76 |
77 | passable('NewUserForm', (test, draft) => {
78 | test('username', 'Must be a string between 2 and 10 chars', () => {
79 | enforce(data.username).isString().longerThan(1).shorterThan(11);
80 | });
81 |
82 | if (!draft.hasErrors('username')) {
83 | // if the username did not pass the previous test, the following test won't run
84 | test('username', 'already exists', fetch(`/check_availability?username=${data.username}`));
85 | }
86 | });
87 | ```
88 |
--------------------------------------------------------------------------------
/docs/getting_started/callbacks.md:
--------------------------------------------------------------------------------
1 | # Passable callbacks
2 |
3 | Passable commes with multiple callback functions that allow you to interact with async tests.
4 |
5 | # `.after()`
6 |
7 | > Since 7.0.0
8 |
9 | The after callback is a function that can be chained to a passable suite and allows invoking a callback whenever a certain field has finished running, regardless of whether it passed or failed. It accepts two arguments: `fieldName` and `callback`. You may chain multiple callbacks to the same field.
10 |
11 | When running, the `.after()` function passes down to its callback argument the current result object, **note** it might not be final yet, as there may be other async fields still being processed.
12 |
13 | ```js
14 | import passable, { enforce } from 'passable';
15 | const email_regex = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}/gi;
16 |
17 | passable('SendEmailForm', (test) => {
18 |
19 | test('UserEmail', 'must be a valid email', () => {
20 | enforce(data.email).matches(email_email_regex);
21 | });
22 |
23 | test('UserName', 'must not be blacklisted', new Promise((resolve, reject) => {
24 | fetch(`/isBlacklisted?name=${data.name}`).then(...).then((data) => {
25 | if (data.blackListed) {
26 | reject();
27 | } else {
28 | resolve();
29 | }
30 | });
31 | }));
32 |
33 | test('Content', 'must be between 5 and 500 chars', () => {
34 | enforce(data.content).longerThan(4).shorterThan(501);
35 | });
36 | }).after((res) => {
37 | if (res.hasErrors('username')) {
38 | showUserNameErrors(res.errors)
39 | }
40 | });
41 | ```
42 |
43 | # `.done()`
44 |
45 | > Since 6.1.0
46 |
47 | The `.done()` callback is a function that can be chained to a passable suite. It accepts a function to be run whenever the suite completes running all [tests](../test/index.md) (both sync, and async - if present), regardless of whether they passed or [failed](../test/how_to_fail.md).
48 |
49 | `.done()` calls can be infinitely chained to one another, and as the passable suite completes - they will all run synchronously - meaning that if there is an async action being performed in one of the callbacks, the next `.done()` call will *not* wait for it to complete before starting.
50 |
51 | When running, the `done()` function passes down to its callback function the final passable result object, so you do not have to store it in an external variable.
52 |
53 | ```js
54 | import passable, { enforce } from 'passable';
55 | const email_regex = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}/gi;
56 |
57 | passable('SendEmailForm', (test) => {
58 |
59 | test('UserEmail', 'must be a valid email', () => {
60 | enforce(data.email).matches(email_email_regex);
61 | });
62 |
63 | test('UserName', 'must not be blacklisted', new Promise((resolve, reject) => {
64 | fetch(`/isBlacklisted?name=${data.name}`).then(...).then((data) => {
65 | if (data.blackListed) {
66 | reject();
67 | } else {
68 | resolve();
69 | }
70 | });
71 | }));
72 |
73 | test('Content', 'must be between 5 and 500 chars', () => {
74 | enforce(data.content).longerThan(4).shorterThan(501);
75 | });
76 | }).done((res) => {
77 | if (res.hasErrors()) {
78 | showValidationErrors(res.errors)
79 | }
80 | }).done(reportToServer).done(promptUserQuestionnaire);
81 | ```
82 |
83 | # `.cancel()`
84 |
85 | > Since 7.0.0
86 |
87 | When running your validation suite multiple times in a short amount of time - for example, when validating user inputs upon change, your async validations may finish after you already started running the suite again. This will cause the `.done()` and `.after()` callbacks of the previous run to be run in proximity to the `.done()` and `.after()` callbacks of the current run.
88 |
89 | Depending on what you do in your callbacks, this can lead to wasteful action, or to validation state rapidly changing in front of the user's eyes.
90 |
91 | To combat this, there's the `.cancel()` callback, which cancels any pending `.done()` and `.after()` callbacks.
92 |
93 | You can use it in many ways, but the simplistic way to look at it is this: You need to keep track of your cancel callback in a scope that's still going to be accessible in the next run.
94 |
95 | Example:
96 |
97 | ```js
98 | let cancel = null;
99 |
100 | // this is a simple event handler
101 | const handleChange = (e) => {
102 |
103 | if (cancel) {
104 | cancel(); // now, if cancel already exists, it will cancel any pending callbacks
105 | }
106 |
107 | // you should ideally import your suite from somewhere else, this is here just for the demonstration
108 | const result = passable('MyForm', (test) => {
109 | // some async validations go here
110 | });
111 |
112 | result.done((res) => {
113 | // do something
114 | });
115 |
116 | cancel = result.cancel; // save the cancel callback aside
117 | }
118 | ```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | All notable changes to this project will be documented in this file.
3 |
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6 |
7 | ## [7.5.0] - 2019-11-12
8 |
9 | ### Additions
10 | - 37ed4d7 [Minor] Add draft as an export from passable (#204) (ealush)
11 | - aff8c86 [Minor] Create a Passable singleton | future hook support (#213) (ealush)
12 |
13 | ## [7.4.0] - 2019-11-04
14 |
15 | ### Additions
16 | - c0a9aec [MINOR] Add return value for test. (#197) (Evyatar Alush)
17 |
18 | ### Fixes and non breaking changes
19 | - 7ec4daf Use anyone/any instead of any (#198) (Evyatar Alush)
20 |
21 | ## [7.3.0] - 2019-09-09
22 |
23 | ### Additions
24 | - d0e88e8 [MINOR] Allow Failing With Rejection Message On Async Tests (Micha Sherman)
25 |
26 | ## [7.2.4] - 2019-08-17
27 |
28 | ### Fixed
29 | - [PATCH] Lookup Proxy on global namespace
30 |
31 | ## [7.2.2] - 2019-08-12
32 |
33 | ### Changed
34 | - [Docs] Improved Enforce documentation
35 | - [Patch] Use n4s npm package
36 |
37 | ## [7.2.0] - 2019-08-03
38 |
39 | ### Added
40 | - [Minor] Add isEven rule.
41 | - [Minor] Add isOdd rule.
42 |
43 | ### Changed
44 | - [Patch] Make result callback functions non-enumerable
45 |
46 |
47 | ## [7.1.1] - 2019-06-24
48 |
49 | ### Added
50 | - [Patch] Add type definitions for cancel callback.
51 |
52 | ## [7.1.0] - 2019-06-23
53 |
54 | ### Added
55 | - [Minor] Added an option to directly import `test` from passable;
56 | - [Minor] `any` utility for assertions with `OR` `||` relationships.
57 |
58 | ## [7.0.0] - 2019-06-11
59 |
60 | ### Changed
61 | - [Major] Lowercased library name when imported on global object.
62 | - [Major] Renamed `validationErrors` and `validationWarnings` output properties to `errors` and `warnings`.
63 | - [Patch] Guarantee that `.done()` callbacks only run once.
64 |
65 | ### Added
66 | - [Minor] `.after()` callback that can run after a specific field finished execution.
67 | - [Minor] New size rules (`lessThan`, `greaterThan`, `lessThanOrEquals`, `greaterThanOrEquals`, `numberEquals`, `numberNotEquals`).
68 | - [Minor] New size rules (`longerThan`, `shorterThan`, `longerThanOrEquals`, `shorterThanOrEquals`, `lengthEquals`, `lengthNotEquals`).
69 | - [Minor] New content rules (`equals`, `notEquals`).
70 | - [Minor] Support returning promise from test callback for async tests.
71 | - [Minor] Add cancel callback.
72 |
73 | ### Removed
74 | - [Major] Removed output properties: `hasValidationErrors`, `hasValidationWarnings`.
75 | - [Major] Removed compound rules (`anyOf`, `allOf`, `noneOf`).
76 | - [Major] Removed general size rules (`smallerThan`, `largerThan`, `smallerThanOrEquals`, `largerThanOrEquals`, `sizeEquals`, `sizeNotEquals`).
77 |
78 | ## [6.3.4] - 2019-05-01
79 |
80 | ### Changed
81 | - [Patch] Reduce bundle size
82 |
83 | ## [6.3.1] - 2019-02-22
84 |
85 | ### Added
86 | - [Patch] Fix documentation typo (#107)
87 | - [Patch] Add type hints to passable (#106 + #107)
88 |
89 | ## [6.3.0] - 2019-01-06
90 |
91 | ### Added
92 | - [Minor] access intermediate validation result via draft (#103)
93 | - [Patch] Publish next tag separately from feature branch (#104)
94 | - [Minor] add hasErrors and hasWarnings functions (#105)
95 |
96 | ## [6.2.0] - 2018-12-27
97 |
98 | ### Added
99 | - [Minor] mark field as async
100 |
101 | ## [6.1.3] - 2018-12-27
102 |
103 | ### Fixed
104 | - [PATCH] run done callback in sync mode
105 |
106 | ## [6.1.0] - 2018-10-20
107 |
108 | ### Added
109 | - [Minor] Async test support + done callback chaining (#100)
110 |
111 | ### Changed
112 | - [Patch] Update github configuration
113 |
114 | ## [6.0.0] - 2018-04-16
115 |
116 | ### Added
117 | - [Major] Extract Enforce as standalone API
118 | - [Patch] Create `Specific` Object
119 | - [Patch] Auto deploy next branch
120 | - [Minor] Add isNumeric check
121 | - [Minor] Initialize proxy with all keys. Add support for proxy-polyfill
122 | - [Minor] Negative rules support
123 | - [Patch] Use proxy polyfill when proxy is not supported
124 | - [Minor] Add isTruthy and isFalsy enforce rules
125 |
126 | ### Changed
127 | - [Major] `specific` field filter: Introduce `only` and `not`
128 | - [Major] Refactor rules to follow convention (Size compares against number only)
129 | - [Major] Pass - move to be the last argument
130 | - [Patch] Rename `pass` to `test`
131 | - [Major] Move specific to last argument, make optional
132 |
133 | ### Removed
134 | - [Major] Make validate leaner by removing `message` arg
135 |
136 | ### Fixed
137 | - [Patch] Type-checking errors ignores
138 |
139 | ## [5.10.3] - 2018-01-29
140 |
141 | ### Added
142 | - [Minor] Validate function
143 | - [PATCH] Commonjs require support for enforce
144 | - [Minor] Add default expect=true to rules
145 | - [Minor] export enforce as a standalone module
146 | - [Minor] Allow chaining rules directly under enforce
147 |
--------------------------------------------------------------------------------
/docs/getting_started/result.md:
--------------------------------------------------------------------------------
1 | # The result object
2 |
3 | ## Properties
4 | | Name | Type | Description |
5 | |----------------------------------|------------|-----------------------------------------------------|
6 | | `name` | `String` | The name of the form being validated
7 | | `failCount` | `Number` | Overall errors count for this form
8 | | `warnCount` | `Number` | Overall warnings count for this form
9 | | `testCount` | `Number` | Overall test count in this form
10 | | `testsPerformed` | `Object{}` | Detailed stats per field (structure detailed below)
11 | | `errors` | `Object[]` | Actual errors per each field
12 | | `errors[field-name]` | `Object[]` | All error strings for this field
13 | | `warnings` | `Object[]` | Actual errors per each field
14 | | `warnings[field-name]` | `Object[]` | All warning strings for this field
15 | | `skipped` | `Array` | All skipped fields (empty, unless the `specific` option is used)
16 | | `getErrors` | `Function` | Getter function which allows accessing the errors array of one or all fields
17 | | `getWarnings` | `Function` | Getter function which allows accessing the warnings array of one or all fields
18 | | `hasErrors` | `Function` | Returns whether a certain field (or the whole suite, if no field passed) has errors
19 | | `hasWarnings` | `Function` | Returns whether a certain field (or the whole suite, if no field passed) has warnings
20 |
21 | ### `testsPerformed` field structure
22 | | Name | Type | Description |
23 | |-------------|----------|---------------------------------------|
24 | | `failCount` | `Number` | Overall errors count for this field |
25 | | `warnCount` | `Number` | Overall warnings count for this field |
26 | | `testCount` | `Number` | Overall test count in this field |
27 |
28 | ## `hasErrors` and `hasWarnings` functions
29 | > since 6.3.0
30 |
31 | If you only need to know if a certain field has validation errors or warnings but don't really care which they are, you can use `hasErrors` or `hasWarnings` functions.
32 |
33 | ```js
34 | resultObject.hasErrors('username');
35 | // true
36 |
37 | resultObject.hasWarnings('password');
38 | // false
39 | ```
40 |
41 | In case you want to know whether the whole suite has errors or warnings, you can use the same functions, just without specifying a field
42 |
43 | ```js
44 | resultObject.hasErrors();
45 | // true
46 |
47 | resultObject.hasWarnings();
48 | // true
49 | ```
50 |
51 | ## `getErrors` and `getWarnings` functions
52 | > since 5.10.0
53 |
54 | You can easily traverse the object tree to acess the field errors and warnings, but when accessing many fields, it can get pretty messy:
55 |
56 | ```js
57 | resultObject.errors.myField && resultObject.errors.myField[0];
58 | ```
59 | This is clearly not ideal. There is a shortcut to getting to a specific field:
60 |
61 | ```js
62 | resultObject.getErrors('username');
63 | // ['Error string 1', `Error string 2`]
64 |
65 | resultObject.getWarnings('password');
66 | // ['Warning string 1', `Warning string 2`]
67 | ```
68 |
69 | If there are no errors for the field, the function returns an empty array:
70 | ```js
71 | resultObject.getErrors('username');
72 | // []
73 |
74 | resultObject.getWarnings('username');
75 | // []
76 | ```
77 |
78 | ## Why isn't there an `isValid` prop?
79 | There is **no** `isValid` prop, this is by design. Passable cannot know your business logic, nor can it ever assume that `0` errors means valid result. `0` errors can be due to skipped fields. Same goes for isInvalid. Even though, usually, errors mean invalidity, it is not always the case. This is why Passable gives you all the information about the tests, but it is your job to decide whether it means that the validation failed or not.
80 |
81 | ## Passing Example
82 | ```js
83 | {
84 | "name": "NewUserForm",
85 | "failCount": 0,
86 | "warnCount": 0,
87 | "testCount": 2,
88 | "testsPerformed": {
89 | "username": {
90 | "testCount": 1,
91 | "failCount": 0,
92 | "warnCount": 0
93 | },
94 | "age": {
95 | "testCount": 1,
96 | "failCount": 0,
97 | "warnCount": 0
98 | }
99 | },
100 | "errors": {},
101 | "warnings": {},
102 | "skipped": []
103 | }
104 | ```
105 |
106 | ## Failing Example
107 | ```js
108 | {
109 | "name": "NewUserForm",
110 | "failCount": 2,
111 | "warnCount": 0,
112 | "testCount": 2,
113 | "testsPerformed": {
114 | "username": {
115 | "testCount": 1,
116 | "failCount": 1,
117 | "warnCount": 0
118 | },
119 | "age": {
120 | "testCount": 1,
121 | "failCount": 1,
122 | "warnCount": 0
123 | }
124 | },
125 | "errors": {
126 | "username": [
127 | "Must be a string between 2 and 10 chars"
128 | ],
129 | "age": [
130 | "Can either be empty, or larger than 18"
131 | ]
132 | },
133 | "warnings": {},
134 | "skipped": []
135 | }
136 | ```
137 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Passable
2 | Passable is an Isomorphic library which may run either on the server, or in the browser (or both), and it should be treated as such. Internal modules should not rely on any runtime specific environment, or assume any exists.
3 |
4 | It provides a declarative API for writing user input validations. When adding new consumer-facing APIs, they should keep the same declarative 'style and spirit'.
5 |
6 | ## Branching guidelines
7 | Our working branch is Master. New versions are merged to, and are published from it. Apart from master, we use a development branch called `next`. `next` is an accumulative branch, collecting all changes waiting to be published. **next pull requests are not merged directly to master**, but to `next` instead.
8 | This also means that when working on a new feature, we branch off from `next` and not from master.
9 |
10 | For urgent changes and bugfixes that cannot be wait until the next version, we open a `hotfix` branch (for example: `hotfix-fix-all-bugs`). It should branch off directly from master, and be merged back directly to master.
11 |
12 | Our `next` branch needs to be constantly updated from master so current fixes and configuration changes are present there as well.
13 |
14 | | Branch Name | Purpose |
15 | |-------------|:----------------------------------------------------------------------------------------|
16 | | master | Working branch. Ready to publish versions go here |
17 | | next | Development branch, accumulates future changes |
18 | | hotfix | Descriptive term for all hotfix branches that cannot wait until the next version update |
19 |
20 | ## Version management
21 | The version number specify `major`.`minor`.`patch`
22 |
23 | |Type | Content | Example |
24 | |------|:--------|:--------|
25 | |patch | Internal fix | Bug fix, Performance improvements, tests, small tweaks|
26 | |minor | Interface change with full backward compatibility | Adding new features, Full backwards compatibility|
27 | |major | Interface change without full backward compatibility | Changing a function name or interface, Removing a function|
28 |
29 | ## Commit message guidelines
30 | Commit messages should be labeled with the version bump the changes require. For example:
31 |
32 | * > [Patch] Fixing a broken runner
33 | * > [Patch] Documentation update
34 | * > [Patch] Configuration update
35 | * > [Minor] Adding a new runner
36 | * > [Major] Replacing all runners
37 |
38 | This is to make it easier to determine which version bump should be done upon adding a new release.
39 |
40 | ## Documentation guidelines
41 | Passable's documentation is present in [https://fiverr.github.io/passable](https://fiverr.github.io/passable). Its source is present as markdown(`.md`) files in the `documentation` folder of the project. Upon releasing a new version, a static html website is generated using [docpress](https://github.com/docpress/docpress). When adding a new page to the documentation, it needs to be added the the `content` first, which is present under: `/documentation/README.md`. Otherwise docpress will not know to look for it.
42 |
43 | When modifying existing API or adding new functionality, it is important to update the documentation as well.
44 |
45 | ## Testing
46 | There are two kinds of tests for Passable's code. All are either named or suffixed with `spec.js`.
47 | ### Unit tests
48 | Unit tests run over the source directory. These tests check each of Passable's internal interfaces.
49 | When testing a module, its spec file should be in the same directory, named `spec.js`:
50 | ```
51 | .
52 | ├── module_name
53 | │ ├── index.js
54 | │ ├── spec.js
55 | ```
56 | When testing a broader part of the api, a filename should be more descriptive:
57 | ```
58 | .
59 | ├── spec
60 | │ ├── passable.api.custom.spec.js
61 | ```
62 | the All new code has to be tested.
63 |
64 | ### Usecase tests
65 | Which test full Passable usecases, and run both on the source and distribution versions of Passable. When adding new functionality to Passable, make sure to add a corresponding usecase test.
66 | If your changes are not reflected in the usecase tests, it is probably due to you having the old distribution file in your branch. Please run:
67 | ```js
68 | npm run build
69 | // or
70 | yarn build
71 | ```
72 |
73 | Usecase tests are located under:
74 | ```
75 | .
76 | ├── spec
77 | │ ├── usecase
78 | │ ├── passable.usecase.spec.js
79 | │ ├── usecase_a.js
80 | │ ├── usecase_b.js
81 | ```
82 |
83 | To run all tests:
84 | ```js
85 | npm test
86 | // or
87 | yarn test
88 | ```
89 |
90 | ## Linting
91 | Linting is done via eslint and Fiverr's eslint configuration.
92 |
93 | ```js
94 | npm run eslint
95 | // or
96 | yarn eslint
97 | ```
98 |
99 | ### Playground
100 | The "playground" directory is a place you can write test validations, and include __Passable__ as a module.
101 | It's node_modules contains a symlink to the parent directory so you're working with your actual work files.
102 |
103 | The content of this directory is gitignored (short of example file and __Passable__ virtual module) so feel free to add your sandbox files there
104 |
--------------------------------------------------------------------------------
/src/spec/passable.api.specific.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | const runSpec = (passable) => {
4 | describe('Test running specific tests', () => {
5 | describe('Test array input', () => {
6 |
7 | it('Should only run first test', () => {
8 | const result = specificTests(['First']);
9 |
10 | expect(result.skipped).to.deep.equal(['Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']);
11 | expect(result.testCount).to.equal(1);
12 | });
13 |
14 | it('Should run Second, Third and Seventh Test', () => {
15 | const result = specificTests(['Second', 'Third', 'Seventh']);
16 |
17 | expect(result.skipped).to.deep.equal(['First', 'Fourth', 'Fifth', 'Sixth']);
18 | expect(result.testCount).to.equal(3);
19 | });
20 |
21 | it('Should skip all tests', () => {
22 | const result = specificTests(['IDoNotExist']);
23 |
24 | expect(result.skipped).to.deep.equal(['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']);
25 | expect(result.testCount).to.equal(0);
26 | });
27 |
28 | it('Should run all tests', () => {
29 | const result = specificTests([]);
30 |
31 | expect(result.skipped).to.deep.equal([]);
32 | expect(result.testCount).to.equal(8);
33 | });
34 | });
35 |
36 | describe('Test string input', () => {
37 |
38 | it('Should only run first test', () => {
39 | const result = specificTests('First');
40 |
41 | expect(result.skipped).to.deep.equal(['Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']);
42 | expect(result.testCount).to.equal(1);
43 | });
44 |
45 | it('Should skip all tests', () => {
46 | const result = specificTests('IDoNotExist');
47 |
48 | expect(result.skipped).to.deep.equal(['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']);
49 | expect(result.testCount).to.equal(0);
50 | });
51 |
52 | it('Should run all tests', () => {
53 | const result = specificTests('');
54 |
55 | expect(result.skipped).to.deep.equal([]);
56 | expect(result.testCount).to.equal(8);
57 | });
58 | });
59 |
60 | describe('Test object input', () => {
61 |
62 | it('Should only run first test', () => {
63 | const result = specificTests({
64 | only: 'First'
65 | });
66 |
67 | expect(result.skipped).to.deep.equal(['Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']);
68 | expect(result.testCount).to.equal(1);
69 | });
70 |
71 | it('Should run Second, Third and Seventh Test', () => {
72 | const result = specificTests({
73 | only: ['Second', 'Third', 'Seventh']
74 | });
75 |
76 | expect(result.skipped).to.deep.equal(['First', 'Fourth', 'Fifth', 'Sixth']);
77 | expect(result.testCount).to.equal(3);
78 | });
79 |
80 | it('Should skip all tests', () => {
81 | const result = specificTests({
82 | only: 'IDoNotExist'
83 | });
84 |
85 | expect(result.skipped).to.deep.equal(['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']);
86 | expect(result.testCount).to.equal(0);
87 | });
88 |
89 | it('Should skip Second and Fourth', () => {
90 | const result = specificTests({
91 | not: ['Second', 'Fourth']
92 | });
93 |
94 | expect(result.skipped).to.deep.equal(['Second', 'Fourth']);
95 | expect(result.testCount).to.equal(6);
96 | });
97 |
98 | it('Should skip First', () => {
99 | const result = specificTests({
100 | not: 'First'
101 | });
102 |
103 | expect(result.skipped).to.deep.equal(['First']);
104 | expect(result.testCount).to.equal(7);
105 | });
106 | });
107 |
108 | it('Should run all tests', () => {
109 | const result = specificTests(null);
110 |
111 | expect(result.skipped).to.deep.equal([]);
112 | expect(result.testCount).to.equal(8);
113 | });
114 | });
115 |
116 | function specificTests(specific) {
117 | return passable('specificTests', (test) => {
118 | test('First', 'should pass', () => true);
119 | test('Second', 'should pass', () => true);
120 | test('Third', 'should fail', () => false);
121 | test('Fourth', 'should fail', () => false);
122 | test('Fifth', 'should fail', () => false);
123 | test('Sixth', 'should pass', () => true);
124 | test('Sixth', 'should pass', () => true); // twice!
125 | test('Seventh', 'should pass', () => true);
126 | }, specific);
127 | };
128 |
129 | };
130 |
131 | runSpec(require('../'));
132 | runSpec(require('../../dist/passable'));
133 | runSpec(require('../../dist/passable.min.js'));
134 |
--------------------------------------------------------------------------------
/src/core/Specific/spec.js:
--------------------------------------------------------------------------------
1 | import Specific from './index';
2 | import { expect } from 'chai';
3 |
4 | describe('Test Specific class constructor', () => {
5 | describe('Test default value fallback', () => {
6 |
7 | const defaultObject = {};
8 |
9 | it('Should return default object when no args passed', () => {
10 | expect(new Specific()).to.deep.equal({});
11 | });
12 |
13 | it('Should return default object when specific is explicitly null', () => {
14 | expect(new Specific(null)).to.deep.equal(defaultObject);
15 | });
16 |
17 | it('Should return default object when specific is an empty string', () => {
18 | expect(new Specific('')).to.deep.equal(defaultObject);
19 | });
20 |
21 | it('Should return default object when specific is an empty array', () => {
22 | expect(new Specific([])).to.deep.equal(defaultObject);
23 | });
24 |
25 | it('Should return default object when specific is of wrong type', () => {
26 | expect(() => new Specific(new Set())).to.throw(TypeError);
27 | expect(() => new Specific(new Map())).to.throw(TypeError);
28 | expect(() => new Specific(55)).to.throw(TypeError);
29 | expect(() => new Specific(true)).to.throw(TypeError);
30 | });
31 | });
32 |
33 | describe('Test legacy api', () => {
34 | it('Should store array values in `only`', () => {
35 | expect(new Specific(['field_1', 'field_2'])).to.deep.equal({
36 | only: {
37 | field_1: true,
38 | field_2: true
39 | }
40 | });
41 | });
42 |
43 | it('Should store string value in `only`', () => {
44 | expect(new Specific('field_1')).to.deep.equal({
45 | only: {
46 | field_1: true
47 | }
48 | });
49 | });
50 | });
51 |
52 | describe('Test Object input', () => {
53 | it('Should add `only` array values to `only` object', () => {
54 | expect(new Specific({
55 | only: ['f1', 'f2']
56 | })).to.deep.equal({
57 | only: {
58 | f1: true,
59 | f2: true
60 | }
61 | });
62 | });
63 |
64 | it('Should add `not` array values to `not` object', () => {
65 | expect(new Specific({
66 | not: ['f1', 'f2']
67 | })).to.deep.equal({
68 | not: {
69 | f1: true,
70 | f2: true
71 | }
72 | });
73 | });
74 |
75 | it('Should add array values to correct object', () => {
76 | expect(new Specific({
77 | only: ['f1', 'f2'],
78 | not: ['f3', 'f4']
79 | })).to.deep.equal({
80 | only: {
81 | f1: true,
82 | f2: true
83 | },
84 | not: {
85 | f3: true,
86 | f4: true
87 | }
88 | });
89 | });
90 |
91 | it('Should add `only` string values to `only` object', () => {
92 | expect(new Specific({
93 | only: 'f1'
94 | })).to.deep.equal({
95 | only: {
96 | f1: true
97 | }
98 | });
99 | });
100 |
101 | it('Should add `not` string values to `not` object', () => {
102 | expect(new Specific({
103 | not: 'f1'
104 | })).to.deep.equal({
105 | not: {
106 | f1:true
107 | }
108 | });
109 | });
110 |
111 | it('Should add string values to correct object', () => {
112 | expect(new Specific({
113 | only: 'f1',
114 | not: 'f3'
115 | })).to.deep.equal({
116 | only: {
117 | f1: true
118 | },
119 | not: {
120 | f3: true
121 | }
122 | });
123 | });
124 | });
125 | });
126 |
127 | describe('Test `is` (specific) function', () => {
128 | describe('Test truthy returns', () => {
129 | it('Should return true for an empty array', () => {
130 | expect(Specific.is([])).to.equal(true);
131 | });
132 |
133 | it('Should return true for an empty string', () => {
134 | expect(Specific.is('')).to.equal(true);
135 | });
136 |
137 | it('Should return true for an Object with `only`', () => {
138 | expect(Specific.is({only: ''})).to.equal(true);
139 | });
140 |
141 | it('Should return true for an Object with `not`', () => {
142 | expect(Specific.is({not: ''})).to.equal(true);
143 | });
144 |
145 | it('Should return true for an array of strings', () => {
146 | expect(Specific.is(['a', 'b', 'c'])).to.equal(true);
147 | });
148 |
149 | it('Should return true for a string', () => {
150 | expect(Specific.is('a')).to.equal(true);
151 | });
152 | });
153 |
154 | describe('Test falsy returns', () => {
155 | it('Should return false for an array of mixed types', () => {
156 | expect(Specific.is([1, 'f2'])).to.equal(false);
157 | });
158 |
159 | it('Should return false for a number', () => {
160 | expect(Specific.is(55)).to.equal(false);
161 | });
162 |
163 | it('Should return false for a boolean', () => {
164 | expect(Specific.is(true)).to.equal(false);
165 | });
166 |
167 | it('Should return false for an Object without `not` or `only`', () => {
168 | expect(Specific.is({})).to.equal(false);
169 | });
170 |
171 | it('Should return false for null', () => {
172 | expect(Specific.is(null)).to.equal(false);
173 | });
174 |
175 | it('Should return false for undefined', () => {
176 | expect(Specific.is(null)).to.equal(false);
177 | });
178 | });
179 | });
--------------------------------------------------------------------------------
/src/core/draft/spec.js:
--------------------------------------------------------------------------------
1 | import Mocha from 'mocha';
2 | import faker from 'faker';
3 | import { ERROR_NO_CONTEXT } from './constants';
4 |
5 | const runSpec = (passable) => {
6 | let suite;
7 | const { WARN } = passable;
8 |
9 | const createSuite = (tests) => {
10 | suite = passable(faker.random.word(), tests);
11 | };
12 |
13 | describe('Draft', () => {
14 |
15 | it('Should be `tests` second argument', (done) => {
16 | createSuite((test, draft) => {
17 | setTimeout(() => {
18 | expect(draft).to.deep.equal(suite);
19 | done();
20 | }, 10);
21 | });
22 | });
23 |
24 | it('Should be exposed as a function from passable', () => {
25 | createSuite((test, draft) => {
26 | expect(passable.draft()).to.equal(draft);
27 | });
28 | });
29 |
30 | it('Should contain intermediate test result', () => {
31 | // This test is so long because it tests `draft` throughout
32 | // a suite's life cycle, both as an argument, and as an import
33 | createSuite((test, draft) => {
34 | expect(draft.testCount).to.equal(0);
35 | expect(passable.draft().testCount).to.equal(0);
36 | expect(draft.failCount).to.equal(0);
37 | expect(passable.draft().failCount).to.equal(0);
38 | expect(draft.warnCount).to.equal(0);
39 | expect(passable.draft().warnCount).to.equal(0);
40 | expect(draft.hasErrors()).to.equal(false);
41 | expect(passable.draft().hasErrors()).to.equal(false);
42 | expect(draft.hasWarnings()).to.equal(false);
43 | expect(passable.draft().hasWarnings()).to.equal(false);
44 | expect(draft.skipped).to.deep.equal([]);
45 | expect(passable.draft().skipped).to.deep.equal([]);
46 |
47 | expect(draft.hasErrors('field1')).to.equal(false);
48 | expect(passable.draft().hasErrors('field1')).to.equal(false);
49 | test('field1', 'message', () => expect(1).to.equal(2));
50 | expect(draft.testCount).to.equal(1);
51 | expect(passable.draft().testCount).to.equal(1);
52 | expect(draft.failCount).to.equal(1);
53 | expect(passable.draft().failCount).to.equal(1);
54 | expect(draft.warnCount).to.equal(0);
55 | expect(passable.draft().warnCount).to.equal(0);
56 | expect(draft.hasErrors()).to.equal(true);
57 | expect(passable.draft().hasErrors()).to.equal(true);
58 | expect(draft.hasErrors('field1')).to.equal(true);
59 | expect(passable.draft().hasErrors('field1')).to.equal(true);
60 | expect(draft.hasWarnings()).to.equal(false);
61 | expect(passable.draft().hasWarnings()).to.equal(false);
62 |
63 | test('field2', 'message', () => expect(2).to.equal(2));
64 | expect(draft.testCount).to.equal(2);
65 | expect(passable.draft().testCount).to.equal(2);
66 | expect(draft.failCount).to.equal(1);
67 | expect(passable.draft().failCount).to.equal(1);
68 | expect(draft.warnCount).to.equal(0);
69 | expect(passable.draft().warnCount).to.equal(0);
70 | expect(draft.hasErrors()).to.equal(true);
71 | expect(passable.draft().hasErrors()).to.equal(true);
72 | expect(draft.hasWarnings()).to.equal(false);
73 | expect(passable.draft().hasWarnings()).to.equal(false);
74 |
75 | expect(draft.hasWarnings('field3')).to.equal(false);
76 | expect(passable.draft().hasWarnings('field3')).to.equal(false);
77 | test('field3', 'message', () => expect(2).to.equal(1), WARN);
78 | expect(draft.testCount).to.equal(3);
79 | expect(passable.draft().testCount).to.equal(3);
80 | expect(draft.failCount).to.equal(1);
81 | expect(passable.draft().failCount).to.equal(1);
82 | expect(draft.warnCount).to.equal(1);
83 | expect(passable.draft().warnCount).to.equal(1);
84 | expect(draft.hasErrors()).to.equal(true);
85 | expect(passable.draft().hasErrors()).to.equal(true);
86 | expect(draft.hasWarnings()).to.equal(true);
87 | expect(passable.draft().hasWarnings()).to.equal(true);
88 | expect(draft.hasWarnings('field3')).to.equal(true);
89 | expect(passable.draft().hasWarnings('field3')).to.equal(true);
90 |
91 | test('field4', 'message', Promise.resolve(), WARN);
92 | expect(draft.testCount).to.equal(4);
93 | expect(passable.draft().testCount).to.equal(4);
94 | expect(draft.failCount).to.equal(1);
95 | expect(passable.draft().failCount).to.equal(1);
96 | expect(draft.warnCount).to.equal(1);
97 | expect(passable.draft().warnCount).to.equal(1);
98 | expect(draft.hasErrors()).to.equal(true);
99 | expect(passable.draft().hasErrors()).to.equal(true);
100 | expect(draft.hasWarnings()).to.equal(true);
101 | expect(passable.draft().hasWarnings()).to.equal(true);
102 | expect(draft.hasWarnings('field4')).to.equal(false);
103 | expect(passable.draft().hasWarnings('field4')).to.equal(false);
104 | });
105 | });
106 | });
107 |
108 | describe('When called outside of a running suite', () => {
109 | let _uncaughtListeners;
110 |
111 | beforeEach(() => {
112 | _uncaughtListeners = process.listeners('uncaughtException');
113 | process.removeAllListeners('uncaughtException');
114 | });
115 |
116 | afterEach(() => {
117 | _uncaughtListeners.forEach((listener) => process.on('uncaughtException', listener));
118 | });
119 |
120 | it('Should throw an error', (done) => {
121 | process.on('uncaughtException', (err) => {
122 | expect(err.message).to.have.string(ERROR_NO_CONTEXT);
123 | process.removeAllListeners('uncaughtException');
124 | done();
125 | });
126 | passable.draft();
127 | });
128 | });
129 | };
130 |
131 | runSpec(require('../../'));
132 | runSpec(require('../../../dist/passable'));
133 | runSpec(require('../../../dist/passable.min.js'));
134 |
--------------------------------------------------------------------------------
/src/core/passableResult/index.js:
--------------------------------------------------------------------------------
1 | import { WARN, FAIL } from '../../constants';
2 | const severities = [ WARN, FAIL ];
3 |
4 | const passableResult = (name) => {
5 |
6 | const completionCallbacks = [];
7 | let asyncObject = null;
8 | let hasValidationErrors = false;
9 | let hasValidationWarnings = false;
10 | let cancelled = false;
11 |
12 | /**
13 | * Initializes specific field's counters
14 | * @param {string} fieldName - The name of the field.
15 | */
16 | const initFieldCounters = (fieldName) => {
17 | if (output.testsPerformed[fieldName]) { return output; }
18 |
19 | output.testsPerformed[fieldName] = {
20 | testCount: 0,
21 | failCount: 0,
22 | warnCount: 0
23 | };
24 | };
25 |
26 | /**
27 | * Bumps test counters to indicate tests that's being performed
28 | * @param {string} fieldName - The name of the field.
29 | */
30 | const bumpTestCounter = (fieldName) => {
31 | if (!output.testsPerformed[fieldName]) { return output; }
32 |
33 | output.testsPerformed[fieldName].testCount++;
34 | output.testCount++;
35 | };
36 |
37 | /**
38 | * Bumps field's warning counts and adds warning string
39 | * @param {string} fieldName - The name of the field.
40 | * @param {string} statement - The error string to add to the object.
41 | */
42 | const bumpTestWarning = (fieldName, statement) => {
43 | hasValidationWarnings = true;
44 | output.warnings[fieldName] = output.warnings[fieldName] || [];
45 | output.warnings[fieldName].push(statement);
46 | output.warnCount++;
47 | output.testsPerformed[fieldName].warnCount++;
48 | };
49 |
50 | /**
51 | * Bumps field's error counts and adds error string
52 | * @param {string} fieldName - The name of the field.
53 | * @param {string} statement - The error string to add to the object.
54 | */
55 | const bumpTestError = (fieldName, statement) => {
56 | hasValidationErrors = true;
57 | output.errors[fieldName] = output.errors[fieldName] || [];
58 | output.errors[fieldName].push(statement);
59 | output.failCount++;
60 | output.testsPerformed[fieldName].failCount++;
61 | };
62 |
63 | /**
64 | * Fails a field and updates output accordingly
65 | * @param {string} fieldName - The name of the field.
66 | * @param {string} statement - The error string to add to the object.
67 | * @param {string} severity - Whether it is a `fail` or `warn` test.
68 | */
69 | const fail = (fieldName, statement, severity) => {
70 | if (!output.testsPerformed[fieldName]) { return output; }
71 | const selectedSeverity = severity && severities.includes(severity) ? severity : FAIL;
72 | selectedSeverity === WARN
73 | ? bumpTestWarning(fieldName, statement)
74 | : bumpTestError(fieldName, statement);
75 | };
76 |
77 | /**
78 | * Uniquely add a field to the `skipped` list
79 | * @param {string} fieldName - The name of the field.
80 | */
81 | const addToSkipped = (fieldName) => {
82 | !output.skipped.includes(fieldName) && output.skipped.push(fieldName);
83 | };
84 |
85 | /**
86 | * Runs completion callbacks aggregated by `done`
87 | * regardless of success or failure
88 | */
89 | const runCompletionCallbacks = () => {
90 | completionCallbacks.forEach((cb) => !cancelled && cb(output));
91 | };
92 |
93 | /**
94 | * Marks a field as async
95 | * @param {string} fieldName - The name of the field.
96 | */
97 | const markAsync = (fieldName) => {
98 | asyncObject = asyncObject || {};
99 | asyncObject[fieldName] = asyncObject[fieldName] || {};
100 | asyncObject[fieldName] = {
101 | done: false,
102 | callbacks: asyncObject[fieldName].callbacks || []
103 | };
104 | };
105 |
106 | /**
107 | * Marks an async field as done
108 | * @param {string} fieldName - The name of the field.
109 | */
110 | const markAsDone = (fieldName) => {
111 | if (!fieldName) {
112 | return runCompletionCallbacks();
113 | }
114 |
115 | if (asyncObject !== null && asyncObject[fieldName]) {
116 | asyncObject[fieldName].done = true;
117 |
118 | // run field callbacks set in `after`
119 | if (asyncObject[fieldName].callbacks) {
120 | asyncObject[fieldName].callbacks.forEach((callback) => !cancelled && callback(output));
121 | }
122 | }
123 | };
124 |
125 | /**
126 | * Registers callback functions to be run when test suite is done running
127 | * If current suite is not async, runs the callback immediately
128 | * @param {function} callback the function to be called on done
129 | * @return {object} output object
130 | */
131 | const done = (callback) => {
132 | if (typeof callback !== 'function') {return output;}
133 | if (!asyncObject) {
134 | callback(output);
135 | }
136 |
137 | completionCallbacks.push(callback);
138 |
139 | return output;
140 | };
141 |
142 | /**
143 | * Registers callback functions to be run when a certain field is done running
144 | * If field is not async, runs the callback immediately
145 | * @param {string} fieldName - The name of the field.
146 | * @param {function} callback the function to be called on done
147 | * @return {object} output object
148 | */
149 | const after = (fieldName, callback) => {
150 | if (typeof callback !== 'function') {
151 | return output;
152 | }
153 |
154 | asyncObject = asyncObject || {};
155 | if (!asyncObject[fieldName] && output.testsPerformed[fieldName]) {
156 | callback(output);
157 | } else if (asyncObject[fieldName]) {
158 | asyncObject[fieldName].callbacks = [...(asyncObject[fieldName].callbacks || []), callback];
159 | }
160 |
161 | return output;
162 | };
163 |
164 | /**
165 | * cancels done/after callbacks. They won't invoke when async operations complete
166 | */
167 | const cancel = () => {
168 | cancelled = true;
169 |
170 | return output;
171 | };
172 |
173 | /**
174 | * Gets all the errors of a field, or of the whole object
175 | * @param {string} fieldName - The name of the field.
176 | * @return {array | object} The field's errors, or all errors
177 | */
178 | const getErrors = (fieldName) => {
179 | if (!fieldName) {
180 | return output.errors;
181 | }
182 |
183 | if (output.errors[fieldName]) {
184 | return output.errors[fieldName];
185 | }
186 |
187 | return [];
188 | };
189 |
190 | /**
191 | * Gets all the warnings of a field, or of the whole object
192 | * @param {string} [fieldName] - The name of the field.
193 | * @return {array | object} The field's warnings, or all warnings
194 | */
195 | const getWarnings = (fieldName) => {
196 | if (!fieldName) {
197 | return output.warnings;
198 | }
199 |
200 | if (output.warnings[fieldName]) {
201 | return output.warnings[fieldName];
202 | }
203 |
204 | return [];
205 | };
206 |
207 | /**
208 | * Checks if a certain field (or the whole suite) has errors
209 | * @param {string} [fieldName]
210 | * @return {boolean}
211 | */
212 | const hasErrors = (fieldName) => {
213 | if (!fieldName) {
214 | return hasValidationErrors;
215 | }
216 |
217 | return Boolean(output.getErrors(fieldName).length);
218 | };
219 |
220 | /**
221 | * Checks if a certain field (or the whole suite) has warnings
222 | * @param {string} [fieldName] - The name of the field.
223 | * @return {boolean}
224 | */
225 | const hasWarnings = (fieldName) => {
226 | if (!fieldName) {
227 | return hasValidationWarnings;
228 | }
229 |
230 | return Boolean(output.getWarnings(fieldName).length);
231 | };
232 |
233 | const output = {
234 | name,
235 | failCount: 0,
236 | warnCount: 0,
237 | testCount: 0,
238 | testsPerformed: {},
239 | errors: {},
240 | warnings: {},
241 | skipped: []
242 | };
243 |
244 | Object.defineProperties(output, {
245 | hasErrors: {
246 | value: hasErrors,
247 | writable: true,
248 | configurable: true,
249 | enumerable: false
250 | },
251 | hasWarnings: {
252 | value: hasWarnings,
253 | writable: true,
254 | configurable: true,
255 | enumerable: false
256 | },
257 | getErrors: {
258 | value: getErrors,
259 | writable: true,
260 | configurable: true,
261 | enumerable: false
262 | },
263 | getWarnings: {
264 | value: getWarnings,
265 | writable: true,
266 | configurable: true,
267 | enumerable: false
268 | },
269 | done: {
270 | value: done,
271 | writable: true,
272 | configurable: true,
273 | enumerable: false
274 | },
275 | after: {
276 | value: after,
277 | writable: true,
278 | configurable: true,
279 | enumerable: false
280 | },
281 | cancel: {
282 | value: cancel,
283 | writable: true,
284 | configurable: true,
285 | enumerable: false
286 | }
287 | });
288 |
289 | return {
290 | initFieldCounters,
291 | bumpTestError,
292 | bumpTestWarning,
293 | bumpTestCounter,
294 | fail,
295 | addToSkipped,
296 | runCompletionCallbacks,
297 | markAsync,
298 | markAsDone,
299 | output
300 | };
301 | };
302 |
303 | export default passableResult;
304 |
--------------------------------------------------------------------------------
/src/core/test/spec.js:
--------------------------------------------------------------------------------
1 | import passable from '../passable';
2 | import test from '.';
3 | import { expect } from 'chai';
4 | import { FAIL, WARN } from '../../index';
5 | import { clone, noop, random } from 'lodash';
6 | import { lorem } from 'faker';
7 | import sinon from 'sinon';
8 |
9 | describe('Test Passables "test" function', () => {
10 | let output;
11 |
12 | describe('Supplied test callback is a function', () => {
13 | const allTests = [];
14 |
15 | beforeEach(() => {
16 | passable(lorem.word(), noop);
17 | allTests.length = 0;
18 |
19 | for (let i = 0; i < random(1, 15); i++) {
20 | allTests.push(() => null);
21 | }
22 | });
23 | });
24 |
25 | describe('Supplied test callback is a promise', () => {
26 | const allTests = [];
27 |
28 | beforeEach(() => {
29 | passable(lorem.word(), noop);
30 | allTests.length = 0;
31 |
32 | for (let i = 0; i < random(1, 15); i++) {
33 | allTests.push(Promise.resolve());
34 | }
35 | });
36 | });
37 |
38 | describe('Supplied test callback is not supported', () => {
39 |
40 | beforeEach(() => {
41 | passable(lorem.word(), noop);
42 | });
43 |
44 |
45 | [0, 1, [], [55], {}, false, true, null, undefined].forEach((testCb) => {
46 | it(`Should return without adding pending test for ${testCb}`, () => {
47 | output = passable(lorem.word(), () => {
48 | test(lorem.word(), lorem.sentence(), testCb);
49 | });
50 | expect(output.testCount).to.equal(0);
51 | });
52 | });
53 | });
54 |
55 | describe('Running test callback', () => {
56 | let allTests;
57 |
58 | beforeEach(() => {
59 | allTests = [];
60 | for (let i = 0; i < random(1, 15); i++) {
61 | allTests.push(sinon.spy());
62 | }
63 | passable(lorem.word(), () => allTests.forEach((t) => {
64 | test(lorem.word(), lorem.sentence(), t);
65 | }));
66 | });
67 |
68 | it('Should call all test callbacks', () => {
69 | allTests.forEach((fn) => {
70 | expect(fn.calledOnce).to.equal(true);
71 | });
72 | });
73 |
74 | it('Should bump test counters for each of the tests', () => {
75 | const count = random(1, 15);
76 |
77 | output = passable(lorem.word(), () => {
78 | for (let i = 0; i < count; i++) {
79 | test(lorem.word(), lorem.sentence(), noop);
80 | }
81 | });
82 |
83 | expect(output.testCount).to.equal(count);
84 | });
85 |
86 | describe('Test is async', () => {
87 |
88 | describe('When returning promise to test callback', () => {
89 | let f1, f2, f3, f4, f5, f6, output;
90 | const rejectionMessage = lorem.sentence();
91 |
92 | beforeEach(() => {
93 | f1 = lorem.word();
94 | f2 = lorem.word();
95 | f3 = lorem.word();
96 | f4 = lorem.word();
97 | f5 = lorem.word();
98 | f6 = lorem.word();
99 |
100 | const rejectLater = () => new Promise((res, rej) => {
101 | setTimeout(rej, 500);
102 | });
103 |
104 | output = passable(lorem.word(), (test) => {
105 | test(f1, f1, () => Promise.reject());
106 | test(f2, f2, () => new Promise((resolve, reject) => {
107 | setTimeout(reject, 200);
108 | }));
109 | test(f3, () => Promise.reject(lorem.word()));
110 | test(f3, () => Promise.reject(rejectionMessage));
111 | test(f4, f4, () => Promise.reject(rejectionMessage));
112 | test(f5, f5, () => new Promise((resolve) => {
113 | setTimeout(resolve, 100);
114 | }));
115 | test(f6, f6, async() => await rejectLater());
116 | });
117 | });
118 |
119 | it('Should fail for rejected promise', (done) => {
120 | [f1, f2, f3, f4, f6].forEach((field) =>
121 | expect(output.hasErrors(field)).to.equal(false)
122 | );
123 |
124 | setTimeout(() => {
125 | [f1, f2, f3, f4, f6].forEach((field) =>
126 | expect(output.hasErrors(field)).to.equal(true)
127 | );
128 |
129 | [f1, f2, f6].forEach((field) =>
130 | expect(output.getErrors(field)).to.include(field)
131 | );
132 |
133 | done();
134 | }, 550);
135 | });
136 |
137 | it('Should fail with rejection message when provided', (done) => {
138 | setTimeout(() => {
139 | [f3, f4].forEach((field) => {
140 | expect(output.getErrors(field)).to.include(rejectionMessage);
141 | });
142 |
143 | done();
144 | }, 550);
145 | });
146 |
147 | it('Should pass for fulfilled promises', (done) => {
148 | expect(output.hasErrors(f5)).to.equal(false);
149 |
150 | setTimeout(() => {
151 | expect(output.hasErrors(f5)).to.equal(false);
152 | done();
153 | }, 500);
154 | });
155 |
156 | });
157 |
158 | describe('When passing a Promise as a test', () => {
159 | describe('failing', () => {
160 | let f1, f2, f3, f4;
161 | const rejectionMessage = lorem.sentence();
162 |
163 | beforeEach(() => {
164 | f1 = lorem.word();
165 | f2 = lorem.word();
166 | f3 = lorem.word();
167 | f4 = lorem.word();
168 |
169 | output = passable(lorem.word(), (test) => {
170 | test(f1, f1, new Promise((_, reject) => setImmediate(reject)));
171 | test(f2, new Promise((_, reject) => setImmediate(() => reject(rejectionMessage))));
172 | test(f3, lorem.sentence(), new Promise((_, reject) => setImmediate(() => reject(lorem.word()))));
173 | test(f3, f3, new Promise((_, reject) => setImmediate(() => reject(rejectionMessage))));
174 | test(f4, f4, new Promise((resolve) => setTimeout(resolve)));
175 | test(f4, f4, new Promise((resolve) => setTimeout(resolve, 500)));
176 | test(lorem.word(), lorem.sentence(), noop);
177 | });
178 | });
179 |
180 | it('Should immediately register tests', () => {
181 | expect(output.testCount).to.equal(7);
182 | });
183 |
184 | it('Should run async test promise', (done) => {
185 | passable(lorem.word(), (test) => {
186 | test(f1, lorem.sentence(), new Promise(() => done()));
187 | test(lorem.word(), lorem.sentence(), noop);
188 | });
189 | });
190 |
191 | it('Should fail with rejection message when provided', (done) => {
192 | setTimeout(() => {
193 | [f2, f3].forEach((field) => {
194 | expect(output.getErrors(field)).to.include(rejectionMessage);
195 | });
196 |
197 | done();
198 | }, 550);
199 | });
200 |
201 | it('Should only mark test as failing after rejection', (done) => {
202 | expect(output.failCount).to.equal(0);
203 |
204 | setTimeout(() => {
205 | expect(output.failCount).to.equal(4);
206 | done();
207 | }, 10);
208 | });
209 | });
210 |
211 | describe('passing', () => {
212 | beforeEach(() => {
213 | output = passable(lorem.word(), (test) => {
214 | test(lorem.word(), lorem.sentence(), new Promise((resolve, reject) => setImmediate(resolve)));
215 | });
216 | });
217 |
218 | it('Should keep test unchanged after resolution', (done) => {
219 | const res = clone(output);
220 | setTimeout(() => {
221 | expect(output).to.deep.equal(res);
222 | done();
223 | }, 10);
224 | });
225 | });
226 | });
227 | });
228 |
229 | describe('sync test behavior', () => {
230 | it('should mark a test as failed for a thrown error', () => {
231 | const name = lorem.word();
232 | output = passable(lorem.word(), (test) => {
233 | test(name, lorem.sentence(), () => { throw new Error(); });
234 | test(lorem.word(), lorem.sentence(), noop);
235 | });
236 | expect(output.failCount).to.equal(1);
237 | expect(output.hasErrors(name)).to.equal(true);
238 | });
239 |
240 | it('should mark a test as failed for explicit `false`', () => {
241 | const name = lorem.word();
242 | output = passable(lorem.word(), (test) => {
243 | test(name, lorem.sentence(), () => false);
244 | test(lorem.word(), lorem.sentence(), noop);
245 | });
246 | expect(output.failCount).to.equal(1);
247 | expect(output.hasErrors(name)).to.equal(true);
248 | });
249 |
250 | it('should implicitly pass test', () => {
251 | const name = lorem.word();
252 | output = passable(lorem.word(), (test) => {
253 | test(name, lorem.sentence(), noop);
254 | });
255 | expect(output.failCount).to.equal(0);
256 | expect(output.testCount).to.equal(1);
257 | });
258 | });
259 | });
260 | });
261 |
--------------------------------------------------------------------------------
/dist/passable.min.js:
--------------------------------------------------------------------------------
1 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).passable=n()}(this,(function(){"use strict";function t(n){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(n)}function n(t,n){for(var e=0;et.length)&&(n=t.length);for(var e=0,r=new Array(n);eNumber(n)}function v(t,n){return b(t)&&b(n)&&Number(t)>=Number(n)}function w(t,n){return b(t)&&b(n)&&Number(t)n},longerThanOrEquals:function(t,n){return t.length>=n},shorterThan:function(t,n){return t.length2?r-2:0),i=2;i0&&void 0!==arguments[0]?arguments[0]:{},n=i({},P,{},t);if(s())return function(t){var e=new Proxy(n,{get:function(n,r){if(u(n,r))return function(){for(var o=arguments.length,i=new Array(o),u=0;u1?e-1:0),o=1;o PassableNS.IEnforceInstance;
5 | const Enforce: PassableNS.IEnforceConstructor;
6 | const validate: PassableNS.IValidate;
7 | const WARN: PassableNS.IWARN;
8 | const FAIL: PassableNS.IFAIL;
9 | const VERSION: PassableNS.IVERSION;
10 |
11 | export default passable;
12 | export { enforce, Enforce, validate, WARN, FAIL, VERSION };
13 |
14 | interface Passable {
15 | (name: string, testFn: (test: (name: string, errorMessage: string, callback: PassableNS.IFunctionOrPromise) => void,
16 | draft: PassableNS.IValidationResult) => void,
17 | specific?: string | string[] | {only?: string | string[], not?: string | string[]}):
18 | PassableNS.IValidationResult,
19 | enforce(value): PassableNS.IEnforceInstance;
20 | test(name: string, errorMessage: string, callback: PassableNS.IFunctionOrPromise): void;
21 | draft(): PassableNS.IEnforceInstance;
22 | Enforce: PassableNS.IEnforceConstructor;
23 | any: PassableNS.IAny;
24 | validate: PassableNS.IValidate;
25 | VERSION: PassableNS.IVERSION;
26 | WARN: PassableNS.IWARN;
27 | FAIL: PassableNS.IFAIL;
28 | }
29 |
30 | namespace PassableNS {
31 |
32 | export interface IValidationResult {
33 | /**
34 | * The name of the form being validated
35 | */
36 | name: string;
37 | /**
38 | * Overall errors count in current validation suite
39 | */
40 | failCount: number;
41 | /**
42 | * All skipped fields in suite (empty, unless the specific option is used)
43 | */
44 | skipped: string[];
45 | /**
46 | * Overall warnings count in current validation suite
47 | */
48 | testCount: number;
49 | /**
50 | * Detailed stats per field (structure detailed below)
51 | */
52 | testsPerformed: {
53 | [fieldName: string]: {
54 | /**
55 | * Overall test count in this field
56 | */
57 | testCount: number;
58 | /**
59 | * Overall errors count for this field
60 | */
61 | failCount: number;
62 | /**
63 | * Overall warnings count for this field
64 | */
65 | warnCount: number;
66 | }
67 | };
68 | /**
69 | * Actual errors per each field
70 | */
71 | errors: {
72 | [fieldName: string]: string[];
73 | };
74 | /**
75 | * Actual errors per each field
76 | */
77 | warnings: {
78 | [fieldName: string]: string[];
79 | };
80 | /**
81 | * Overall warnings count for this form
82 | */
83 | warnCount: number;
84 | /**
85 | * Getter function which allows accessing the errors array of a certain field (or the whole suite if not supplied)
86 | */
87 | getErrors: (field?: string) => any[];
88 | /**
89 | * Getter function which allows accessing the warnings array of a certain field (or the whole suite if not supplied)
90 | */
91 | getWarnings: (field?: string) => any[];
92 | /**
93 | * Returns whether a certain field (or the whole suite if not supplied) has errors
94 | */
95 | hasErrors: (field?: string) => boolean;
96 | /**
97 | * Returns whether a certain field (or the whole suite if not supplied) has warnings
98 | */
99 | hasWarnings: (field?: string) => boolean;
100 |
101 | /**
102 | * Registers a completion callback for the validation suite
103 | */
104 | done: (callback: (res: PassableNS.IValidationResult) => void) => PassableNS.IValidationResult;
105 |
106 | /**
107 | * Registers a completion callback for a specific field
108 | */
109 | after: (fieldName: string, callback: (res: PassableNS.IValidationResult) => void) => PassableNS.IValidationResult;
110 |
111 | /**
112 | * Cancels Async tests callbacks (after/done)
113 | */
114 | cancel: () => PassableNS.IValidationResult;
115 | }
116 |
117 | export type IFunctionOrPromise = () => void | Promise;
118 |
119 | export type IVERSION = string;
120 | export type IWARN = 'warn';
121 | export type IFAIL = 'fail';
122 |
123 | export interface IValidate {
124 | (): boolean;
125 | }
126 | export interface IAny {
127 | (): boolean;
128 | }
129 |
130 | type IEnforceChain = {
131 | [K in keyof T]: IEnforceInstance
132 | };
133 |
134 | export type IEnforceConstructor = {
135 | new(): (value) => IEnforceInstance;
136 | new boolean }>
137 | (arg: T): (value) => IEnforceInstance>;
138 | };
139 |
140 | export interface IEnforceInstance {
141 | /**
142 | * Checks if a value contains a regex match by Regex expression
143 | *
144 | * @example
145 | * enforce(1984).matches(/[0-9]/) // truthy
146 | *
147 | * enforce('nineteen eighty four').matches(/[0-9]/) // falsy
148 | */
149 | matches(regex: RegExp): IEnforceInstance & T;
150 |
151 | /**
152 | * Checks if a value contains a regex match by a string expression
153 | *
154 | * @example
155 | * enforce(1984).matches('[0-9]') //truthy
156 | *
157 | * enforce('nineteen eighty four').matches('[0-9]') // falsy
158 | */
159 | matches(regexAsString: string): IEnforceInstance & T;
160 |
161 | /**
162 | * Checks if a value doesn't contains a regex match by Regex expression
163 | *
164 | * @example
165 | * enforce(1984).notMatches(/[0-9]/) // falsy
166 | */
167 | notMatches(regex: RegExp): IEnforceInstance & T;
168 |
169 | /**
170 | * Checks if a value doesn't contains a regex match by string expression
171 | *
172 | * @example
173 | * enforce('nineteen eighty four').notMatches('[0-9]') // truthy
174 | */
175 | notMatches(regexAsString: string): IEnforceInstance & T;
176 |
177 | /**
178 | * Checks if your enforce value is contained in another array
179 | *
180 | * @example
181 | * enforce('hello').inside(['hello', 'world']) // truthy
182 | *
183 | * enforce(3).inside([1, 2]) // falsy
184 | *
185 | * enforce(false).inside([true, false]) // truthy
186 | */
187 | inside(array: number[] | string[] | boolean[]): IEnforceInstance & T;
188 | /**
189 | * Checks if your enforce value is contained in another string
190 | *
191 | * @example
192 | * enforce('da').inside('tru dat.') // truthy
193 | *
194 | * enforce('ad').inside('tru dat.') // falsy
195 | */
196 | inside(text: string): IEnforceInstance & T;
197 |
198 | /**
199 | * Checks if your enforce value is not contained in another array
200 | *
201 | * @example
202 | * enforce('hello').notInside(['hello', 'world']) // falsy
203 | */
204 | notInside(array: number[] | string[] | boolean[]): IEnforceInstance & T;
205 | /**
206 | * Checks if your enforce value is not contained in another string
207 | *
208 | * @example
209 | * enforce('ad').notInside('tru dat.') // truthy
210 | */
211 | notInside(text: string): IEnforceInstance & T;
212 |
213 | /**
214 | * Checks if a value is of type Array
215 | *
216 | * @example
217 | * enforce(['hello']).isArray() // truthy
218 | *
219 | * enforce('hello').isArray() // falsy
220 | */
221 | isArray(): IEnforceInstance & T;
222 |
223 | /**
224 | * Checks if a value is of any type other than array
225 | *
226 | * @example
227 | * enforce(['hello']).isNotArray() // falsy
228 | *
229 | * enforce('hello').isNotArray() // truthy
230 | */
231 | isNotArray(): IEnforceInstance & T;
232 |
233 | /**
234 | * Checks if a value is of type String
235 | *
236 | * @example
237 | * enforce('hello').isString() // truthy
238 | *
239 | * enforce(['hello']).isString() // falsy
240 | */
241 | isString(): IEnforceInstance & T;
242 |
243 | /**
244 | * Checks if a value is of any type other than string
245 | *
246 | * @example
247 | * enforce('hello').isNotString() // falsy
248 | *
249 | * enforce(['hello']).isNotString() // truthy
250 | */
251 | isNotString(): IEnforceInstance & T;
252 |
253 | /**
254 | * Checks if a value is of type number
255 | *
256 | * @example
257 | * enforce(143).isNumber() // truthy
258 | *
259 | * enforce(NaN).isNumber() // truthy! (NaN is of type 'number!')
260 | */
261 | isNumber(): IEnforceInstance & T;
262 |
263 | /**
264 | * Checks if a value is of any type other than number
265 | *
266 | * @example
267 | * enforce(143).isNotNumber() // falsy
268 | *
269 | * enforce('143').isNotNumber() // truthy
270 | */
271 | isNotNumber(): IEnforceInstance & T;
272 |
273 | /**
274 | * Checks if your enforce value is empty, false, zero, null or undefined
275 | *
276 | * @example
277 | * enforce([]).isEmpty() // truthy
278 | *
279 | * enforce('').isEmpty() // truthy
280 | *
281 | * enforce({}).isEmpty() // truthy
282 | */
283 | isEmpty(): IEnforceInstance & T;
284 |
285 | /**
286 | * Checks that your enforce value is not empty, false, or zero
287 | *
288 | * @example
289 | *
290 | * enforce([1]).isNotEmpty() // truthy
291 | *
292 | * enforce({1:1}).isNotEmpty() // truthy
293 | *
294 | * enforce([]).isNotEmpty() // falsy
295 | */
296 | isNotEmpty(): IEnforceInstance & T;
297 |
298 | /**
299 | * Checks that your enforce value is a numeric value
300 | *
301 | * @example
302 | *
303 | * enforce('-0x42').isNumeric() // falsy
304 | *
305 | * enforce('0xFF').isNumeric() // truthy
306 | */
307 | isNumeric(): IEnforceInstance & T;
308 |
309 | /**
310 | * Checks that your enforce value is not a numeric value
311 | *
312 | * @example
313 | *
314 | * enforce('7.2acdgs').isNotNumeric() // truthy
315 | *
316 | * enforce('-10').isNotNumeric() // falsy
317 | */
318 | isNotNumeric(): IEnforceInstance & T;
319 |
320 | /**
321 | * Checks that your numeric enforce value is smaller than another value
322 | *
323 | * @example
324 | *
325 | * enforce(0).lessThan(1) // truthy
326 | *
327 | * enforce('1').lessThan(0) // falsy
328 | */
329 | lessThan(size: number): IEnforceInstance & T;
330 |
331 | /**
332 | * Checks that your numeric enforce value is smaller than another value
333 | *
334 | * @example
335 | *
336 | * enforce(0).lt(1) // truthy
337 | *
338 | * enforce('1').lt(0) // falsy
339 | */
340 |
341 | lt(size: number): IEnforceInstance & T;
342 | /**
343 | * Checks that your numeric enforce value is smaller than or equals another value
344 | *
345 | * @example
346 | *
347 | * enforce(0).lessThanOrEquals(1) // truthy
348 | *
349 | * enforce('1').lessThanOrEquals(1) // truthy
350 | *
351 | * enforce(2).lessThanOrEquals(1) // falsy
352 | */
353 | lessThanOrEquals(size: number): IEnforceInstance & T;
354 |
355 | /**
356 | * Checks that your numeric enforce value is smaller than or equals another value
357 | *
358 | * @example
359 | *
360 | * enforce(0).lte(1) // truthy
361 | *
362 | * enforce('1').lte(1) // truthy
363 | *
364 | * enforce(2).lte('1') // falsy
365 | */
366 | lte(size: number): IEnforceInstance & T;
367 |
368 | /**
369 | * Checks that your numeric enforce value is greater than another value
370 | *
371 | * @example
372 | *
373 | * enforce(1).greaterThan(0) // truthy
374 | *
375 | * enforce('0').greaterThan('1') // falsy
376 | */
377 | greaterThan(size: number): IEnforceInstance & T;
378 |
379 | /**
380 | * Checks that your numeric enforce value is greater than another value
381 | *
382 | * @example
383 | *
384 | * enforce(1).gt(0) // truthy
385 | *
386 | * enforce(0).gt('1') // falsy
387 | */
388 |
389 | gt(size: number): IEnforceInstance & T;
390 | /**
391 | * Checks that your numeric enforce value is greater than or equals another value
392 | *
393 | * @example
394 | *
395 | * enforce(1).greaterThanOrEquals(1) // truthy
396 | *
397 | * enforce('1').greaterThanOrEquals(0) // truthy
398 | *
399 | * enforce('2').greaterThanOrEquals(3) // falsy
400 | */
401 | greaterThanOrEquals(size: number): IEnforceInstance & T;
402 |
403 | /**
404 | * Checks that your numeric enforce value is greater than or equals another value
405 | *
406 | * @example
407 | *
408 | * enforce(1).gte(1) // truthy
409 | *
410 | * enforce('1').gte(0) // truthy
411 | *
412 | * enforce(2).gte('3') // falsy
413 | */
414 | gte(size: number): IEnforceInstance & T;
415 |
416 | /**
417 | * Checks that your enforce value equals a given number
418 | *
419 | * @example
420 | *
421 | * enforce('1').numberEquals(1) // truthy
422 | *
423 | * enforce(2).numberEquals(2) // truthy
424 | *
425 | * enforce('2').numberEquals(0) // falsy
426 | */
427 | numberEquals(size: number): IEnforceInstance & T;
428 |
429 | /**
430 | * Checks that your enforce value does not equal a given number
431 | *
432 | * @example
433 | *
434 | * enforce(3).numberNotEquals(1) // truthy
435 | *
436 | * enforce('3').numberNotEquals(3) // falsy
437 | */
438 | numberNotEquals(size: number): IEnforceInstance & T;
439 |
440 | /**
441 | * Checks that your enforce value is longer than a given number
442 | *
443 | * @example
444 | *
445 | * enforce(['one']).longerThan(0) // truthy
446 | *
447 | * enforce('').longerThan(0) // falsy
448 | */
449 | longerThan(size: number): IEnforceInstance & T;
450 |
451 | /**
452 | * Checks that your enforce value is longer than or equals another value
453 | *
454 | * @example
455 | *
456 | * enforce([1]).longerThanOrEquals(0) // truthy
457 | *
458 | * enforce('').longerThanOrEquals(1) // falsy
459 | */
460 | longerThanOrEquals(size: number): IEnforceInstance & T;
461 |
462 | /**
463 | * Checks that your enforce value is shorter than a given number
464 | *
465 | * @example
466 | *
467 | * enforce([]).shorterThan(1) // truthy
468 | *
469 | * enforce('0').shorterThan(0) // falsy
470 | */
471 | shorterThan(size: number): IEnforceInstance & T;
472 |
473 | /**
474 | * Checks that your enforce value is shorter than or equals another value
475 | *
476 | * @example
477 | *
478 | * enforce([]).shorterThanOrEquals(1) // truthy
479 | *
480 | * enforce('0').shorterThanOrEquals(0) // falsy
481 | */
482 | shorterThanOrEquals(size: number): IEnforceInstance & T;
483 |
484 | /**
485 | * Checks that your enforce value equals a given number
486 | *
487 | * @example
488 | *
489 | * enforce([1]).lengthEquals(1) // truthy
490 | *
491 | * enforce('0').lengthEquals(0) // falsy
492 | */
493 | lengthEquals(size: number): IEnforceInstance & T;
494 |
495 | /**
496 | * Checks that your enforce value does not equal a given number
497 | *
498 | * @example
499 | *
500 | * enforce([]).lengthNotEquals(1) // truthy
501 | *
502 | * enforce('').lengthNotEquals(0) // falsy
503 | */
504 | lengthNotEquals(size: number): IEnforceInstance & T;
505 |
506 | /**
507 | * Checks that your enforce value strictly equals (===) a given value
508 | *
509 | * @example
510 | *
511 | * enforce(1).equals(1) // truthy
512 | *
513 | * enforce('hello').equals('hello') // truthy
514 | *
515 | * enforce('1').equals(1) // falsy
516 | *
517 | * enforce([1]).equals([1]) // falsy
518 | */
519 | equals(value: any): IEnforceInstance & T;
520 |
521 | /**
522 | * Checks that your enforce value doesn't strictly equal (===) a given value
523 | *
524 | * @example
525 | *
526 | * enforce('1').notEquals(1) // truthy
527 | *
528 | * enforce([1]).notEquals([1]) // truthy
529 | *
530 | * enforce(1).notEquals(1) // falsy
531 | *
532 | * enforce('hello').notEquals('hello') // falsy
533 | */
534 | notEquals(value: any): IEnforceInstance & T;
535 | }
536 | }
537 | }
538 |
--------------------------------------------------------------------------------
/docs/enforce.md:
--------------------------------------------------------------------------------
1 | # Enforce
2 | For assertions, Passable is bundled with [Enforce](npmjs.com/package/n4s). Enforce is a validation assertions library. It allows you to run your data against rules and conditions and test whether it passes your validations. It is intended for validation logic that gets repeated over and over again and should not be written manually. It comes with a wide-variety of pre-built rules, but it can also be extended to support your own repeated custom logic.
3 |
4 | The way Enforce operates is similar to most common assertion libraries. You pass it a value, and one or more rules to test your value against - if the validation fails, it throws an Error, otherwise - it will move on to the next rule rule in the chain.
5 |
6 | ```js
7 | import { passable } from 'passable'
8 |
9 | enforce(4)
10 | .isNumber();
11 | // passes
12 |
13 | enforce(4)
14 | .isNumber()
15 | .greaterThan(2);
16 | // passes
17 |
18 | enforce(4)
19 | .lessThan(2) // throws an error, will not carry on to the next rule
20 | .greaterThan(3);
21 | ```
22 |
23 | ## Content
24 | - [List of Enforce rules](#list-of-enforce-rules)
25 | - [Custom Enforce Rules](#custom-enforce-rules)
26 |
27 | Enforce exposes all predefined and custom rules. You may use chaining to make multiple enfocements for the same value.
28 |
29 | # List of Enforce rules
30 | Enforce rules are functions that allow you to test your data against different criteria. The following rules are supported out-of-the-box.
31 |
32 | - [equals](#equals)
33 | - [notEquals](#notequals)
34 | - [isEmpty](#isempty)
35 | - [isNotEmpty](#isnotempty)
36 | - [isNumeric](#isnumeric)
37 | - [isNotNumeric](#isnotnumeric)
38 | - [greaterThan](#greaterthan)
39 | - [greaterThanOrEquals](#greaterthanorequals)
40 | - [lengthEquals](#lengthequals)
41 | - [lengthNotEquals](#lengthnotequals)
42 | - [lessThan](#lessthan)
43 | - [lessThanOrEquals](#lessthanorequals)
44 | - [longerThan](#longerthan)
45 | - [longerThanOrEquals](#longerthanorequals)
46 | - [numberEquals](#numberequals)
47 | - [numberNotEquals](#numbernotequals)
48 | - [shorterThan](#shorterthan)
49 | - [shorterThanOrEquals](#shorterthanorequals)
50 | - [matches](#matches)
51 | - [notMatches](#notmatches)
52 | - [inside](#inside)
53 | - [notInside](#notinside)
54 | - [isTruthy](#istruthy)
55 | - [isFalsy](#isfalsy)
56 | - [isArray](#isarray)
57 | - [isNotArray](#isnotarray)
58 | - [isNumber](#isnumber)
59 | - [isNotNumber](#isnotnumber)
60 | - [isString](#isstring)
61 | - [isNotString](#isnotstring)
62 | - [isOdd](#isodd)
63 | - [isEven](#iseven)
64 |
65 | ### equals
66 | ### Description
67 | Checks if your enforced value strictly equals (`===`) another.
68 |
69 | It is not recommended to use this rule to compare arrays or objects, as it does not perform any sort of deep comparison on the value.
70 |
71 | For numeric value comparison, you should use `numberEquals`, which coerces numeric strings into numbers before comparing.
72 |
73 | ### Arguments
74 | * `value`: Any value you wish to check your enforced value against
75 |
76 | ### Usage examples:
77 |
78 | ```js
79 | enforce(1).equals(1);
80 |
81 | enforce('hello').equals('hello');
82 |
83 | const a = [1, 2, 3];
84 |
85 | enforce(a).equals(a);
86 | // passes
87 | ```
88 |
89 | ```js
90 | enforce('1').equals(1);
91 | enforce([1, 2, 3]).equals([1, 2, 3]);
92 | // throws
93 | ```
94 |
95 |
96 | ## notEquals
97 | ### Description
98 | Checks if your enforced value does not strictly equal (`===`) another.
99 |
100 | Reverse implementation of `equals`.
101 |
102 | ### Usage examples:
103 |
104 | ```js
105 | enforce('1').notEquals(1);
106 | enforce([1, 2, 3]).notEquals([1, 2, 3]);
107 | // passes
108 | ```
109 |
110 | ```js
111 | enforce(1).notEquals(1);
112 | enforce('hello').notEquals('hello');
113 |
114 | const a = [1, 2, 3];
115 |
116 | enforce(a).notEquals(a);
117 | // throws
118 | ```
119 |
120 |
121 | ## isEmpty
122 | ### Description
123 | Checks if your enforced value is empty, false, zero, null or undefined.
124 |
125 | Expected results are:
126 | * object: checks against count of keys (`0` is empty)
127 | * array/string: checks against length. (`0` is empty)
128 | * number: checks the value of the number. (`0` and `NaN` are empty)
129 | * boolean: `false` is empty.
130 | * undefined/null: are both empty.
131 |
132 | ### Usage examples:
133 |
134 | ```js
135 | enforce([]).isEmpty();
136 | enforce('').isEmpty();
137 | enforce({}).isEmpty();
138 | enforce(0).isEmpty();
139 | enforce(NaN).isEmpty();
140 | enforce(undefined).isEmpty();
141 | enforce(null).isEmpty();
142 | enforce(false).isEmpty();
143 | // passes
144 | ```
145 |
146 | ```js
147 | enforce([1]).isEmpty();
148 | enforce('1').isEmpty();
149 | enforce({1:1}).isEmpty();
150 | enforce(1).isEmpty();
151 | enforce(true).isEmpty();
152 | // throws
153 | ```
154 |
155 |
156 | ## isNotEmpty
157 | ### Description
158 | Checks that your enforced value is not empty, false, or zero.
159 | Reverse implementation of `isEmpty`.
160 |
161 | ### Usage examples:
162 |
163 | ```js
164 | enforce([1]).isNotEmpty();
165 | enforce('1').isNotEmpty();
166 | enforce({1:1}).isNotEmpty();
167 | // passes
168 | ```
169 |
170 | ```js
171 | enforce([]).isNotEmpty();
172 | enforce('').isNotEmpty();
173 | enforce({}).isNotEmpty();
174 | enforce(0).isNotEmpty();
175 | // throws
176 | ```
177 |
178 |
179 | ## isNumeric
180 | ### Description
181 | Checks if a value is a representation of a real number
182 |
183 | ### Usage examples:
184 |
185 | ```js
186 | enforce(143).isNumeric();
187 | enforce('143').isNumeric();
188 | // passes
189 | ```
190 |
191 | ```js
192 | enforce(NaN).isNumeric();
193 | enforce('1hello').isNumeric();
194 | enforce('hi').isNumeric();
195 | // throws
196 | ```
197 |
198 |
199 | ## isNotNumeric
200 | ### Description
201 | Checks if a value is not a representation of a real number.
202 | Reverse implementation of `isNumeric`.
203 |
204 | ### Usage examples:
205 |
206 | ```js
207 | enforce(NaN).isNotNumeric();
208 | enforce('Hello World!').isNotNumeric();
209 | // passes
210 | ```
211 |
212 | ```js
213 | enforce(731).isNotNumeric();
214 | enforce('42').isNotNumeric();
215 | // throws
216 | ```
217 |
218 |
219 | ## greaterThan
220 |
221 | - alias: `gt`
222 |
223 | ### Description
224 | Checks that your numeric enforced value is larger than a given numeric value.
225 |
226 | ### Arguments
227 | * `value`: `number | string` | A numeric value against which you want to check your enforced value.
228 |
229 | Strings are parsed using `Number()`, values which are non fully numeric always return false;
230 |
231 | ### Usage
232 |
233 | ```js
234 | enforce(1).greaterThan(0);
235 | enforce('10').greaterThan(0);
236 | enforce(900).gt('100');
237 | // passes
238 | ```
239 |
240 | ```js
241 | enforce(100).greaterThan(100);
242 | enforce('100').greaterThan(110);
243 | enforce([100]).gt(1);
244 | // throws
245 | ```
246 |
247 |
248 | ## greaterThanOrEquals
249 | - alias: `gte()`
250 |
251 | ### Description
252 | Checks that your numeric enforced value is larger than or equals to a given numeric value.
253 |
254 | ### Arguments
255 | * `value`: `number | string` | A numeric value against which you want to check your enforced value.
256 |
257 | Strings are parsed using `Number()`, values which are non fully numeric always return false;
258 |
259 | ### Usage
260 |
261 | ```js
262 | enforce(1).greaterThanOrEquals(0);
263 | enforce('10').greaterThanOrEquals(0);
264 | enforce(900).greaterThanOrEquals('100');
265 | enforce(100).greaterThanOrEquals('100');
266 | enforce(900).gte('900');
267 | enforce('1337').gte(1337);
268 | // passes
269 | ```
270 |
271 | ```js
272 | enforce(100).greaterThanOrEquals('120');
273 | enforce('100').greaterThanOrEquals(110);
274 | enforce([100]).gte(1);
275 | // throws
276 | ```
277 |
278 |
279 | ## lengthEquals
280 | ### Description
281 | Checks that your enforced value is equal to the given number.
282 |
283 | ### Arguments
284 | * `size`: `number` | the number which you would like your initial value to be tested against.
285 |
286 | The `value` argument can be of the following types:
287 | * array: checks against length.
288 | * string: checks against length.
289 |
290 | ### Usage examples:
291 |
292 | ```js
293 | enforce([1]).lengthEquals(1);
294 | enforce('a').lengthEquals(1);
295 | // passes
296 | ```
297 |
298 | ```js
299 | enforce([1, 2]).lengthEquals(1);
300 | enforce('').lengthEquals(1);
301 | // throws
302 | ```
303 |
304 | ## lengthNotEquals
305 | ### Description
306 | Checks that your enforced value is not equal to the given number.
307 | Reverse implementation of `lengthEquals`.
308 |
309 | ### Arguments
310 | * `size`: `number` | the number which you would like your initial value to be tested against.
311 |
312 | The `value` argument can be of the following types:
313 | * array: checks against length.
314 | * string: checks against length.
315 |
316 | ### Usage examples:
317 |
318 | ```js
319 | enforce([1]).lengthNotEquals(0);
320 | enforce('a').lengthNotEquals(3);
321 | // passes
322 | ```
323 |
324 | ```js
325 | enforce([1]).lengthNotEquals(1);
326 | enforce('').lengthNotEquals(0);
327 | // throws
328 | ```
329 |
330 |
331 | ## lessThan
332 |
333 | - alias: `lt()`
334 |
335 | ### Description
336 | Checks that your numeric enforced value is smaller than a given numeric value.
337 |
338 | ### Arguments
339 | * `value`: `number | string` | A numeric value against which you want to check your enforced value.
340 |
341 | Strings are parsed using `Number()`, values which are non fully numeric always return false;
342 |
343 | ### Usage
344 |
345 | ```js
346 | enforce(0).lessThan(1);
347 | enforce(2).lessThan('10');
348 | enforce('90').lt(100);
349 | // passes
350 | ```
351 |
352 | ```js
353 | enforce(100).lessThan(100);
354 | enforce('110').lessThan(100);
355 | enforce([0]).lt(1);
356 | // throws
357 | ```
358 |
359 |
360 | ## lessThanOrEquals
361 |
362 | - alias: `lte()`
363 |
364 | ### Description
365 | Checks that your numeric enforced value is smaller than or equals to a given numeric value.
366 |
367 | ### Arguments
368 | * `value`: `number | string` | A numeric value against which you want to check your enforced value.
369 |
370 | Strings are parsed using `Number()`, values which are non fully numeric always return false;
371 |
372 | ### Usage
373 |
374 | ```js
375 | enforce(0).lessThanOrEquals(1);
376 | enforce(2).lessThanOrEquals('10');
377 | enforce('90').lte(100);
378 | enforce(100).lte('100');
379 | // passes
380 | ```
381 |
382 | ```js
383 | enforce(100).lessThanOrEquals(90);
384 | enforce('110').lessThanOrEquals(100);
385 | enforce([0]).lte(1);
386 | // throws
387 | ```
388 |
389 |
390 | ## longerThan
391 | ### Description
392 | Checks that your enforced value is longer than a given number.
393 |
394 | ### Arguments
395 | * `size`: `number` | the number which you would like your initial value to be tested against.
396 |
397 | The `value` argument can be of the following types:
398 | * array: checks against length.
399 | * string: checks against length.
400 |
401 | ### Usage examples:
402 |
403 | ```js
404 | enforce([1]).longerThan(0);
405 | enforce('ab').longerThan(1);
406 | // passes
407 | ```
408 |
409 | ```js
410 | enforce([1]).longerThan(2);
411 | enforce('').longerThan(0);
412 | // throws
413 | ```
414 |
415 |
416 | ## longerThanOrEquals
417 | ### Description
418 | Checks that your enforced value is longer than or equals to a given number.
419 |
420 | ### Arguments
421 | * `size`: `number` | the number which you would like your initial value to be tested against.
422 |
423 | The `value` argument can be of the following types:
424 | * array: checks against length.
425 | * string: checks against length.
426 |
427 | ### Usage examples:
428 |
429 | ```js
430 | enforce([1]).longerThanOrEquals(0);
431 | enforce('ab').longerThanOrEquals(1);
432 | enforce([1]).longerThanOrEquals(1);
433 | enforce('a').longerThanOrEquals(1);
434 | // passes
435 | ```
436 |
437 | ```js
438 | enforce([1]).longerThanOrEquals(2);
439 | enforce('').longerThanOrEquals(1);
440 | // throws
441 | ```
442 |
443 |
444 | ## numberEquals
445 | ### Description
446 | Checks that your numeric enforced value is equals another value.
447 |
448 | ### Arguments
449 | * `value`: `number | string` | A numeric value against which you want to check your enforced value.
450 |
451 | Strings are parsed using `Number()`, values which are non fully numeric always return false;
452 |
453 | ### Usage
454 |
455 | ```js
456 | enforce(0).numberEquals(0);
457 | enforce(2).numberEquals('2');
458 | // passes
459 | ```
460 |
461 | ```js
462 | enforce(100).numberEquals(10);
463 | enforce('110').numberEquals(100);
464 | enforce([0]).numberEquals(1);
465 | // throws
466 | ```
467 |
468 |
469 | ## numberNotEquals
470 | ### Description
471 | Checks that your numeric enforced value does not equal another value.
472 | Reverse implementation of `numberEquals`.
473 |
474 | ### Arguments
475 | * `value`: `number | string` | A numeric value against which you want to check your enforced value.
476 |
477 | Strings are parsed using `Number()`, values which are non fully numeric always return false;
478 |
479 | ### Usage
480 |
481 | ```js
482 | enforce(2).numberNotEquals(0);
483 | enforce('11').numberNotEquals('10');
484 | // passes
485 | ```
486 |
487 | ```js
488 | enforce(100).numberNotEquals(100);
489 | enforce('110').numberNotEquals(100);
490 | // throws
491 | ```
492 |
493 |
494 | ## shorterThan
495 | ### Description
496 | Checks that your enforced value is shorter than a given number.
497 |
498 | ### Arguments
499 | * `size`: `number` | the number which you would like your initial value to be tested against.
500 |
501 | The `value` argument can be of the following types:
502 | * array: checks against length.
503 | * string: checks against length.
504 |
505 | ### Usage examples:
506 |
507 | ```js
508 | enforce([]).shorterThan(1);
509 | enforce('a').shorterThan(2);
510 | // passes
511 | ```
512 |
513 | ```js
514 | enforce([1]).shorterThan(0);
515 | enforce('').shorterThan(0);
516 | // throws
517 | ```
518 |
519 |
520 | ## shorterThanOrEquals
521 | ### Description
522 | Checks that your enforced value is shorter than or equals to a given number.
523 |
524 | ### Arguments
525 | * `size`: `number` | the number which you would like your initial value to be tested against.
526 |
527 | The `value` argument can be of the following types:
528 | * array: checks against length.
529 | * string: checks against length.
530 |
531 | ### Usage examples:
532 |
533 | #### Passing examples:
534 | ```js
535 | enforce([]).shorterThanOrEquals(1);
536 | enforce('a').shorterThanOrEquals(2);
537 | enforce([]).shorterThanOrEquals(0);
538 | enforce('a').shorterThanOrEquals(1);
539 | // passes
540 | ```
541 |
542 | ```js
543 | enforce([1]).shorterThanOrEquals(0);
544 | enforce('ab').shorterThanOrEquals(1);
545 | // throws
546 | ```
547 |
548 |
549 | ## matches
550 | ### Description
551 | Checks if a value contains a regex match.
552 |
553 | ### Arguments
554 | * `regexp`: either a `RegExp` object, or a RegExp valid string
555 |
556 | ### Usage examples:
557 |
558 | ```js
559 | enforce(1984).matches(/[0-9]/);
560 | enforce(1984).matches('[0-9]');
561 | enforce('1984').matches(/[0-9]/);
562 | enforce('1984').matches('[0-9]');
563 | enforce('198four').matches(/[0-9]/);
564 | enforce('198four').matches('[0-9]');
565 | // passes
566 | ```
567 |
568 | ```js
569 | enforce('ninety eighty four').matches(/[0-9]/);
570 | enforce('ninety eighty four').matches('[0-9]');
571 | // throws
572 | ```
573 |
574 |
575 | ## notMatches
576 | ### Description
577 | Checks if a value does not contain a regex match.
578 | Reverse implementation of `matches`.
579 |
580 | ### Usage examples:
581 |
582 | ```js
583 | enforce(1984).notMatches(/[0-9]/);
584 | // throws
585 | ```
586 |
587 | ```js
588 | enforce('ninety eighty four').notMatches('[0-9]');
589 | // passes
590 | ```
591 |
592 |
593 | ## inside
594 | ### Description
595 | Checks if your enforced value is contained in another array or string.
596 | Your enforced value can be of the following types:
597 | * `string`
598 | * `number`
599 | * `boolean`
600 |
601 | ### Arguments
602 | * `container`: a `string` or an `array` which may contain the value specified.
603 |
604 | ### Usage examples:
605 |
606 | #### inside: array
607 | Checks for membership in an array.
608 |
609 | - string: checks if a string is an element in an array
610 |
611 | ```js
612 | enforce('hello').inside(['hello', 'world']);
613 | // passes
614 | ```
615 |
616 | ```js
617 | enforce('hello!').inside(['hello', 'world']);
618 | // throws
619 | ```
620 | - number: checks if a number is an element in an array
621 |
622 | ```js
623 | enforce(1).inside([1, 2]);
624 | // passes
625 | ```
626 |
627 | ```js
628 | enforce(3).inside([1, 2]);
629 | // throws
630 | ```
631 |
632 | - boolean: checks if a number is an element in an array
633 |
634 | ```js
635 | enforce(false).inside([true, false]);
636 | // passes
637 | ```
638 |
639 | ```js
640 | enforce(true).inside([1,2,3]);
641 | // throws
642 | ```
643 |
644 | #### inside: string
645 | - string: checks if a string is inside another string
646 |
647 | ```js
648 | enforce('da').inside('tru dat.');
649 | // passes
650 | ```
651 |
652 | ```js
653 | enforce('ad').inside('tru dat.');
654 | // throws
655 | ```
656 |
657 |
658 | ## notInside
659 | ### Description
660 | Checks if a given value is not contained in another array or string.
661 | Reverse implementation of `inside`.
662 |
663 | ### Usage examples:
664 |
665 | ```js
666 | enforce('ad').notInside('tru dat.');
667 | enforce('hello!').notInside(['hello', 'world']);
668 | // passes
669 | ```
670 |
671 | ```js
672 | enforce('hello').notInside(['hello', 'world']);
673 | enforce('da').notInside('tru dat.');
674 | // throws
675 | ```
676 |
677 |
678 | ## isTruthy
679 | ### Description
680 | Checks if a value is truthy; Meaning: if it can be coerced into boolean `true`.
681 | Anything not in the following list is considered to be truthy.
682 |
683 | * `undefined`
684 | * `null`
685 | * `false`
686 | * `0`
687 | * `NaN`
688 | * empty string (`""`)
689 |
690 | ### Usage examples:
691 |
692 | ```js
693 | enforce("hello").isTruthy();
694 | enforce(true).isTruthy();
695 | enforce(1).isTruthy();
696 | // passes
697 | ```
698 |
699 | ```js
700 | enforce(false).isTruthy();
701 | enforce(null).isTruthy();
702 | enforce(undefined).isTruthy();
703 | enforce(0).isTruthy();
704 | enforce(NaN).isTruthy();
705 | enforce("").isTruthy();
706 | // throws
707 | ```
708 |
709 |
710 | ## isFalsy
711 | ### Description
712 | Checks if a value is falsy; Meaning: if it can be coerced into boolean `false`.
713 | Reverse implementation of `isTruthy`.
714 |
715 | Anything not in the following list is considered to be truthy:
716 | * `undefined`
717 | * `null`
718 | * `false`
719 | * `0`
720 | * `NaN`
721 | * empty string (`""`)
722 |
723 | ### Usage examples:
724 |
725 | ```js
726 | enforce(false).isFalsy();
727 | enforce(0).isFalsy();
728 | enforce(undefined).isFalsy();
729 | // passes
730 | ```
731 |
732 | ```js
733 | enforce(1).isFalsy();
734 | enforce(true).isFalsy();
735 | enforce('hi').isFalsy();
736 | // throws
737 | ```
738 |
739 |
740 | ## isArray
741 | ### Description
742 | Checks if a value is of type `Array`.
743 |
744 | ### Usage examples:
745 |
746 | ```js
747 | enforce(['hello']).isArray();
748 | // passes
749 | ```
750 |
751 | ```js
752 | enforce('hello').isArray();
753 | // throws
754 | ```
755 |
756 |
757 | ## isNotArray
758 | ### Description
759 | Checks if a value is of any type other than `Array`.
760 | Reverse implementation of `isArray`.
761 |
762 | ### Usage examples:
763 |
764 | ```js
765 | enforce(['hello']).isNotArray();
766 | // throws
767 | ```
768 |
769 | ```js
770 | enforce('hello').isNotArray();
771 | // passes
772 | ```
773 |
774 |
775 | ## isNumber
776 | ### Description
777 | Checks if a value is of type `number`.
778 |
779 | ### Usage examples:
780 |
781 | ```js
782 | enforce(143).isNumber();
783 | enforce(NaN).isNumber(); // (NaN is of type 'number!')
784 | // passes
785 | ```
786 |
787 | ```js
788 | enforce([]).isNumber();
789 | enforce("143").isNumber();
790 | // throws
791 | ```
792 |
793 |
794 | ## isNotNumber
795 | ### Description
796 | Checks if a value is of any type other than `number`.
797 | Reverse implementation of `isNumber`.
798 |
799 | ### Usage examples:
800 |
801 | ```js
802 | enforce('143').isNotNumber();
803 | enforce(143).isNotNumber();
804 | // passes
805 | ```
806 |
807 | ```js
808 | enforce(143).isNotNumber();
809 | enforce(NaN).isNotNumber(); // throws (NaN is of type 'number!')
810 | // throws
811 | ```
812 |
813 |
814 | ## isString
815 | ### Description
816 | Checks if a value is of type `String`.
817 |
818 | ### Usage examples:
819 |
820 | ```js
821 | enforce('hello').isString();
822 | // passes
823 | ```
824 |
825 | ```js
826 | enforce(['hello']).isString();
827 | enforce(1984).isString();
828 | // throws
829 | ```
830 |
831 |
832 | ## isNotString
833 | ### Description
834 | Checks if a value is of any type other than `String`.
835 | Reverse implementation of `isString`.
836 |
837 | ### Usage examples:
838 |
839 | ```js
840 | enforce('hello').isNotString();
841 | // throws
842 | ```
843 |
844 | ```js
845 | enforce(['hello']).isNotString();
846 | // passes
847 | ```
848 |
849 |
850 | ## isOdd
851 | ### Description
852 | Checks if a value is an odd numeric value.
853 |
854 | ### Usage examples:
855 |
856 | ```js
857 | enforce('1').isOdd();
858 | enforce(9).isOdd();
859 | // passes
860 | ```
861 |
862 | ```js
863 | enforce(2).isOdd();
864 | enforce('4').isOdd();
865 | enforce('1withNumber').isOdd();
866 | enforce([1]).isOdd();
867 | // throws
868 | ```
869 |
870 |
871 | ## isEven
872 | ### Description
873 | Checks if a value is an even numeric value.
874 |
875 | ### Usage examples:
876 |
877 | ```js
878 | enforce(0).isEven();
879 | enforce('2').isEven();
880 | // passes
881 | ```
882 |
883 | ```js
884 | enforce(1).isEven();
885 | enforce('3').isEven();
886 | enforce('2withNumber').isEven();
887 | enforce([0]).isEven();
888 | // throws
889 | ```
890 |
891 |
892 | # Custom enforce rules
893 | To make it easier to reuse logic across your application, sometimes you would want to encapsulate bits of logic in rules that you can use later on, for example, "what's considered a valid email".
894 |
895 | Your custom rules are essentially a single javascript object containing your rules.
896 | ```js
897 | const myCustomRules = {
898 | isValidEmail: (value) => value.indexOf('@') > -1,
899 | hasKey: (value, {key}) => value.hasOwnProperty(key),
900 | passwordsMatch: (passConfirm, options) => passConfirm === options.passConfirm && options.passIsValid
901 | }
902 | ```
903 | Just like the predefined rules, your custom rules can accepts two parameters:
904 | * `value` The actual value you are testing against.
905 | * `args` (optional) the arguments which you pass on when running your tests.
906 |
907 |
908 | You can extend enforce with your custom rules by creating a new instance of `Enforce` and adding the rules object as the argument.
909 |
910 | ```js
911 | import { Enforce } from 'passable';
912 |
913 | const myCustomRules = {
914 | isValidEmail: (value) => value.indexOf('@') > -1,
915 | hasKey: (value, key) => value.hasOwnProperty(key),
916 | passwordsMatch: (passConfirm, options) => passConfirm === options.passConfirm && options.passIsValid
917 | }
918 |
919 | const enforce = new Enforce(myCustomRules);
920 |
921 | enforce(user.email).isValidEmail();
922 | ```
923 |
--------------------------------------------------------------------------------