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

Create jest structure #107

Merged
merged 22 commits into from Oct 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4033b26
Begin to implement assertions
talyssonoc Oct 23, 2019
813486a
Migrate boolean validation assertions to use jest-structure
talyssonoc Oct 23, 2019
a05b077
Assert no expected for valid/invalid matchers
talyssonoc Oct 23, 2019
4777ab8
Assert attribute path is passed
talyssonoc Oct 23, 2019
56ec941
Add package to automatically add assertions to jest
talyssonoc Oct 23, 2019
c4fa253
Begin to write tests
talyssonoc Oct 23, 2019
938c6e3
Add tests for validating error messages
talyssonoc Oct 23, 2019
cce291b
Reorganize and begin to write toHaveInvalidAttributes
talyssonoc Oct 28, 2019
5f227d3
Implement toHaveInvalidAttributes and cover with tests
talyssonoc Oct 29, 2019
80c23e4
Rename auto.js to extend-expect.js
talyssonoc Oct 29, 2019
8487692
Refactor checking of path presence and usage hint
talyssonoc Oct 29, 2019
b45f967
Refactor and remove hint from validity check of toHaveInvalidAttributes
talyssonoc Oct 29, 2019
4406c95
Remove hint from validity check of toHaveInvalidAttribute
talyssonoc Oct 29, 2019
88ede3b
Refactor checking of attribute validity
talyssonoc Oct 29, 2019
7cf2dc1
Refactor usage error and remove hint for non-usage-related errors
talyssonoc Oct 29, 2019
8899957
Replace usage of assertValid with expect().toBeValid()
talyssonoc Oct 29, 2019
96a08ae
Replace validitationMatchers with jest-structure in array specs
talyssonoc Oct 29, 2019
3d6eebd
Migrate whole structure spec to use jest-structure
talyssonoc Oct 30, 2019
3471dde
Rename validity assertions to contain 'structure' in the name
talyssonoc Oct 30, 2019
fefb1f7
Fix coverage script
talyssonoc Oct 30, 2019
bb393f9
Add tests for when using arrayContaining with jest-structure
talyssonoc Oct 30, 2019
cbe1706
Add readme
talyssonoc Oct 30, 2019
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
3 changes: 3 additions & 0 deletions packages/jest-structure/.eslintrc.js
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('../../.eslintrc')],
};
1 change: 1 addition & 0 deletions packages/jest-structure/.gitignore
@@ -0,0 +1 @@
distTest/
1 change: 1 addition & 0 deletions packages/jest-structure/.prettierignore
@@ -0,0 +1 @@
README.md
1 change: 1 addition & 0 deletions packages/jest-structure/.prettierrc.js
@@ -0,0 +1 @@
module.exports = require('../../.prettierrc.json');
178 changes: 178 additions & 0 deletions packages/jest-structure/README.md
@@ -0,0 +1,178 @@
# jest-structure

Custom [Jest](https://www.npmjs.com/package/jest) matchers to test [Structure](https://www.npmjs.com/package/structure) instances.

## Example usage

```js
expect(user).toBeValidStructure()

expect(user).toBeInvalidStructure()

expect(user).toHaveInvalidAttribute(['name'])

expect(user).toHaveInvalidAttribute(['name'], ['"name" is required'])

expect(user).toHaveInvalidAttribute(['name'], expect.arrayContaining(['"name" is required']))

expect(user).toHaveInvalidAttributes([
{ path: ['name'], messages: expect.arrayContaining(['"name" is required']) },
{
path: ['age'],
messages: [
'"age" must be larger than or equal to 2',
'"age" must be a positive number'
]
}
])
```


## Installation

jest-structure is available in npm, so you can install it with npm or yarn as a development dependency:

```sh
npm install --save-dev jest-structure

# or

yarn --dev add jest-structure
```


## Setup

After installing, you need to tell Jest to use jest-structure, this can be done in two ways:

1. By importing and manually adding it to Jest (in a setup file or directly in the top of your test file):

```js
import jestStructure from 'jest-structure'

expect.extend(jestStructure);
```

2. By allowing jest-structure to add itself to Jest matchers:

```js
import 'jest-structure/extend-expect'
```

## Matchers

### `toBeValidStructure()`

This matcher passes if the structure is _valid_:

```js
const User = attributes({
name: { type: String, required: true }
})(class User {})

const validUser = new User({ name: 'Me' })

expect(validUser).toBeValidStructure() // passes

const invalidUser = new User()

expect(invalidUser).toBeValidStructure() // fails
```

### `toBeInvalidStructure()`

This matcher passes if the structure is _invalid_:

```js
const User = attributes({
name: { type: String, required: true }
})(class User {})

const invalidUser = new User()

expect(invalidUser).toBeInvalidStructure() // passes

const validUser = new User({ name: 'Me' })

expect(validUser).toBeInvalidStructure() // fails
```

## `toHaveInvalidAttribute(path, messages)`

This matcher allows you to assert that a _single attribute_ of the structure is invalid, optionally passing the array of error messages for that attribute:

```js
const User = attributes({
name: { type: String, required: true },
age: { type: Number, required: true }
})(class User {})

const user = new User({ age: 42 })

// passes, because name is invalid
expect(user).toHaveInvalidAttribute(['name'])

// fails, because age is valid
expect(user).toHaveInvalidAttribute(['age'])

// passes, because name is invalid with this message
expect(user).toHaveInvalidAttribute(['name'], ['"name" is required'])

// fails, because name is invalid but not with this message
expect(user).toHaveInvalidAttribute(['name'], ['"name" is not cool'])

// passes. Notice that you can even use arrayContaining to check for a subset of the errros
expect(user).toHaveInvalidAttribute(['name'], expect.arrayContaining(['"name" is required']))

// passes. And stringContaining can be used as well
expect(user).toHaveInvalidAttribute(['name'], [expect.stringContaining('required')])
```

## `toHaveInvalidAttributes([ { path, messages } ])`

This matcher allows you to assert that _multiple attributes_ of the structure are invalid, optionally passing the array of error messages for each attribute:

```js
const User = attributes({
name: { type: String, required: true },
age: { type: Number, required: true }
})(class User {})

const user = new User({ age: 42 })

// passes, because name is invalid
expect(user).toHaveInvalidAttributes([
{ path: ['name'] }
])

// fails, because age is valid
expect(user).toHaveInvalidAttributes([
{ path: ['age'] }
])

// fails, because name is invalid but age is valid
expect(user).toHaveInvalidAttributes([
{ path: ['name'] },
{ path: ['age'] }
])

// passes, because name is invalid with this message
expect(user).toHaveInvalidAttributes([
{ path: ['name'], messages: ['"name" is required'] }
])

// fails, because name is invalid but not with this message
expect(user).toHaveInvalidAttributes([
{ path: ['name'], messages: ['"name" is not cool'] }
])

// passes. Notice that you can even use arrayContaining to check for a subset of the errros
expect(user).toHaveInvalidAttributes([
{ path: ['name'], messages: expect.arrayContaining(['"name" is required']) }
])

// passes. And stringContaining can be used as well
expect(user).toHaveInvalidAttributes([
{ path: ['name'], messages: [expect.stringContaining('required')] }
])
```
1 change: 1 addition & 0 deletions packages/jest-structure/extend-expect.js
@@ -0,0 +1 @@
expect.extend(require('./'));
6 changes: 6 additions & 0 deletions packages/jest-structure/index.js
@@ -0,0 +1,6 @@
module.exports = {
toBeValidStructure: require('./src/assertions/toBeValidStructure'),
toBeInvalidStructure: require('./src/assertions/toBeInvalidStructure'),
toHaveInvalidAttribute: require('./src/assertions/toHaveInvalidAttribute'),
toHaveInvalidAttributes: require('./src/assertions/toHaveInvalidAttributes'),
};
18 changes: 18 additions & 0 deletions packages/jest-structure/package.json
@@ -0,0 +1,18 @@
{
"name": "jest-structure",
"version": "2.0.0-alpha",
"description": "Jest assertions to use with Structure",
"main": "index.js",
"author": "Talysson <talyssonoc@gmail.com>",
"license": "MIT",
"scripts": {
"test": "jest"
},
"devDependencies": {
"structure": "2.0.0-alpha"
},
"peerDependencies": {
"jest": "^24.0.0"
},
"dependencies": {}
}
@@ -0,0 +1,7 @@
const createValidityAssertion = require('../lib/validityAssertion');

module.exports = createValidityAssertion({
pass: (valid) => !valid,
passName: 'invalid',
failName: 'valid',
});
7 changes: 7 additions & 0 deletions packages/jest-structure/src/assertions/toBeValidStructure.js
@@ -0,0 +1,7 @@
const createValidityAssertion = require('../lib/validityAssertion');

module.exports = createValidityAssertion({
pass: (valid) => valid,
passName: 'valid',
failName: 'invalid',
});
68 changes: 68 additions & 0 deletions packages/jest-structure/src/assertions/toHaveInvalidAttribute.js
@@ -0,0 +1,68 @@
const { sortMessagesByExpected } = require('../lib/sorting');
const { isValidPath } = require('../lib/attributePath');
const { failInvalidUsage, failNoNegative, failWrongValidity } = require('../lib/errors');
const matcherName = 'toHaveInvalidAttribute';
const exampleName = 'structure';
const attributePathHint = 'attributePath';
const errorMessagesHint = '[errorMessages]';

module.exports = function toHaveInvalidAttribute(structure, attributePath, expectedErrorMessages) {
if (this.isNot) {
return failNoNegative(matcherName);
}

if (!isValidPath(attributePath)) {
return failInvalidUsage(
matcherName,
usageHint(this),
'must not be called without the attribute path'
);
}

const { valid, errors } = structure.validate();

if (valid) {
return failWrongValidity({
pass: false,
passName: 'invalid',
failName: 'valid',
context: this,
});
}

const attributeErrors = errors.filter((error) => this.equals(error.path, attributePath));

const joinedAttributeName = attributePath.join('.');

if (isExpectedAttributeValid(expectedErrorMessages, attributeErrors)) {
return {
pass: Boolean(attributeErrors.length),
message: () =>
`Expected: ${joinedAttributeName} to be ${this.utils.EXPECTED_COLOR('invalid')}\n` +
`Received: ${joinedAttributeName} is ${this.utils.RECEIVED_COLOR('valid')}`,
};
}

const validationErrorMessages = attributeErrors.map((error) => error.message);
const errorMessages = sortMessagesByExpected(validationErrorMessages, expectedErrorMessages);

return {
pass: this.equals(errorMessages, expectedErrorMessages),
message: () =>
this.utils.printDiffOrStringify(
expectedErrorMessages,
errorMessages,
`Expected ${joinedAttributeName} error messages`,
`Received ${joinedAttributeName} error messages`,
this.expand
),
};
};

const usageHint = (context) =>
context.utils.matcherHint(matcherName, exampleName, attributePathHint, {
secondArgument: errorMessagesHint,
});

const isExpectedAttributeValid = (expectedErrorMessages, attributeErrors) =>
!(expectedErrorMessages && attributeErrors.length);
61 changes: 61 additions & 0 deletions packages/jest-structure/src/assertions/toHaveInvalidAttributes.js
@@ -0,0 +1,61 @@
const { sortErrorsByExpected } = require('../lib/sorting');
const { areExpectedErrorsPathsValid } = require('../lib/attributePath');
const { failInvalidUsage, failNoNegative, failWrongValidity } = require('../lib/errors');
const matcherName = 'toHaveInvalidAttributes';
const exampleName = 'structure';
const expectedErrorsHint = '[{ path (required), messages (optional) }]';

module.exports = function toHaveInvalidAttributes(structure, expectedErrors) {
if (this.isNot) {
return failNoNegative(matcherName);
}

if (!areExpectedErrorsPresent(expectedErrors)) {
return failInvalidUsage(
matcherName,
usageHint(this),
'must not be called without the expected errros'
);
}

const { valid, errors } = structure.validate();

if (valid) {
return failWrongValidity({
pass: false,
passName: 'invalid',
failName: 'valid',
context: this,
});
}

if (!areExpectedErrorsPathsValid(expectedErrors)) {
return failNoPath(this);
}

const errorsForComparison = sortErrorsByExpected(errors, expectedErrors, this);

return {
pass: this.equals(errorsForComparison, expectedErrors),
message: () =>
this.utils.printDiffOrStringify(
expectedErrors,
errorsForComparison,
`Expected errors`,
`Received errors`,
this.expand
),
};
};

const areExpectedErrorsPresent = (expectedErrors) => expectedErrors && expectedErrors.length;

const usageHint = (context) =>
context.utils.matcherHint(matcherName, exampleName, expectedErrorsHint);

const failNoPath = (context) => ({
pass: false,
message: () =>
`${matcherName} must not be called without the attribute paths\n` +
`Example: ${usageHint(context)}`,
});
20 changes: 20 additions & 0 deletions packages/jest-structure/src/lib/attributePath.js
@@ -0,0 +1,20 @@
const isValidPath = (path) => Boolean(path && path.length);

const areExpectedErrorsPathsValid = (expectedErrors) => expectedErrors.every(errorHasPath);
const errorHasPath = (error) => isValidPath(error.path);

const groupByPath = (errors, context) =>
errors.reduce((grouped, error) => {
const group = grouped.find((group) => context.equals(group.path, error.path));

if (group) {
group.messages.push(error.message);
return grouped;
}

const newGroup = { path: error.path, messages: [error.message] };

return [...grouped, newGroup];
}, []);

module.exports = { isValidPath, areExpectedErrorsPathsValid, groupByPath };