Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added: test grouping support #174

Merged
merged 1 commit into from
May 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [The test Function](./test)
- [How to fail a test](./test#failing_a_test)
- [Warn only tests](./warn)
- [Grouping tests](./group)
- [Assertions with enforce](./enforce)
- [Understanding Vest's state](./state)
- Advanced cases
Expand Down
4 changes: 2 additions & 2 deletions docs/draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

In some cases, you want to conditionally run tests based on the results of other tests. For example, preventing validating a field over the server when you already know the field is invalid.

For this you can use `vest.draft()`. The `traft` function returns the intermediate [result object](./result) of the currently running suite.
For this you can use `vest.draft()`. The `draft` function returns the intermediate [result object](./result) of the currently running suite.

**Limitations when using vest.draft()**

- 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 syenc tests.
- 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.
- You may not call draft from outside a running suite. Doing that will result in a thrown error.
- Each `draft()` call returns a copy of your suite result, and its result gets outdated between test calls. Do not try to save `draft` result in a variable for later use, instead, call `draft` whenever you need to use it.

Expand Down
2 changes: 1 addition & 1 deletion docs/enforce.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Enforce
# Enforce

For assertions, Vest is bundled with [Enforce](https://npmjs.com/package/n4s). Enforce is a validation assertion 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.

Expand Down
2 changes: 1 addition & 1 deletion docs/enforce.md.bak
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Enforce
# Enforce
For assertions, Vest is bundled with [Enforce](https://npmjs.com/package/n4s). Enforce is a validation assertion 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.

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 in the chain.
Expand Down
2 changes: 1 addition & 1 deletion docs/exclusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ When performing validations in real world-scenarios, you may need to only run te

## Important to know before using exclusion hooks:

When using `vest.only()` or `vest.skip()` you must place them before any of the tests defined in the suite. Hooks run in order of appearence, which means that if you place your `skip` hook after the filed you're skipping - it won't have any effect.
When using `vest.only()` or `vest.skip()` you must place them before any of the tests defined in the suite. Hooks run in order of appearance, which means that if you place your `skip` hook after the filed you're skipping - it won't have any effect.

### Only running specific tests

Expand Down
4 changes: 2 additions & 2 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Installation
# Installation

To install the stable version of Vest:

Expand Down Expand Up @@ -38,7 +38,7 @@ const validationResult = validate();
| `name` | `string` | No | Suite name. _Must be unique_. |
| callback | `function` | No | Your validation suite's body. This is where your tests reside. |

vest.create returns a `validate` function which runs your validation suite. All the arguments you pass to it are being forwared to your tests callback. You can use it to pass form data to your validation, excluded fields, and anything required for during your validation runtime.
vest.create returns a `validate` function which runs your validation suite. All the arguments you pass to it are being forwarded to your tests callback. You can use it to pass form data to your validation, excluded fields, and anything required for during your validation runtime.

A simple validation suite would look somewhat like this:

Expand Down
144 changes: 144 additions & 0 deletions docs/group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Grouping tests

In many cases it can be helpful to group tests together so you can include or exclude a portion of the suite with a single condition.
Similar to the `describe` and `context` features provided by unit testing frameworks, Vest provides `group`.

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

vest.create('authentication_form', data => {
vest.skip(data.userExists ? 'newUser' : 'existingUser');

test('userName', "Can't be empty", () => {
enforce(data.username).isNotEmpty();
});
test('password', "Can't be empty", () => {
enforce(data.password).isNotEmpty();
});

group('existingUser', () => {
test(
'userName',
'User does not exist. Please check if you typed it correctly.'
);
});

group('newUser', () => {
test('email', 'Email already registered', isEmailRegistered(data.email));

test('age', 'You must be at least 18 years old to join', () => {
enforce(data.age).largerThanOrEquals(18);
});
});
});
```

## Why use `group` and not just wrap the tests with an `if` statement?

In many cases it is sufficient to just use an `if` statement. The benefit of using `group` is that when skipping (either using [vest.skip or vest.only](./exclusion)), Vest will merge the previous group result with the current suite. This is mostly suitable for cases like demonstrated in the first example of the multi stage form.

## Use cases

### 1. Multi stage form

You may have in your application a multi-screen form, in which you want to validate each screen individually, but submit it all at once.

```js
// validation.js
import vest, { test, group, enforce } from 'vest';

const validate = vest.create('product-create', (data, currentTab) => {
vest.only(currentScreen);

group('overview_tab', () => {
test('productTitle', 'Must be at least 5 chars.', () => {
enforce(data.productTitle).longerThanOrEquals(5);
});

test('productDescription', "Can't be longer than 2500 chars.", () => {
enforce(data.productDescription).shorterThanOrEquals(2500);
});

test('productTags', 'Please provide up to 5 tags', () => {
enforce(data.tags).lengthEquals(5);
});
});

group('pricing_tab', () => {
test('price', '5$ or more.', () => {
enforce(data.price).lte(5);
});

test('productExtras', "Can't be empty.", () => {
enforce(data.extras).isNotEmpty();
});
});
});

export default validate;
```

```js
// myFeature.js

validate(data, 'overview_tab'); // will only validate 'overview_tab' group
validate(data, 'pricing_tab'); // will only validate 'pricing_tab' group
```

### 2. Skipping tests with shared fields

You sometimes want to skip some tests on a certain condition, but still run other tests with the same field-name.

In the example below, we don't mind skipping the `balance` field directly, but if we skip the `quantity` field directly, it won't be tested at all - even though it has one test outside of the group. That's why we skip the `used_promo`.

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

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

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

test(
'balance',
'Balance is lower than product price',
hasSufficientFunds(data.productId)
);

test('quantity', `Quantity on this item is limited to ${data.limit}`, () => {
enforce(data.quantity).lessThanOrEquals(data.limit);
});

group('used_promo', () => {
test(
'quantity',
'promo code purchases are limited to one item only',
() => {
enforce(data.quantity).equals(1);
}
);

test(
'promoCode',
'Promo code can only be used once',
isPromoCodeUsed(data.usedPromo)
);
});
});
```

## Querying the result object for groups

Groups represent a portion of your validation suite, so when using `group`, you are likely to need to get the group-specific validation results.
Your result object exposes the following methods:

- [_hasErrorsByGroup_](./result#haserrorsbygroup-and-haswarningsbygroup-functions)
- [_hasWarningsByGroup_](./result#haserrorsbygroup-and-haswarningsbygroup-functions)
- [_hasErrorsByGroup_](./result#geterrorsbygroup-and-getwarningsbygroup-functions)
- [_hasWarningsByGroup_](./result#geterrorsbygroup-and-getwarningsbygroup-functions)

Read more about these methods in [the result object](./result).
64 changes: 57 additions & 7 deletions docs/result.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Vest's result object

Vest validations reuturn a results object that holds all the information regarding the current run, and methods to easily interact with the data.
Vest validations return a results object that holds all the information regarding the current run, and methods to easily interact with the data.

A result object would look somewhat like this:

Expand Down Expand Up @@ -44,6 +44,32 @@ resultObject.hasWarnings();
// true
```

## `hasErrorsByGroup` and `hasWarningsByGroup` functions

Similar to `hasErrors` and `hasWarnings`, but returns the result for a specified [group](./group)

To get the result for a given field in the group:

```js
resultObject.hasErrorsByGroup('groupName', 'fieldName');
// true

resultObject.hasWarningsByGroup('groupName', 'fieldName');
// false
```

And to get the result for a whole group.

```js
resultObject.hasErrorsByGroup('groupName');
// true

resultObject.hasWarningsByGroup('groupName');
// true
```

[Read more about groups](./group)

## `getErrors` and `getWarnings` functions

These functions return an array of errors for the specified field. If no field specified, it returns an object with all fields as keys and their error arrays as values.
Expand All @@ -66,28 +92,52 @@ resultObject.getWarnings('username');
// []
```

You can also call these functions without a field name, which will return you an array per field:

```js
resultObject.getErrors();

// {
// username: ['Username is too short', `Username already exists`],
// password: ['Password must contain special characters']
// }
```

**Note** If you did not specify error messages for your tests, your errors array will be empty as well. In such case you should always rely on `.hasErrors()` instead.

## `getErrorsByGroup` and `getWarningsByGroup` functions

Just like get `getErrors` and `getWarnings`, but narrows the result to a specified [group](./group).

```js
resultObject.getErrorsByGroup('groupName', 'fieldName');
resultObject.getWarningsByGroup('groupName', 'fieldName');
resultObject.getErrorsByGroup('groupName'');
resultObject.getWarningsByGroup('groupName'');
```

[Read more about groups](./group).

## `.done()`

Done is a function that can be chained to your validation suite, and allows invoking callbacks whenever a specific, or all, tests finish their validation - regardless of the validation result.

If we specify a fieldname in our `done` call, vest will not wait for the whole suite to finish before running our callback. It will invoke immediately when all tests with that given name finished running.
If we specify a field name in our `done` call, vest will not wait for the whole suite to finish before running our callback. It will invoke immediately when all tests with that given name finished running.

`.done()` calls can be infinitely chained after one another, and as the validation suite completes - they will all run immediately.

`done` takes one or two arguments:

| Name | Type | Optional | Description |
| ----------- | ---------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| `fieldName` | `String` | Yes | If passed, current done call will not wait for the whole suite to complete, but instead wait for a certain field to finish. |
| `callback` | `Function` | No | A callback to be run when either the whole suite or the specified field finished running. |
| Name | Type | Optional | Description |
| ----------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `fieldName` | `String` | Yes | If passed, the current done call will not wait for the whole suite to complete, but instead wait for a certain field to finish. |
| `callback` | `Function` | No | A callback to be run when either the whole suite or the specified field finished running. |

The result object is being passed down to the `done` object as an argument.

**Example**

In the below example, the `done` callback for `UserName` may run before the whole suite finishes. Only when the rest of the suite finishes, it will call the other two done callbacks that do not have a fieldname specified.
In the below example, the `done` callback for `UserName` may run before the whole suite finishes. Only when the rest of the suite finishes, it will call the other two done callbacks that do not have a field name specified.

```js
import vest, { test, enforce } from 'vest';
Expand Down
2 changes: 1 addition & 1 deletion docs/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This can be done with Vest's [`only()` hook](./exclusion). That's where the stat

When you have skipped fields in your validation suite, vest will try to see if those skipped fields ran in the previous suite, and merge them into the currently running suite result - so the result object you get will include all the fields that your user interacted with.

Vest state is anchored to the suite name (defined in vest.create's first argument), which means the suite name must be unique.
Vest state is anchored to the suite name (defined in `vest.create`'s first argument), which means the suite name must be unique.

## What Vest's state does

Expand Down
2 changes: 1 addition & 1 deletion docs/stateless_validations.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Server side and stateless validations

By default Vest validations are stateful in order to reduce boilerplate code on the consumer side. This is usually fine, but in some cases, usually when performing server side validations, we would like our validations to be statelss - so that validation results do not leak between validation runs.
By default Vest validations are stateful in order to reduce boilerplate code on the consumer side. This is usually fine, but in some cases, usually when performing server side validations, we would like our validations to be stateless - so that validation results do not leak between validation runs.

Writing stateless validations is just like writing stateful validations, only that instead of declaring your suite using `vest.create`, you directly import your validate function from Vest:

Expand Down
11 changes: 6 additions & 5 deletions docs/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ A test can either be synchronous or asynchronous, and it can either have a [seve

There are three ways to fail a test:

### Throwing an errorr inside your test body (using enforce)
### Throwing an error inside your test body (using enforce)

Just like in most unit testing frameworks, a validation fails whenever an error is thrown inside the test body. The [`enforce`](./enforce) function throws an error whenever the enforced value does not meet the specified criteria.

```js
// const username = 'Gina.Vandervort';
// const password = 'Q3O';

test('username', 'Should be at least 3 charachters long', () => {
test('username', 'Should be at least 3 characters long', () => {
enforce(username).longerThanOrEquals(3);
}); // this test passes

test('password', 'Should be at least 6 charachters long', () => {
test('password', 'Should be at least 6 characters long', () => {
enforce(password).longerThanOrEquals(6); // an error is thrown here
}); // this test fails
```
Expand All @@ -39,11 +39,11 @@ To make it easy to migrate your existing validation logic into Vest, it also sup
// const username = 'Gina.Vandervort';
// const password = 'Q3O';

test('username', 'Should be at least 3 charachters long', () => {
test('username', 'Should be at least 3 characters long', () => {
return username.length >= 3; // = true
}); // this test passes

test('password', 'Should be at least 6 charachters long', () => {
test('password', 'Should be at least 6 characters long', () => {
return password.length >= 6; // = false
}); // this test fails
```
Expand Down Expand Up @@ -90,6 +90,7 @@ test('name', () =>
**Read next about:**

- [Warn only tests](./warn).
- [Grouping tests](./group).
- [Asserting with enforce](./enforce).
- [Skipping or including tests](./exclusion).
- [Accessing intermediate suite result](./draft).
2 changes: 1 addition & 1 deletion docs/warn.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ By default, a failing test has a severity of `error`. Sometimes you may need to
To set a test's severity level to `warn`, you need to simply call Vest's warn function from your test body.

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

const validate = vest.create('Password', data => {
test('password', 'A password must have at least 6 characters', () => {
Expand Down