From 052f31732a2743b756964563c89be4f3ff7e859c Mon Sep 17 00:00:00 2001 From: Evyatar Date: Sat, 19 Feb 2022 22:17:28 +0200 Subject: [PATCH] add(vest): eager run mode (#793) --- packages/vest/src/core/ctx/ctx.ts | 3 + .../src/core/test/lib/registerPrevRunTest.ts | 7 ++ packages/vest/src/hooks/mode/Modes.ts | 4 + .../src/hooks/mode/__tests__/eager.test.ts | 104 ++++++++++++++++++ packages/vest/src/hooks/mode/mode.ts | 50 +++++++++ packages/vest/src/vest.ts | 2 + tsconfig.json | 2 + website/docs/writing_your_suite/eager.md | 33 ++++++ .../writing_your_suite/optional_fields.md | 4 + 9 files changed, 209 insertions(+) create mode 100644 packages/vest/src/hooks/mode/Modes.ts create mode 100644 packages/vest/src/hooks/mode/__tests__/eager.test.ts create mode 100644 packages/vest/src/hooks/mode/mode.ts create mode 100644 website/docs/writing_your_suite/eager.md diff --git a/packages/vest/src/core/ctx/ctx.ts b/packages/vest/src/core/ctx/ctx.ts index fdfef79a8..8c5576f10 100644 --- a/packages/vest/src/core/ctx/ctx.ts +++ b/packages/vest/src/core/ctx/ctx.ts @@ -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'; @@ -24,6 +25,7 @@ export default createContext((ctxRef, parentContext) => prev: {}, }, }, + mode: [Modes.ALL], testCursor: createCursor(), }, ctxRef @@ -46,6 +48,7 @@ type CTXType = { groupName?: string; skipped?: boolean; omitted?: boolean; + mode: [Modes]; bus?: { on: ( event: string, diff --git a/packages/vest/src/core/test/lib/registerPrevRunTest.ts b/packages/vest/src/core/test/lib/registerPrevRunTest.ts index fef13e0e6..d772cad24 100644 --- a/packages/vest/src/core/test/lib/registerPrevRunTest.ts +++ b/packages/vest/src/core/test/lib/registerPrevRunTest.ts @@ -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'; @@ -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(); diff --git a/packages/vest/src/hooks/mode/Modes.ts b/packages/vest/src/hooks/mode/Modes.ts new file mode 100644 index 000000000..8359a772a --- /dev/null +++ b/packages/vest/src/hooks/mode/Modes.ts @@ -0,0 +1,4 @@ +export enum Modes { + ALL, + EAGER, +} diff --git a/packages/vest/src/hooks/mode/__tests__/eager.test.ts b/packages/vest/src/hooks/mode/__tests__/eager.test.ts new file mode 100644 index 000000000..645d104dc --- /dev/null +++ b/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); + }); + }); +}); diff --git a/packages/vest/src/hooks/mode/mode.ts b/packages/vest/src/hooks/mode/mode.ts new file mode 100644 index 000000000..15fb4b273 --- /dev/null +++ b/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; +} diff --git a/packages/vest/src/vest.ts b/packages/vest/src/vest.ts index fdc028f26..35aff2cae 100644 --- a/packages/vest/src/vest.ts +++ b/packages/vest/src/vest.ts @@ -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'; @@ -28,4 +29,5 @@ export { VERSION, context, include, + eager, }; diff --git a/tsconfig.json b/tsconfig.json index 92a965259..be7415c81 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -208,6 +208,8 @@ "exclusive": ["./packages/vest/src/hooks/exclusive.ts"], "hookErrors": ["./packages/vest/src/hooks/hookErrors.ts"], "include": ["./packages/vest/src/hooks/include.ts"], + "mode": ["./packages/vest/src/hooks/mode/mode.ts"], + "Modes": ["./packages/vest/src/hooks/mode/Modes.ts"], "optionalTests": ["./packages/vest/src/hooks/optionalTests.ts"], "warn": ["./packages/vest/src/hooks/warn.ts"], "vest.d": ["./packages/vest/src/typings/vest.d.ts"], diff --git a/website/docs/writing_your_suite/eager.md b/website/docs/writing_your_suite/eager.md new file mode 100644 index 000000000..378d6c4d1 --- /dev/null +++ b/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); + }); +}); +``` diff --git a/website/docs/writing_your_suite/optional_fields.md b/website/docs/writing_your_suite/optional_fields.md index 9e3e4c293..7c54a2fa6 100644 --- a/website/docs/writing_your_suite/optional_fields.md +++ b/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.