Skip to content

Commit

Permalink
add(vest): eager run mode (#793)
Browse files Browse the repository at this point in the history
  • Loading branch information
ealush committed Feb 19, 2022
1 parent b0ceeb6 commit 052f317
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/vest/src/core/ctx/ctx.ts
Expand Up @@ -3,6 +3,7 @@ import { createContext } from 'context';
import { createCursor } from 'cursor';

import { IsolateKeys, IsolateTypes } from 'IsolateTypes';
import { Modes } from 'Modes';
import VestTest from 'VestTest';
import type { TStateRef } from 'createStateRef';

Expand All @@ -24,6 +25,7 @@ export default createContext<CTXType>((ctxRef, parentContext) =>
prev: {},
},
},
mode: [Modes.ALL],
testCursor: createCursor(),
},
ctxRef
Expand All @@ -46,6 +48,7 @@ type CTXType = {
groupName?: string;
skipped?: boolean;
omitted?: boolean;
mode: [Modes];
bus?: {
on: (
event: string,
Expand Down
7 changes: 7 additions & 0 deletions packages/vest/src/core/test/lib/registerPrevRunTest.ts
Expand Up @@ -3,6 +3,7 @@ import isPromise from 'isPromise';
import VestTest from 'VestTest';
import cancelOverriddenPendingTest from 'cancelOverriddenPendingTest';
import { isExcluded } from 'exclusive';
import { shouldSkipBasedOnMode } from 'mode';
import { isOmitted } from 'omitWhen';
import registerTest from 'registerTest';
import runAsyncTest from 'runAsyncTest';
Expand All @@ -14,6 +15,12 @@ import { useTestAtCursor, useSetTestAtCursor } from 'useTestAtCursor';
export default function registerPrevRunTest(testObject: VestTest): VestTest {
const prevRunTest = useTestAtCursor(testObject);

if (shouldSkipBasedOnMode(testObject)) {
testCursor.moveForward();
testObject.skip();
return testObject;
}

if (isOmitted()) {
prevRunTest.omit();
testCursor.moveForward();
Expand Down
4 changes: 4 additions & 0 deletions packages/vest/src/hooks/mode/Modes.ts
@@ -0,0 +1,4 @@
export enum Modes {
ALL,
EAGER,
}
104 changes: 104 additions & 0 deletions packages/vest/src/hooks/mode/__tests__/eager.test.ts
@@ -0,0 +1,104 @@
import { dummyTest } from '../../../../testUtils/testDummy';

import { create, eager, only, group } from 'vest';

describe('mode: eager', () => {
let suite;

describe('When tests fail', () => {
beforeEach(() => {
suite = create(include => {
only(include);

eager();
dummyTest.failing('field_1', 'first-of-field_1');
dummyTest.failing('field_1', 'second-of-field_1'); // Should not run
dummyTest.failing('field_2', 'first-of-field_2');
dummyTest.failing('field_2', 'second-of-field_2'); // Should not run
dummyTest.failing('field_3', 'first-of-field_3');
dummyTest.failing('field_3', 'second-of-field_3'); // Should not run
});
});

it('Should fail fast for every failing field', () => {
expect(suite.get().testCount).toBe(0); // sanity
suite();
expect(suite.get().testCount).toBe(3);
expect(suite.get().errorCount).toBe(3);
expect(suite.get().getErrors('field_1')).toEqual(['first-of-field_1']);
expect(suite.get().getErrors('field_2')).toEqual(['first-of-field_2']);
expect(suite.get().getErrors('field_3')).toEqual(['first-of-field_3']);
});

describe('When test is `only`ed', () => {
it('Should fail fast for failing field', () => {
suite('field_1');
expect(suite.get().testCount).toBe(1);
expect(suite.get().errorCount).toBe(1);
expect(suite.get().getErrors('field_1')).toEqual(['first-of-field_1']);
});
});

describe('When test is in a group', () => {
beforeEach(() => {
suite = create(() => {
eager();
group('group_1', () => {
dummyTest.failing('field_1', 'first-of-field_1');
});
dummyTest.failing('field_1', 'second-of-field_1');
});
});
it('Should fail fast for failing field', () => {
suite();
expect(suite.get().testCount).toBe(1);
expect(suite.get().errorCount).toBe(1);
expect(suite.get().getErrors('field_1')).toEqual(['first-of-field_1']);
});
});
});

describe('When tests pass', () => {
beforeEach(() => {
suite = create(() => {
eager();
dummyTest.passing('field_1', 'first-of-field_1');
dummyTest.failing('field_1', 'second-of-field_1');
dummyTest.passing('field_2', 'first-of-field_2');
dummyTest.failing('field_2', 'second-of-field_2');
dummyTest.passing('field_3', 'first-of-field_3');
dummyTest.failing('field_3', 'second-of-field_3');
});
});

it('Should fail fast for every failing field', () => {
expect(suite.get().testCount).toBe(0); // sanity
suite();
expect(suite.get().testCount).toBe(6);
expect(suite.get().errorCount).toBe(3);
expect(suite.get().getErrors('field_1')).toEqual(['second-of-field_1']);
expect(suite.get().getErrors('field_2')).toEqual(['second-of-field_2']);
expect(suite.get().getErrors('field_3')).toEqual(['second-of-field_3']);
});
});

describe('sanity', () => {
beforeEach(() => {
suite = create(() => {
dummyTest.failing('field_1', 'first-of-field_1');
dummyTest.failing('field_1', 'second-of-field_1');
dummyTest.failing('field_2', 'first-of-field_2');
dummyTest.failing('field_2', 'second-of-field_2');
dummyTest.failing('field_3', 'first-of-field_3');
dummyTest.failing('field_3', 'second-of-field_3');
});
});

it('Should run all tests', () => {
expect(suite.get().testCount).toBe(0); // sanity
suite();
expect(suite.get().testCount).toBe(6);
expect(suite.get().errorCount).toBe(6);
});
});
});
50 changes: 50 additions & 0 deletions packages/vest/src/hooks/mode/mode.ts
@@ -0,0 +1,50 @@
import { Modes } from './Modes';

import VestTest from 'VestTest';
import ctx from 'ctx';
import { hasErrors } from 'hasFailures';

/**
* Sets the suite to "eager" (fail fast) mode.
* Eager mode will skip running subsequent tests of a failing fields.
*
* @example
* // in the following example, the second test of username will not run
* // if the first test of username failed.
* const suite = create((data) => {
* eager();
*
* test('username', 'username is required', () => {
* enforce(data.username).isNotBlank();
* });
*
* test('username', 'username is too short', () => {
* enforce(data.username).longerThan(2);
* });
* });
*/
export function eager() {
setMode(Modes.EAGER);
}

export function shouldSkipBasedOnMode(testObject: VestTest): boolean {
if (isEager() && hasErrors(testObject.fieldName)) return true;

return false;
}

function isEager(): boolean {
return isMode(Modes.EAGER);
}

function isMode(mode: Modes): boolean {
const { mode: currentMode } = ctx.useX();

return currentMode[0] === mode;
}

function setMode(nextMode: Modes): void {
const { mode } = ctx.useX();

mode[0] = nextMode;
}
2 changes: 2 additions & 0 deletions packages/vest/src/vest.ts
Expand Up @@ -6,6 +6,7 @@ import each from 'each';
import { only, skip } from 'exclusive';
import group from 'group';
import include from 'include';
import { eager } from 'mode';
import omitWhen from 'omitWhen';
import optional from 'optionalTests';
import skipWhen from 'skipWhen';
Expand All @@ -28,4 +29,5 @@ export {
VERSION,
context,
include,
eager,
};
2 changes: 2 additions & 0 deletions tsconfig.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions website/docs/writing_your_suite/eager.md
@@ -0,0 +1,33 @@
---
sidebar_position: 5
---

# eager mode, failing fast

Sometimes we wish to fail fast and not continue run subsequent tests of a failing field. We can do this manually per test using [skipWhen](./including_and_excluding/skipWhen.md), but if we want to do this automatically for all the tests in the suite, we can set the suite to `eager` mode.

`eager` mode means that once a test of a given field fails, the suite will continue running subsequent tests of that same field. Other tests will run normally.

:::tip NOTE
Eager mode disregards groups and nested blocks, meaning that a failing field at any level, will skip its subsequent runs regardless of where the test was specified.
:::

## Usage

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

const suite = create(data => {
eager(); // set the suite to eager mode

test('name', 'Name is required', () => {
enforce(data.name).isNotBlank();
});

// this test will not run if the previous test fails
// because the suite is in eager mode
test('name', 'Name is too short', () => {
enforce(data.name).longerThan(3);
});
});
```
4 changes: 4 additions & 0 deletions website/docs/writing_your_suite/optional_fields.md
@@ -1,3 +1,7 @@
---
sidebar_position: 4
---

# optional fields

By default, all the tests inside Vest are required in order for the suite to be considered as "valid". Sometimes your app's logic may allow tests not to be filled out, and you want them not to be accounted for in the suites validity.
Expand Down

1 comment on commit 052f317

@vercel
Copy link

@vercel vercel bot commented on 052f317 Feb 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

vest-next – ./website

vest-next.vercel.app
vest-website.vercel.app
vest-next-ealush.vercel.app
vest-next-git-latest-ealush.vercel.app

Please sign in to comment.