diff --git a/.eslintrc.js b/.eslintrc.js index 6f06506ef..7cc524e7c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -43,6 +43,13 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 'off', }, }, + { + excludedFiles: ['./**/__tests__/**/*.*'], + files: ['*.ts'], + rules: { + 'jest/no-export': 0, + }, + }, ], parser: '@typescript-eslint/parser', plugins: ['jest'], diff --git a/packages/vest/src/core/test/VestTest.ts b/packages/vest/src/core/test/VestTest.ts index d6118d151..c793c9811 100644 --- a/packages/vest/src/core/test/VestTest.ts +++ b/packages/vest/src/core/test/VestTest.ts @@ -94,6 +94,10 @@ export default class VestTest { return this.isFailing() || this.isWarning(); } + isTested(): boolean { + return this.hasFailures() || this.isPassing(); + } + isFailing(): boolean { return this.status === STATUS_FAILED; } diff --git a/packages/vest/src/core/test/test.each.ts b/packages/vest/src/core/test/test.each.ts index f9f33defd..ea095113f 100644 --- a/packages/vest/src/core/test/test.each.ts +++ b/packages/vest/src/core/test/test.each.ts @@ -6,7 +6,6 @@ import type { TStringable } from 'utilityTypes'; import VestTest, { TTestResult } from 'VestTest'; import type { TTestBase } from 'test'; -/* eslint-disable jest/no-export */ export default function bindTestEach(test: TTestBase): (table: any[]) => { (fieldName: TStringable, message: TStringable, cb: TEachCb): VestTest[]; (fieldName: TStringable, cb: TEachCb): VestTest[]; diff --git a/packages/vest/src/core/test/test.memo.ts b/packages/vest/src/core/test/test.memo.ts index 503414574..679268045 100644 --- a/packages/vest/src/core/test/test.memo.ts +++ b/packages/vest/src/core/test/test.memo.ts @@ -14,7 +14,6 @@ import { useSetTestAtCursor, } from 'stateHooks'; import type { TTestBase } from 'test'; -/* eslint-disable jest/no-export */ // eslint-disable-next-line max-lines-per-function export default function bindTestMemo(test: TTestBase): { diff --git a/packages/vest/src/exports/__tests__/classnames.test.ts b/packages/vest/src/exports/__tests__/classnames.test.ts index 133a2b3fe..345a6d8e3 100644 --- a/packages/vest/src/exports/__tests__/classnames.test.ts +++ b/packages/vest/src/exports/__tests__/classnames.test.ts @@ -56,7 +56,7 @@ describe('Utility: classnames', () => { 'invalid_string tested_string warning_string'.split(' ').sort() ); expect(genClass('field_3').split(' ').sort()).toEqual( - 'tested_string warning_string'.split(' ').sort() + 'tested_string valid_string warning_string'.split(' ').sort() ); expect(genClass('field_4').split(' ').sort()).toEqual( 'tested_string valid_string'.split(' ').sort() diff --git a/packages/vest/src/exports/__tests__/parser.test.ts b/packages/vest/src/exports/__tests__/parser.test.ts new file mode 100644 index 000000000..5ee9cb08d --- /dev/null +++ b/packages/vest/src/exports/__tests__/parser.test.ts @@ -0,0 +1,208 @@ +import * as suiteDummy from '../../../testUtils/suiteDummy'; + +import { parse } from 'parser'; +import * as vest from 'vest'; + +describe('parser.parse', () => { + describe('parse().invalid', () => { + it('Should return true when provided suite result is failing and no field name is provided', () => { + expect(parse(suiteDummy.failing()).invalid()).toBe(true); + }); + + it('Should return false when provided suite result is passing and no field name is provided', () => { + expect(parse(suiteDummy.passing()).invalid()).toBe(false); + }); + + it('Should return true when provided field is failing', () => { + expect(parse(suiteDummy.failing('username')).invalid('username')).toBe( + true + ); + }); + + it('Should return false when provided field is passing', () => { + expect(parse(suiteDummy.passing('username')).invalid('username')).toBe( + false + ); + }); + }); + + describe('parse().tested', () => { + it('Should return true if any field is tested but no field is provided', () => { + expect(parse(suiteDummy.passing()).tested()).toBe(true); + }); + it('Should return true if no field is tested', () => { + expect(parse(suiteDummy.untested()).tested()).toBe(false); + expect(parse(suiteDummy.untested()).tested('field')).toBe(false); + }); + it('Should return true if provided field is tested', () => { + expect(parse(suiteDummy.passing('username')).tested('username')).toBe( + true + ); + expect(parse(suiteDummy.failing('username')).tested('username')).toBe( + true + ); + }); + }); + + describe('parse().untested', () => { + it('Should return true if no field is tested', () => { + expect(parse(suiteDummy.untested()).untested()).toBe(true); + expect(parse(suiteDummy.untested()).untested('username')).toBe(true); + }); + + it('Should return true if provided field is untested while others are', () => { + expect( + parse( + vest.create(() => { + vest.test('x', () => {}); + vest.skipWhen(true, () => { + vest.test('untested', () => {}); + }); + })() + ).untested('untested') + ).toBe(true); + }); + + it('Should return false if any field is tested', () => { + expect(parse(suiteDummy.passing('username')).untested()).toBe(false); + }); + + it('Should return false if provided field is tested', () => { + expect(parse(suiteDummy.passing('username')).untested('username')).toBe( + false + ); + }); + }); + + describe('parse().valid', () => { + it('Should return true if all fields are passing', () => { + expect(parse(suiteDummy.passing(['f1', 'f2', 'f3'])).valid()).toBe(true); + expect(parse(suiteDummy.passing(['f1', 'f2', 'f3'])).valid('f2')).toBe( + true + ); + }); + + it('Should return true if all required fields have been tested and are passing', () => { + expect( + parse(suiteDummy.passingWithUntestedOptional('optional')).valid() + ).toBe(true); + }); + + it('Should return true if all fields, including optional, pass', () => { + expect(parse(suiteDummy.passingWithOptional('optional')).valid()).toBe( + true + ); + }); + + it('Should return false if suite has errors', () => { + expect(parse(suiteDummy.failing()).valid()).toBe(false); + }); + + it('Should return false if suite has failing optional tests', () => { + expect(parse(suiteDummy.failingOptional()).valid()).toBe(false); + }); + + it('Should return true if suite only has warnings', () => { + expect(parse(suiteDummy.warning(['f1', 'f2', 'f3'])).valid()).toBe(true); + }); + + it('Should return false if no tests ran', () => { + expect(parse(suiteDummy.untested()).valid()).toBe(false); + }); + + it('should return false if not all required fields ran', () => { + expect( + parse( + vest.create(() => { + vest.test('x', () => {}); + vest.test('untested', () => {}); + vest.skipWhen(true, () => { + vest.test('untested', () => {}); + }); + })() + ).valid() + ).toBe(false); + }); + + describe('With field name', () => { + it('Should return false when field is untested', () => { + expect( + parse( + vest.create(() => { + vest.skipWhen(true, () => { + vest.test('f1', () => {}); + }); + })() + ).valid('f1') + ).toBe(false); + }); + + it('Should return true if optional field is untested', () => { + expect( + parse( + vest.create(() => { + vest.optional('f1'); + vest.skipWhen(true, () => { + vest.test('f1', () => {}); + }); + })() + ).valid('f1') + ).toBe(true); + }); + + it('Should return true if field is passing', () => { + expect( + parse( + vest.create(() => { + vest.test('f1', () => {}); + vest.test('f2', () => false); + })() + ).valid('f1') + ).toBe(true); + }); + + it('Should return false if field is failing', () => { + expect( + parse( + vest.create(() => { + vest.test('f1', () => {}); + vest.test('f2', () => false); + })() + ).valid('f2') + ).toBe(false); + expect( + parse( + vest.create(() => { + vest.test('f1', () => {}); + vest.test('f2', () => {}); + vest.test('f2', () => false); + })() + ).valid('f2') + ).toBe(false); + }); + + it('Should return true if field is warning', () => { + expect( + parse( + vest.create(() => { + vest.test('f1', () => { + vest.warn(); + return false; + }); + })() + ).valid('f1') + ).toBe(true); + }); + }); + }); + + describe('parse().warning', () => { + it('Should return true when the suite has warnings', () => { + expect(parse(suiteDummy.warning()).warning()).toBe(true); + }); + + it('Should return false when the suite is not warnings', () => { + expect(parse(suiteDummy.failing()).warning()).toBe(false); + }); + }); +}); diff --git a/packages/vest/src/exports/classnames.ts b/packages/vest/src/exports/classnames.ts index 9b5b9c6b6..7bba30246 100644 --- a/packages/vest/src/exports/classnames.ts +++ b/packages/vest/src/exports/classnames.ts @@ -1,7 +1,7 @@ -import { greaterThan } from 'greaterThan'; -import hasOwnProperty from 'hasOwnProperty'; import isFunction from 'isFunction'; +import throwError from 'throwError'; +import { parse } from 'parser'; import type { IVestResult } from 'produce'; import type { TDraftResult } from 'produceDraft'; @@ -13,31 +13,12 @@ export default function classNames( classes: TSupportedClasses = {} ): (fieldName: string) => string { if (!res || !isFunction(res.hasErrors)) { - throw new Error( + throwError( "[vest/classNames]: Expected first argument to be Vest's result object." ); } - const testedStorage: Record = {}; - - const selectors = { - invalid: (key: string): boolean => - selectors.tested(key) && res.hasErrors(key), - tested: (key: string): boolean => { - if (hasOwnProperty(testedStorage, key)) return testedStorage[key]; - - testedStorage[key] = - hasOwnProperty(res.tests, key) && - greaterThan(res.tests[key].testCount, 0); - - return selectors.tested(key); - }, - untested: (key: string): boolean => !selectors.tested(key), - valid: (key: string): boolean => - selectors.tested(key) && !res.hasWarnings(key) && !res.hasErrors(key), - warning: (key: string): boolean => - selectors.tested(key) && res.hasWarnings(key), - }; + const selectors = parse(res); return (key: string): string => { const classesArray: string[] = []; diff --git a/packages/vest/src/exports/parser.ts b/packages/vest/src/exports/parser.ts new file mode 100644 index 000000000..0ad6fec4d --- /dev/null +++ b/packages/vest/src/exports/parser.ts @@ -0,0 +1,38 @@ +import { greaterThan } from 'greaterThan'; +import hasOwnProperty from 'hasOwnProperty'; + +import type { IVestResult } from 'produce'; +import type { TDraftResult } from 'produceDraft'; + +export function parse(res: IVestResult | TDraftResult): { + valid: (fieldName?: string) => boolean; + tested: (fieldName?: string) => boolean; + invalid: (fieldName?: string) => boolean; + untested: (fieldName?: string) => boolean; +} { + const testedStorage: Record = {}; + + const selectors = { + invalid: res.hasErrors, + tested: (fieldName?: string): boolean => { + if (!fieldName) { + return greaterThan(res.testCount, 0); + } + + if (hasOwnProperty(testedStorage, fieldName)) + return testedStorage[fieldName]; + + testedStorage[fieldName] = + hasOwnProperty(res.tests, fieldName) && + greaterThan(res.tests[fieldName].testCount, 0); + + return selectors.tested(fieldName); + }, + untested: (fieldName?: string): boolean => + res.testCount === 0 || !selectors.tested(fieldName), + valid: res.isValid, + warning: res.hasWarnings, + }; + + return selectors; +} diff --git a/packages/vest/src/exports/promisify.ts b/packages/vest/src/exports/promisify.ts index 864ecc639..70cbc94ea 100644 --- a/packages/vest/src/exports/promisify.ts +++ b/packages/vest/src/exports/promisify.ts @@ -1,4 +1,5 @@ import isFunction from 'isFunction'; +import throwError from 'throwError'; import { IVestResult } from 'produce'; import { TDraftResult } from 'produceDraft'; @@ -7,9 +8,7 @@ const promisify = (validatorFn: (...args: any[]) => IVestResult) => (...args: any[]): Promise => { if (!isFunction(validatorFn)) { - throw new Error( - '[vest/promisify]: Expected validatorFn to be a function.' - ); + throwError('[vest/promisify]: Expected validatorFn to be a function.'); } return new Promise(resolve => validatorFn(...args).done(resolve)); diff --git a/packages/vest/src/produce/__tests__/isValid.test.ts b/packages/vest/src/produce/__tests__/isValid.test.ts index 0f103c4a9..16c88cbe9 100644 --- a/packages/vest/src/produce/__tests__/isValid.test.ts +++ b/packages/vest/src/produce/__tests__/isValid.test.ts @@ -244,7 +244,7 @@ describe('isValid', () => { }); }); - describe('When fieldname is specified', () => { + describe('When field name is specified', () => { it('Should return false when field did not run yet', () => { expect( create(() => { @@ -254,6 +254,14 @@ describe('isValid', () => { ).toBe(false); }); + it('Should return false when testing for a field that does not exist', () => { + expect( + create(() => { + test('field_1', () => {}); + })().isValid('field 2') + ).toBe(false); + }); + it("Should return false when some of the field's tests ran", () => { expect( create(() => { diff --git a/packages/vest/src/produce/isValid.ts b/packages/vest/src/produce/isValid.ts index 9acc9246d..89a1611ab 100644 --- a/packages/vest/src/produce/isValid.ts +++ b/packages/vest/src/produce/isValid.ts @@ -15,6 +15,10 @@ export function isValid(result: TDraftResult, fieldName?: string): boolean { return false; } + if (fieldDoesNotExist(result, fieldName)) { + return false; + } + if ( isNotEmpty( useAllIncomplete().filter(testObject => { @@ -31,6 +35,10 @@ export function isValid(result: TDraftResult, fieldName?: string): boolean { return noMissingTests(fieldName); } +function fieldDoesNotExist(result: TDraftResult, fieldName?: string): boolean { + return !!fieldName && isEmpty(result.tests[fieldName]); +} + function noMissingTests(fieldName?: string): boolean { const [testObjects] = useTestObjects(); @@ -43,6 +51,6 @@ function noMissingTests(fieldName?: string): boolean { return true; } - return !testObject.isSkipped(); + return testObject.isTested(); }); } diff --git a/packages/vest/testUtils/collector.ts b/packages/vest/testUtils/collector.ts deleted file mode 100644 index 1fbd19abe..000000000 --- a/packages/vest/testUtils/collector.ts +++ /dev/null @@ -1,14 +0,0 @@ -const collector = () => { - const collection: any[] = []; - - const collect = val => { - collection.push(val); - return val; - }; - - collect.collection = collection; - - return collect; -}; - -export default collector; diff --git a/packages/vest/testUtils/itWithContext.ts b/packages/vest/testUtils/itWithContext.ts index 54f624944..79fb7b892 100644 --- a/packages/vest/testUtils/itWithContext.ts +++ b/packages/vest/testUtils/itWithContext.ts @@ -4,7 +4,6 @@ import runCreateRef from './runCreateRef'; import context from 'ctx'; -// eslint-disable-next-line jest/no-export export default function itWithContext( str: string, cb: () => void, diff --git a/packages/vest/testUtils/suiteDummy.ts b/packages/vest/testUtils/suiteDummy.ts new file mode 100644 index 000000000..4941fe856 --- /dev/null +++ b/packages/vest/testUtils/suiteDummy.ts @@ -0,0 +1,104 @@ +import asArray from '../../shared/src/asArray'; + +import { dummyTest } from './testDummy'; + +import { optional, create, skip } from 'vest'; + +export function failing(failingFields?: string | string[]) { + return createSuiteResult(failingFields, fieldName => { + dummyTest.failing(fieldName); + }); +} + +export function warning(failingFields?: string | string[]) { + return createSuiteResult(failingFields, fieldName => { + dummyTest.failingWarning(fieldName); + }); +} + +export function failingAsync(failingFields?: string | string[]) { + return createSuiteResult(failingFields, fieldName => { + dummyTest.failingAsync(fieldName); + }); +} + +export function passing(fields?: string | string[]) { + return createSuiteResult(fields, fieldName => { + dummyTest.passing(fieldName); + }); +} + +export function passingWithUntestedOptional( + optionals: string | string[] = 'optional_field', + required: string | string[] = 'field_1' +) { + return create(() => { + optional(optionals); + skip(optionals); + + asArray(optionals).forEach(fieldName => { + dummyTest.failing(fieldName); + }); + + asArray(required).forEach(fieldName => { + dummyTest.passing(fieldName); + }); + })(); +} + +export function passingWithOptional( + optionals: string | string[] = 'optional_field', + required: string | string[] = 'field_1' +) { + return create(() => { + optional(optionals); + + asArray(optionals).forEach(fieldName => { + dummyTest.passing(fieldName); + }); + + asArray(required).forEach(fieldName => { + dummyTest.passing(fieldName); + }); + })(); +} + +export function failingOptional( + optionals: string | string[] = 'optional_field', + required: string | string[] = 'field_1' +) { + return create(() => { + optional(optionals); + + asArray(optionals).forEach(fieldName => { + dummyTest.failing(fieldName); + }); + + asArray(required).forEach(fieldName => { + dummyTest.passing(fieldName); + }); + })(); +} + +export function untested(fields?: string | string[]) { + const suite = createSuite(fields, fieldName => { + dummyTest.failing(fieldName); + }); + return suite.get(); +} + +function createSuiteResult( + fieldNames: string[] | string | undefined, + callback: (fieldName?: string) => void +) { + return createSuite(fieldNames, callback)(); +} + +function createSuite( + fieldNames: string[] | string | undefined = 'field_1', + callback: (fieldName?: string) => void +) { + return create(() => { + asArray(fieldNames).forEach(fieldName => callback(fieldName)); + }); +} diff --git a/packages/vest/testUtils/testDummy.ts b/packages/vest/testUtils/testDummy.ts index 99ad2f546..cc4642b78 100644 --- a/packages/vest/testUtils/testDummy.ts +++ b/packages/vest/testUtils/testDummy.ts @@ -1,4 +1,3 @@ -/* eslint-disable jest/no-export */ import faker from 'faker'; import { test, warn } from 'vest'; diff --git a/tsconfig.json b/tsconfig.json index 181091dee..e7f7d5af6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -137,6 +137,7 @@ "test": ["./packages/vest/src/core/test/test.ts"], "VestTest": ["./packages/vest/src/core/test/VestTest.ts"], "classnames": ["./packages/vest/src/exports/classnames.ts"], + "parser": ["./packages/vest/src/exports/parser.ts"], "promisify": ["./packages/vest/src/exports/promisify.ts"], "exclusive": ["./packages/vest/src/hooks/exclusive.ts"], "group": ["./packages/vest/src/hooks/group.ts"],