Skip to content

Commit

Permalink
feat: vest.skipWhen for conditional test exclusion (#611)
Browse files Browse the repository at this point in the history
  • Loading branch information
ealush committed May 11, 2021
1 parent b9a7407 commit 09b2aa4
Show file tree
Hide file tree
Showing 21 changed files with 280 additions and 112 deletions.
2 changes: 0 additions & 2 deletions packages/vest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,3 @@ export default vest.create('user_form', (data = {}, currentField) => {
- 🧱 Your validations are structured, making it very simple to read and write. All validation files look the same.
- 🖇 Your validation logic is separate from your feature logic, preventing the spaghetti code that's usually involved with writing validations.
- 🧩 Validation logic is easy to share and reuse across features.

**Vest is an evolution of [Passable](https://github.com/fiverr/passable) by Fiverr.**
51 changes: 25 additions & 26 deletions packages/vest/docs/cross_field_validations.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ the `any` utility will run each function until a passing test is found. This mea
Demo: https://codesandbox.io/s/demo-forked-tdj92?file=/src/validate.js

```js
import vest, { test, enforce } from "vest";
import any from "vest/any";
import vest, { test, enforce } from 'vest';
import any from 'vest/any';

export default vest.create("form_name", (data = {}) => {
export default vest.create('form_name', (data = {}) => {
any(
() =>
test("email", "Email or phone must be set", () => {
test('email', 'Email or phone must be set', () => {
enforce(data.email).isNotEmpty();
}),
() =>
test("phone", "Email or phone must be set", () => {
test('phone', 'Email or phone must be set', () => {
enforce(data.phone).isNotEmpty();
})
);
Expand All @@ -43,11 +43,11 @@ You could also use any within your test, if you have a joined test for both scen
Demo: https://codesandbox.io/s/demo-forked-ltn8l?file=/src/validate.js

```js
import vest, { test, enforce } from "vest";
import any from "vest/any";
import vest, { test, enforce } from 'vest';
import any from 'vest/any';

export default vest.create("form_name", (data = {}) => {
test("email_or_phone", "Email or phone must be set", () =>
export default vest.create('form_name', (data = {}) => {
test('email_or_phone', 'Email or phone must be set', () =>
any(
() => {
enforce(data.email).isNotEmpty();
Expand All @@ -62,47 +62,46 @@ export default vest.create("form_name", (data = {}) => {
});
```

## if/else for conditionally skipping fields
## vest.skipWhen for conditionally skipping fields

If your field depends on a different field's existence or a different simple condition, you could use a basic if/else statement.
If your field depends on a different field's existence or a different simple condition, you could use `skipWhen`.
In the following example I only validate `confirm` if password is not empty:

DEMO: https://codesandbox.io/s/demo-forked-z2ur9?file=/src/validate.js

```js
import vest, { test, enforce } from "vest";
import vest, { test, enforce } from 'vest';

export default vest.create("user_form", (data = {}) => {
test("password", "Password is required", () => {
export default vest.create('user_form', (data = {}) => {
test('password', 'Password is required', () => {
enforce(data.password).isNotEmpty();
});

if (data.password) {
test("confirm", "Passwords do not match", () => {
vest.skipWhen(!!data.password, () => {
test('confirm', 'Passwords do not match', () => {
enforce(data.confirm).equals(data.password);
});
}
});
});
```

## if/else for conditionally skipping field based on a previous result
## vest.skipWhen for conditionally skipping field based on a previous result

Sometimes you might want to run a certain validation based on the validation result of a previously run test, for example - only test for password strength if password DOESN'T have Errors. You could access the intermediate validation result and use it mid-run.

This requires using the function created from vest.create():

```js
import vest, { test, enforce } from "vest";
import vest, { test, enforce } from 'vest';

const suite = vest.create("user_form", (data = {}) => {
test("password", "Password is required", () => {
const suite = vest.create('user_form', (data = {}) => {
test('password', 'Password is required', () => {
enforce(data.password).isNotEmpty();
});

if (!suite.get().hasErrors('password')) {
test("password", "Password is weak", () => {
vest.skipWhen(suite.get().hasErrors('password'), () => {
test('password', 'Password is weak', () => {
enforce(data.password).longerThan(8);
});
}
});
});
export default suite;
```
28 changes: 25 additions & 3 deletions packages/vest/docs/exclusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ In this case, and in similar others, you can use `vest.skip()`. When called, it
import vest, { enforce, test } from 'vest';

const suite = vest.create('purchase', data => {
if (!data.promo) {
vest.skip('promo');
}
if (!data.promo) vest.skip('promo');

// this test won't run when data.promo is falsy.
test('promo', 'Promo code is invalid', () => {
Expand All @@ -55,6 +53,30 @@ const suite = vest.create('purchase', data => {
const validationResult = suite(formData);
```

## Conditionally excluding portions of the suite

In some cases we might need to skip a test or a group based on a given condition. In these cases, we may find it easier to use vest.skipWhen which takes a boolean expression and a callback with the tests to run. This is better than simply wrapping the tests with an if/else statement because they can are still listed in the suite result as skipped.

In the following example we're skipping the server side verification of the username if the username is invalid to begin with:

```js
import vest, { test, enforce } from 'vest';

const suite = vest.create('user_form', (data = {}) => {
test('username', 'Username is required', () => {
enforce(data.username).isNotEmpty();
});

vest.skipWhen(suite.get().hasErrors('password'), () => {
test('username', 'Username already exists', () => {
// this is an example for a server call
return doesUserExist(data.username);
});
});
});
export default suite;
```

## Including and excluding groups of tests

Similar to the way you use `vest.skip` and `vest.only` to include and exclude tests, you can use `vest.skip.group` and `vest.only.group` to exclude and include whole groups.
Expand Down
9 changes: 2 additions & 7 deletions packages/vest/docs/group.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,8 @@ In the example below, we don't mind skipping the `balance` field directly, but i
import vest, { test, group, enforce } from 'vest';

const suite = vest.create('checkout_form', data => {
if (!data.usedPromo) {
vest.skip.group('used_promo');
}

if (!data.paysWithBalance) {
vest.skip.group('balance');
}
if (!data.usedPromo) vest.skip.group('used_promo');
if (!data.paysWithBalance) vest.skip.group('balance');

test(
'balance',
Expand Down
5 changes: 4 additions & 1 deletion packages/vest/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/docsify-themeable@0/dist/css/theme-simple.css"
/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.22.0/themes/prism-tomorrow.css">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/prismjs@1.22.0/themes/prism-tomorrow.css"
/>
</head>
<body>
<div id="app"></div>
Expand Down
4 changes: 2 additions & 2 deletions packages/vest/docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ const suite = vest.create('user_form', () => {

```js
const suite = vest.create('user_form', () => {
if (suite.get().hasErrors('username')) {
vest.skipWhen(suite.get().hasErrors('username'), () => {
/* ... */
}
});
});
```

Expand Down
4 changes: 2 additions & 2 deletions packages/vest/docs/result.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ const suite = vest.create('my_form', data => {
enforce(data.username).longerThanOrEquals(3);
});

if (!suite.get().hasErrors('username')) {
vest.skipWhen(suite.get().hasErrors('username'), () => {
test('username', 'already taken', async () => {
// some async test
});
}
});
});
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ Object {
"group": [Function],
"only": [Function],
"skip": [Function],
"skipWhen": [Function],
"test": [Function],
"warn": [Function],
}
Expand Down
1 change: 0 additions & 1 deletion packages/vest/src/core/produce/produce.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import createCache from 'cache';
import context from 'ctx';
import genTestsSummary from 'genTestsSummary';
Expand Down
1 change: 0 additions & 1 deletion packages/vest/src/core/test/__tests__/runAsyncTest.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import _ from 'lodash';


import expandStateRef from '../../../../testUtils/expandStateRef';
import runCreateRef from '../../../../testUtils/runCreateRef';

Expand Down
3 changes: 1 addition & 2 deletions packages/vest/src/core/test/lib/__tests__/VestTest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import VestTest from 'VestTest';
import addTestToState from 'addTestToState';
import context from 'ctx';
import { setPending } from 'pending';
import { usePending , useTestObjects } from 'stateHooks';

import { usePending, useTestObjects } from 'stateHooks';

const fieldName = 'unicycle';
const statement = 'I am Root.';
Expand Down
1 change: 0 additions & 1 deletion packages/vest/src/core/test/lib/__tests__/pending.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import _ from 'lodash';


import expandStateRef from '../../../../../testUtils/expandStateRef';
import runCreateRef from '../../../../../testUtils/runCreateRef';

Expand Down
1 change: 0 additions & 1 deletion packages/vest/src/core/test/lib/removeTestFromState.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import asArray from 'asArray';
import removeElementFromArray from 'removeElementFromArray';
import { useTestObjects } from 'stateHooks';
Expand Down
3 changes: 2 additions & 1 deletion packages/vest/src/core/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const register = testObject => {
* Changes to this function need to reflect in test.memo as well
*/
function test(fieldName, args) {
const { skip } = context.use();
const [testFn, statement] = args.reverse();
const [, setSkippedTests] = useSkippedTests();

Expand All @@ -91,7 +92,7 @@ function test(fieldName, args) {
testFn,
});

if (isExcluded(testObject)) {
if (skip || isExcluded(testObject)) {
setSkippedTests(skippedTests => skippedTests.concat(testObject));
return testObject;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`exclusive hooks \`skipWhen\` hook When \`shouldSkip\` is \`false\` Should produce correct validation result 1`] = `
Object {
"done": [Function],
"errorCount": 3,
"getErrors": [Function],
"getErrorsByGroup": [Function],
"getWarnings": [Function],
"getWarningsByGroup": [Function],
"groups": Object {
"group": Object {
"field2": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
},
},
"hasErrors": [Function],
"hasErrorsByGroup": [Function],
"hasWarnings": [Function],
"hasWarningsByGroup": [Function],
"name": undefined,
"testCount": 3,
"tests": Object {
"field1": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"field2": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"field3": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
},
"warnCount": 0,
}
`;
81 changes: 81 additions & 0 deletions packages/vest/src/hooks/__tests__/exclusive.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,87 @@ describe('exclusive hooks', () => {
});
});

describe('`skipWhen` hook', () => {
let suite, fn1, fn2, fn3;

beforeEach(() => {
fn1 = jest.fn(() => false);
fn2 = jest.fn(() => false);
fn3 = jest.fn(() => false);
suite = vest.create(shouldSkip => {
vest.skipWhen(shouldSkip, () => {
vest.test('field1', fn1);
vest.group('group', () => {
vest.test('field2', fn2);
});
});
vest.test('field3', fn3);
});
});

describe('When `shouldSkip` is `true`', () => {
it('Should skip all tests within the callback', () => {
expect(fn1).not.toHaveBeenCalled();
expect(fn2).not.toHaveBeenCalled();
expect(fn3).not.toHaveBeenCalled(); // sanity
suite(true);
expect(fn1).not.toHaveBeenCalled();
expect(fn2).not.toHaveBeenCalled();
expect(fn3).toHaveBeenCalled(); // sanity
});

it('Should list all skipped tests as skipped', () => {
const res = suite(true);
expect(res.tests).toMatchInlineSnapshot(`
Object {
"field1": Object {
"errorCount": 0,
"testCount": 0,
"warnCount": 0,
},
"field2": Object {
"errorCount": 0,
"testCount": 0,
"warnCount": 0,
},
"field3": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
}
`);
expect(res.groups).toMatchInlineSnapshot(`
Object {
"group": Object {
"field2": Object {
"errorCount": 0,
"testCount": 0,
"warnCount": 0,
},
},
}
`);
});
});

describe('When `shouldSkip` is `false`', () => {
it('Should run all tests', () => {
expect(fn1).not.toHaveBeenCalled();
expect(fn2).not.toHaveBeenCalled();
expect(fn3).not.toHaveBeenCalled();
suite(false);
expect(fn1).toHaveBeenCalled();
expect(fn2).toHaveBeenCalled();
expect(fn3).toHaveBeenCalled();
});

it('Should produce correct validation result', () => {
expect(suite(false)).toMatchSnapshot();
});
});
});

describe('Error handling', () => {
let mockThrowError, hooks;

Expand Down
Loading

0 comments on commit 09b2aa4

Please sign in to comment.