Skip to content
Permalink
Browse files

docs(addAssertion): document alternations, flags and optional values (#…

…668)

* docs(addAssertion): document alternations, flags and optional values

* docs(addAssertion): fix sample output

* docs(addAssertion): fix code formatting

* docs(addAssertion): make improvements based on PR comments

* docs(addAssertion): improve wording

* docs(addAssertion): improve wording

* docs(addAssertion): inline handlers, improve wording

* docs(addAssertion): fix typos

* docs(addAssertion): match headings

* docs(addAssertion): simplify alternations example
  • Loading branch information
joelmukuthu committed Dec 2, 2019
1 parent a7f5e05 commit 9c35e90a2b10aea1892cbbeea6196ede610447a0
Showing with 275 additions and 54 deletions.
  1. +275 −54 documentation/api/addAssertion.md
@@ -10,82 +10,303 @@ expect.addAssertion(pattern, handler);
expect.addAssertion([pattern, ...]], handler);
```

New assertions can be added to Unexpected the following way.
`expect.addAssertion` takes two arguments:

1. a string pattern (or an array of patterns) that describes the assertion.
2. a handler function that is called when the assertion is invoked.

For example:

```js
expect.addAssertion('<array> to have item <any>', function(
expect,
subject,
value
) {
expect(subject, 'to contain', value);
});
```

A handler function can use other assertions, including other custom assertions
previously added via `expect.addAssertion`. This way, one could build up complex assertions from simpler ones or just reword an existing assertion, like in this example.

The new assertion can then be used as follows:

```js
expect([1, 2, 3], 'to have item', 2);
```

The first parameter to `addAssertion` is a string or an array of strings
describing the pattern(s) the assertion should match. The pattern takes the
following structure:

`<subject type> an assertion string <value type>`

The words in angle brackets define what types the assertion applies to. These
can be any of the internally-defined types or new types added via
[expect.addType](../addType). In this example, the subject to `to have item`
must be an array, while the value may be of any type.

If mismatching types are used when an assertion is invoked, Unexpected throws an
error with a helpful suggestion:

```js
expect('abcd', 'to have item', 'a');
```

```output
expected 'abcd' to have item 'a'
The assertion does not have a matching signature for:
<string> to have item <string>
did you mean:
<array> to have item <any>
```

An assertion may only have one `<subject type>` definition, followed by the
desired assertion string, followed by zero or more `<value type>` definitions.
It's not possible to add words between the value-type definitions. For instance,
an assertion such as `<number> to be between <number> and <number>` could
instead be written as:

```js
expect.addAssertion('<number> to be between <number> <number>', function(
expect,
subject,
value1,
value2
) {
expect(subject, 'to be greater than', value1).and('to be less than', value2);
});
```

```js
expect(2, 'to be between', 1, 3);
```

Assertions that support different subject or value types can be defined as
follows:

<!-- unexpected-markdown freshExpect:true -->

```js
expect.addAssertion('<array> to have item <number|string>', function(
expect,
subject,
value
) {
expect(subject, 'to contain', value);
});
```

This would make the assertion more strict, only allowing number and string
values but not boolean values, for example:

```js
expect([1, 2, 3], 'to have item', 2);
expect(['a', 'b', 'c'], 'to have item', 'a');
expect([true, false], 'to have item', true);
```

```output
expected [ true, false ] to have item true
The assertion does not have a matching signature for:
<array> to have item <boolean>
did you mean:
<array> to have item <number|string>
```

## Alternations

Different versions of the same assertion, or different assertions that share the
same handler function, can be added using an array:

<!-- unexpected-markdown freshExpect:true -->

```js
var errorMode = 'default'; // use to control the error mode later in the example
expect.addAssertion(
'<array> [not] to be (sorted|ordered) <function?>',
function(expect, subject, cmp) {
expect.errorMode = errorMode;
expect(subject, '[not] to equal', [].concat(subject).sort(cmp));
['<array> to have item <any>', '<array> to have value <any>'],
function(expect, subject, value) {
expect(subject, 'to contain', value);
}
);
```

The above assertion definition makes the following expects possible:
```js
expect([1, 2, 3], 'to have item', 2);
expect([1, 2, 3], 'to have value', 3);
```

However, when it's a small deviation, as in this case, an alternation is more
handy:

<!-- unexpected-markdown freshExpect:true -->

```js
expect([1, 2, 3], 'to be sorted');
expect([1, 2, 3], 'to be ordered');
expect([2, 1, 3], 'not to be sorted');
expect([2, 1, 3], 'not to be ordered');
expect([3, 2, 1], 'to be sorted', function(x, y) {
return y - x;
expect.addAssertion('<array> to have (item|value) <any>', function(
expect,
subject,
value
) {
expect(subject, 'to contain', value);
});
```

Let's dissect the different parts of the custom assertion we just
introduced.
```js
expect([1, 2, 3], 'to have item', 2);
expect([1, 2, 3], 'to have value', 3);
```

The first parameter to `addAssertion` is a string or an array of strings
stating the patterns this assertion should match. A pattern has the following
syntax. A word in angle brackets represents a type of either the subject
or one of the parameters to the assertion. In this case the assertion
is only defined for arrays. If no subject type is specified,
the assertion will be defined for the type `any`, and would
be applicable any type. See the `Extending Unexpected with new types`
section for more information about the type system in Unexpected.

A word in square brackets represents a flag that can either be
there or not. If the flag is present `expect.flags[flag]` will contain
the value `true`. In this case `not` is a flag. When a flag it present
in a nested `expect` it will be inserted if the flag is present;
otherwise it will be removed. Text that is in parentheses with
vertical bars between them are treated as alternative texts that can
be used. In this case you can write _ordered_ as an alternative to
_sorted_.

An assertion can only have a single assertion string, which must be
provided after the subject type. This means that you cannot have
more words, flags, and alternations after the first type.

The second and last parameter to `addAssertion` is function that will
be called when `expect` is invoked with an expectation matching the
type and pattern of the assertion.

So in this case, when `expect` is called the following way:
Alternations allow branching, similar to an `if..else` statement. They are made
available to the handler function as an `expect.alternations` array which
contains the word used when the assertion is invoked:

<!-- unexpected-markdown evaluate:false -->
<!-- unexpected-markdown freshExpect:true -->

```js
expect.addAssertion('<array> to have (index|value) <any>', function(
expect,
subject,
value
) {
if (expect.alternations[0] === 'index') {
expect(subject[value], 'to be defined');
} else {
expect(subject, 'to contain', value);
}
});
```

```js
expect([3, 2, 1], 'to be sorted', reverse);
expect(['a', 'b'], 'to have index', 1);
expect(['a', 'b'], 'to have value', 'b');
```

The handler to our assertion will be called with the values the
following way, where the _not_ flag in the nested expect will be
removed:
## Flags

<!-- unexpected-markdown evaluate:false -->
<!-- eslint-skip -->
Flags allow assertions to define modifiers which can alter the behaviour of the assertion. The most common example is the `not` flag which requests that
the assertion be negated:

<!-- unexpected-markdown freshExpect:true -->

```js
expect.addAssertion('<array> [not] to have item <any>', function(
expect,
subject,
value
) {
if (expect.flags.not) {
expect(subject, 'not to contain', value);
} else {
expect(subject, 'to contain', value);
}
});
```

This makes the following assertions possible:

```js
expect.addAssertion('<array> [not] to be (sorted|ordered) <function?>', function(expect, [3,2,1], reverse){
expect([3,2,1], '[not] to equal', [].concat([3,2,1]).sort(reverse));
expect([1, 2, 3], 'to have item', 2);
expect([1, 2, 3], 'not to have item', 4);
```

Flags are made available to the handler function as an `expect.flags` object,
where the keys are the names of the flags and the values are `true`, if the flag
is used, or otherwise `undefined`.

This example could be improved further. Since
[to contain](../../assertions/array-like/to-contain/) also supports the `not`
flag, one can propagate the flag to that assertion as follows:

<!-- unexpected-markdown freshExpect:true -->

```js
expect.addAssertion('<array> [not] to have item <any>', function(
expect,
subject,
value
) {
expect(subject, '[not] to contain', value);
});
```

In this way, when `to have item` is invoked with the `not` flag, that flag will
be passed along to `to contain`.

When flags are propagated, one can also invert the flag as follows:

<!-- unexpected-markdown freshExpect:true -->

```js
expect.addAssertion('<array> [not] to have item <any>', function(
expect,
subject,
value
) {
expect(subject, '[!not] to contain', value);
});
```

This means that if `to have item` is invoked with the `not` flag, that flag will
not be propagated to `to contain` - and vice versa:

```js
expect([1, 2, 3], 'not to have item', 2);
expect([1, 2, 3], 'to have item', 4);
```

Fun with flags, right? Flags can also be used to define optional filler words
that make an assertion read better:

<!-- unexpected-markdown freshExpect:true -->

```js
expect.addAssertion('<array> to have [this] item <any>', function(
expect,
subject,
value
) {
expect(subject, 'to contain', value);
});
```

```js
expect([1, 2, 3], 'to have item', 2);
expect([1, 2, 3], 'to have this item', 2);
```

## Optional values

Assertions where a value is optional can be defined by adding a `?` after the
value's type definition. For instance, this can be used to define optional
`function` values:

```js
var errorMode = 'default'; // use to control the error mode in later examples
expect.addAssertion(
'<array> [not] to be (sorted|ordered) [by] <function?>',
function(expect, subject, cmp) {
expect.errorMode = errorMode;
expect(subject, '[not] to equal', [].concat(subject).sort(cmp));
}
);
```

Which can then be used as follows:

```js
expect([1, 2, 3], 'to be sorted');
expect([1, 2, 3], 'to be ordered');
expect([2, 1, 3], 'not to be sorted');
expect([2, 1, 3], 'not to be ordered');
expect([3, 2, 1], 'to be sorted', function(x, y) {
return y - x;
});
expect([3, 2, 1], 'to be sorted by', function(x, y) {
return y - x;
});
```

### Overriding the standard error message
## Overriding the standard error message

When you create a new assertion Unexpected will generate an error
message from the assertion text and the input arguments. In some cases
@@ -155,7 +376,7 @@ expect(4, 'to be similar to', 4.0001);
expected 4 to be similar to 4.0001, (epsilon: 1e-9)
```

### Controlling the output of nested expects
## Controlling the output of nested expects

When a call to `expect` fails inside your assertion the standard error
message for the custom assertion will be used. In the case of our
@@ -266,7 +487,7 @@ expect([1, 3, 2, 4], 'to be sorted');
]
```

### Asynchronous assertions
## Asynchronous assertions

Unexpected comes with built-in support for asynchronous
assertions. You basically just return a promise from the assertion.

0 comments on commit 9c35e90

Please sign in to comment.
You can’t perform that action at this time.