Skip to content

added: [vest] hook vest.get #202

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

Merged
merged 1 commit into from
Jun 13, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/jest/jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const glob = require('glob');

const {
default: isDeepCopy,
} = require('../../packages/vest/testUtils/isDeepCopy');
const { packagePath } = require('../../util');

global.isWatchMode = (process.argv || []).some(
Expand All @@ -16,4 +19,8 @@ test.skipOnWatch = (...args) => {

return test(...args);
};

expect.extend({
isDeepCopyOf: isDeepCopy,
});
/* eslint-enable */
45 changes: 36 additions & 9 deletions docs/result.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,47 @@ A result object would look somewhat like this:

```js
{
'name': 'formName', // The name of the validation suite
'errorCount': 0, // Overall count of errors in the suite
'warnCount': 0, // Overall count of warnings in the suite
'tests': Object { // An object containing all non-skipped tests
'fieldName': Object { // Name of each field
'errorCount': 0, // Error count per field
'errors': Array [], // Array of error messages fer field (may be undefined)
'warnings': Array [], // Array of warning messages fer field (may be undefined)
'warnCount': 0, // Warning count per field
'name': 'formName', // The name of the validation suite
'errorCount': Number 0, // Overall count of errors in the suite
'warnCount': Number 0, // Overall count of warnings in the suite
'testCount': Number 0, // Overall test count for the suite (passing, failing and warning)
'tests': Object { // An object containing all non-skipped tests
'fieldName': Object { // Name of each field
'errorCount': Number 0, // Error count per field
'errors': Array [], // Array of error messages fer field (may be undefined)
'warnings': Array [], // Array of warning messages fer field (may be undefined)
'warnCount': Number 0, // Warning count per field
'testCount': Number 0, // Overall test count for the field (passing, failing and warning)
},
'groups': Object { // An object containing groups declared in the suite
'fieldName': Object { // Subset of res.tests[fieldName] only containing tests
/*... */ // only containing tests that ran within the group
}
}
}
}
```

## Accessing the last result object with `vest.get`

Alternatively, if you need to access your validation results out of context - for example, from a different UI component or function, you can use `vest.get`.

Vest exposes the `vest.get` function that is able to retrieve the most recent validation result of [**stateful**](./state) suites (suites created using vest.create()).

In case your validations did not run yet, `vest.get` returns an empty validation result object - which can be helpful when trying to access validation result object when rendering the initial UI, or setting it in the initial state of your components.

vest.get takes a single argument: the suite name. It is used to identify which validation result to retrieve.

```js
import vest from 'vest';

const res = vest.get('suite_name');

res.hasErrors('fieldName');
```

# Result Object Methods:

Along with these values, the result object exposes the following methods:

## `hasErrors` and `hasWarnings` functions
Expand Down
2 changes: 1 addition & 1 deletion packages/vest/src/__snapshots__/spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,11 @@ Object {

exports[`Vest exports All vest exports exist 1`] = `
Object {
"Enforce": undefined,
"VERSION": Any<String>,
"create": [Function],
"draft": [Function],
"enforce": [Function],
"get": [Function],
"group": [Function],
"only": [Function],
"reset": [Function],
Expand Down
19 changes: 19 additions & 0 deletions packages/vest/src/core/createSuite/__snapshots__/spec.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Test createSuite module Initial run Should initialize with an empty state object 1`] = `
Array [
Object {
"doneCallbacks": Array [],
"exclusive": Object {},
"fieldCallbacks": Object {},
"groups": Object {},
"lagging": Array [],
"name": "initial_run_spec",
"pending": Array [],
"suiteId": "initial_run_spec",
"testObjects": Array [],
"tests": Object {},
},
undefined,
]
`;
33 changes: 22 additions & 11 deletions packages/vest/src/core/createSuite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import runWithContext from '../../lib/runWithContext';
import singleton from '../../lib/singleton';
import validateSuiteParams from '../../lib/validateSuiteParams';
import produce from '../produce';
import { getSuite } from '../state';
import getSuiteState from '../state/getSuiteState';
import registerSuite from '../state/registerSuite';
import mergeExcludedTests from '../test/lib/mergeExcludedTests';
Expand All @@ -17,19 +18,29 @@ import runAsyncTest from '../test/runAsyncTest';
const createSuite = (name, tests) => {
validateSuiteParams('vest.create', name, tests);

// returns validator function
return (...args) => {
const shouldCreateId = !singleton.useContext()?.suiteId;
const ctx = singleton.useContext();

const ctxRef = {
suiteId: ctx?.suiteId || name,
operationMode: ctx?.operationMode || OPERATION_MODE_STATEFUL,
name,
};

const ctxRef = singleton.useContext() ?? {
name,
operationMode: OPERATION_MODE_STATEFUL,
tests,
...(shouldCreateId && {
suiteId: name,
}),
};
/**
* Initialize empty suite state. This is not required for vest
* itself, but it is handy to have a default value when using
* UI frameworks that might try to get the validation results
* during initial render. This is irrelevant for stateless mode.
*/
if (
ctxRef.operationMode === OPERATION_MODE_STATEFUL &&
!getSuite(ctxRef.suiteId)
) {
runWithContext(ctxRef, registerSuite);
}

// returns validator function
return (...args) => {
const output = runWithContext(ctxRef, context => {
registerSuite();
const { suiteId } = context;
Expand Down
50 changes: 50 additions & 0 deletions packages/vest/src/core/createSuite/spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import faker from 'faker';
import { noop } from 'lodash';
import mock from '../../../testUtils/mock';
import resetState from '../../../testUtils/resetState';
import { OPERATION_MODE_STATELESS } from '../../constants';
import runWithContext from '../../lib/runWithContext';
import { getSuite } from '../state';
import createSuite from '.';

describe('Test createSuite module', () => {
Expand Down Expand Up @@ -57,4 +61,50 @@ describe('Test createSuite module', () => {
expect(testsCallback).toHaveBeenCalledWith(...params);
});
});

describe('Initial run', () => {
const testsCb = jest.fn();
const suiteId = 'initial_run_spec';
const runCreateSuite = () => createSuite(suiteId, testsCb);

afterEach(() => {
resetState();
});

it('Should initialize with an empty state object', () => {
expect(getSuite(suiteId)).toBeUndefined();
runCreateSuite();
const state = getSuite(suiteId);
expect(state).toHaveLength(2);
expect(state[1]).toBeUndefined();
expect(state[0].suiteId).toBe(suiteId);
expect(state[0].name).toBe(suiteId);
expect(state[0].testObjects).toHaveLength(0);
expect(state[0].pending).toHaveLength(0);
expect(state[0].lagging).toHaveLength(0);
expect(Object.keys(state[0].exclusive)).toHaveLength(0);
expect(Object.keys(state[0].tests)).toHaveLength(0);
expect(Object.keys(state[0].groups)).toHaveLength(0);
expect(Object.keys(state[0].doneCallbacks)).toHaveLength(0);
expect(Object.keys(state[0].fieldCallbacks)).toHaveLength(0);

expect(getSuite(suiteId)).toMatchSnapshot();
});

it('Should return without calling tests callback', () => {
const validate = runCreateSuite();
expect(testsCb).not.toHaveBeenCalled();
validate();
expect(testsCb).toHaveBeenCalled();
});

describe('When in stateless mode', () => {
it('Should return without creating initial state', () => {
runWithContext({ operationMode: OPERATION_MODE_STATELESS }, () => {
runCreateSuite();
});
});
expect(getSuite(suiteId)).toBeUndefined();
});
});
});
27 changes: 16 additions & 11 deletions packages/vest/src/core/produce/spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import _ from 'lodash';
import collector from '../../../testUtils/collector';
import isDeepCopy from '../../../testUtils/isDeepCopy';
import resetState from '../../../testUtils/resetState';
import runRegisterSuite from '../../../testUtils/runRegisterSuite';
import runSpec from '../../../testUtils/runSpec';
Expand Down Expand Up @@ -87,8 +86,7 @@ runSpec(vest => {
});

it('Should create a deep copy of subset of the state', () => {
isDeepCopy(
_.pick(getSuiteState(suiteId), KEPT_PROPERTIES),
expect(_.pick(getSuiteState(suiteId), KEPT_PROPERTIES)).isDeepCopyOf(
_.pick(produced, KEPT_PROPERTIES)
);
});
Expand Down Expand Up @@ -444,12 +442,16 @@ runSpec(vest => {

it('Should pass produced result to callback', () => {
produced.done(doneCallback_1).done(doneCallback_2);
isDeepCopy(doneCallback_1.mock.calls[0][0], produce(state));
isDeepCopy(doneCallback_2.mock.calls[0][0], produce(state));
expect(doneCallback_1.mock.calls[0][0]).isDeepCopyOf(
produce(state)
);
expect(doneCallback_2.mock.calls[0][0]).isDeepCopyOf(
produce(state)
);
});

it('Should return produced result', () => {
isDeepCopy(produced.done(doneCallback_1), produce(state));
expect(produced.done(doneCallback_1)).isDeepCopyOf(produce(state));
});
});

Expand All @@ -466,13 +468,16 @@ runSpec(vest => {
it('Should pass produced result to callback', () => {
produced.done('field_1', doneCallback_1).done(doneCallback_2);

isDeepCopy(doneCallback_1.mock.calls[0][0], produce(state));
isDeepCopy(doneCallback_2.mock.calls[0][0], produce(state));
expect(doneCallback_1.mock.calls[0][0]).isDeepCopyOf(
produce(state)
);
expect(doneCallback_2.mock.calls[0][0]).isDeepCopyOf(
produce(state)
);
});

it('Should return produced result', () => {
isDeepCopy(
produced.done('field_1', doneCallback_1),
expect(produced.done('field_1', doneCallback_1)).isDeepCopyOf(
produce(state)
);
});
Expand Down Expand Up @@ -516,7 +521,7 @@ runSpec(vest => {
expect(doneCallback_1).not.toHaveBeenCalled();
});
it('Should return produced output', () => {
isDeepCopy(produced.done(doneCallback_1), produce(state));
expect(produced.done(doneCallback_1)).isDeepCopyOf(produce(state));
});

it('Should add callback to `doneCallBacks` array', () =>
Expand Down
3 changes: 1 addition & 2 deletions packages/vest/src/core/state/registerSuite/spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { getState } from '..';
import isDeepCopy from '../../../../testUtils/isDeepCopy';
import resetState from '../../../../testUtils/resetState';
import runRegisterSuite from '../../../../testUtils/runRegisterSuite';
import { OPERATION_MODE_STATEFUL } from '../../../constants';
Expand Down Expand Up @@ -58,7 +57,7 @@ describe('registerSuite', () => {
runRegisterSuite(context);
});
it('Should merge previous pending and lagging into lagging', () => {
isDeepCopy(suite[0].lagging, [...pending, ...lagging]);
expect(suite[0].lagging).isDeepCopyOf([...pending, ...lagging]);
});

it('Should match snapshot', () => {
Expand Down
7 changes: 3 additions & 4 deletions packages/vest/src/hooks/draft/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import produce from '../../core/produce';
import getSuiteState from '../../core/state/getSuiteState';
import singleton from '../../lib/singleton';
import throwError from '../../lib/throwError';
import { ERROR_HOOK_CALLED_OUTSIDE } from '../constants';
import get from '../get';

/**
* @returns {Object} Current output object.
Expand All @@ -14,8 +13,8 @@ const draft = () => {
throwError('draft ' + ERROR_HOOK_CALLED_OUTSIDE);
return;
}
const state = getSuiteState(ctx.suiteId);
return produce(state, { draft: true });

return get(ctx.suiteId);
};

export default draft;
3 changes: 1 addition & 2 deletions packages/vest/src/hooks/draft/spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import faker from 'faker';
import isDeepCopy from '../../../testUtils/isDeepCopy';
import runSpec from '../../../testUtils/runSpec';

runSpec(vest => {
Expand All @@ -20,7 +19,7 @@ runSpec(vest => {
createSuite(() => {
const a = vest.draft();
const b = vest.draft();
isDeepCopy(a, b);
expect(a).isDeepCopyOf(b);
});
});
it('Should only contain has/get callbacks', () => {
Expand Down
36 changes: 36 additions & 0 deletions packages/vest/src/hooks/get/__snapshots__/spec.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`hook: vest.get() When suite exists Should return produced result 1`] = `
Object {
"errorCount": 1,
"getErrors": [Function],
"getErrorsByGroup": [Function],
"getWarnings": [Function],
"getWarningsByGroup": [Function],
"groups": Object {},
"hasErrors": [Function],
"hasErrorsByGroup": [Function],
"hasWarnings": [Function],
"hasWarningsByGroup": [Function],
"name": "form-name",
"tests": Object {
"f1": Object {
"errorCount": 0,
"testCount": 1,
"warnCount": 0,
},
"f2": Object {
"errorCount": 1,
"errors": Array [
"msg",
],
"testCount": 2,
"warnCount": 1,
"warnings": Array [
"msg",
],
},
},
"warnCount": 1,
}
`;
18 changes: 18 additions & 0 deletions packages/vest/src/hooks/get/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import produce from '../../core/produce';
import getSuiteState from '../../core/state/getSuiteState';
import throwError from '../../lib/throwError';

/**
* @param {String} suiteId Suite id to find
* @returns {Object} Up to date state copy.
*/
const get = suiteId => {
if (!suiteId) {
throwError('`get` hook was called without a suite name.');
}

const state = getSuiteState(suiteId);
return produce(state, { draft: true });
};

export default get;
Loading