Skip to content

Commit

Permalink
patch(vest): automatically purge not-found test results
Browse files Browse the repository at this point in the history
  • Loading branch information
ealush committed Nov 10, 2021
1 parent 2dfedaf commit 49b72f4
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ module.exports = {
'no-useless-computed-key': 2,
'no-useless-return': 2,
'no-var': 2,
'no-warning-comments': 2,
'no-warning-comments': 1,
'object-shorthand': [2, 'always', { avoidQuotes: true }],
'prefer-const': 2,
'sort-keys': [
Expand Down
2 changes: 1 addition & 1 deletion packages/vest/docs/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default create(data => {

Use test.each when you need to dynamically create tests from data, or when you have multiple tests that have the same overall structure.

test.each takes an array of arrays. The inner array contains the arguments that each of the tests will recieve.
test.each takes an array of arrays. The inner array contains the arguments that each of the tests will receive.

Because of the dynamic nature of the iterative tests, you can also dynamically construct the fieldName and the test message by providing a function instead of a string. Your array's content will be passed over as arguments to each of these functions.

Expand Down
15 changes: 0 additions & 15 deletions packages/vest/src/core/state/stateHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,6 @@ export function useRefreshTestObjects(): void {
setTestObjects(testObjects => testObjects.slice(0));
}

export function useSetTestAtCursor(testObject: VestTest): void {
const [cursorAt] = useCursorAt();
const [testObjects, setTestObjects] = useTestObjects();

if (testObject === testObjects[cursorAt]) {
return;
}

setTestObjects((testObjects: VestTest[]) => {
const newTestsOrder = testObjects.slice(0);
newTestsOrder[cursorAt] = testObject;
return newTestsOrder;
});
}

// DERIVED VALUES

const omittedFieldsCache = createCache();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe('Merging of previous test runs', () => {
});
});

describe('When tests are passed in a different order between tests', () => {
describe('When tests are passed in a different order between runs', () => {
let throwErrorDeferred, vest;
beforeEach(() => {
throwErrorDeferred = jest.fn();
Expand All @@ -82,7 +82,7 @@ describe('Merging of previous test runs', () => {
vest = require('vest');
});

afterEach(() => {
afterAll(() => {
jest.resetAllMocks();
});

Expand All @@ -106,5 +106,199 @@ describe('Merging of previous test runs', () => {
)
);
});

describe('When test is omitted in subsequent run', () => {
it('Should omit the test from the results', () => {
const { create, test } = vest;
suite = create(() => {
test('f1', () => false);
if (counter === 0) {
test('f2', () => false);
}
test('f3', () => false);
counter++;
});

const resA = suite();
expect(resA.tests.f2).toBeDefined();
expect(resA.hasErrors('f1')).toBe(true);
expect(resA.hasErrors('f2')).toBe(true);
expect(resA.hasErrors('f3')).toBe(true);
expect(resA.tests).toMatchInlineSnapshot(`
Object {
"f1": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"f2": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"f3": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
}
`);

const resB = suite();
expect(resB.tests.f2).not.toBeDefined();
expect(resB.hasErrors('f1')).toBe(true);
expect(resB.hasErrors('f2')).toBe(false);
expect(resB.hasErrors('f3')).toBe(true);
expect(resB.tests).toMatchInlineSnapshot(`
Object {
"f1": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"f3": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
}
`);
});

describe('When multiple tests are omitted between a test', () => {
it('Should omit the tests from the results', () => {
const { create, test } = vest;
suite = create(() => {
test('f1', () => false);
if (counter === 0) {
test('f2', () => false);
test('f3', () => false);
}
test('f4', () => false);
if (counter === 0) {
test('f5', () => false);
test('f6', () => false);
test('f7', () => false);
}
test('f4', () => false);
counter++;
});

const resA = suite();
expect(resA.tests.f2).toBeDefined();
expect(resA.tests.f3).toBeDefined();
expect(resA.tests.f5).toBeDefined();
expect(resA.tests.f6).toBeDefined();
expect(resA.tests.f7).toBeDefined();
expect(resA.hasErrors('f1')).toBe(true);
expect(resA.hasErrors('f2')).toBe(true);
expect(resA.hasErrors('f3')).toBe(true);
expect(resA.hasErrors('f4')).toBe(true);
expect(resA.hasErrors('f5')).toBe(true);
expect(resA.hasErrors('f6')).toBe(true);
expect(resA.hasErrors('f7')).toBe(true);
expect(resA.tests).toMatchInlineSnapshot(`
Object {
"f1": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"f2": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"f3": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"f4": Object {
"errorCount": 2,
"testCount": 2,
"warnCount": 0,
},
"f5": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"f6": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"f7": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
}
`);
const resB = suite();
expect(resB.tests.f2).not.toBeDefined();
expect(resB.tests.f3).not.toBeDefined();
expect(resB.tests.f5).not.toBeDefined();
expect(resB.tests.f6).not.toBeDefined();
expect(resB.tests.f7).not.toBeDefined();
expect(resB.hasErrors('f1')).toBe(true);
expect(resB.hasErrors('f2')).toBe(false);
expect(resB.hasErrors('f3')).toBe(false);
expect(resB.hasErrors('f4')).toBe(true);
expect(resB.hasErrors('f5')).toBe(false);
expect(resB.hasErrors('f6')).toBe(false);
expect(resB.hasErrors('f7')).toBe(false);
expect(resB.tests).toMatchInlineSnapshot(`
Object {
"f1": Object {
"errorCount": 1,
"testCount": 1,
"warnCount": 0,
},
"f4": Object {
"errorCount": 2,
"testCount": 2,
"warnCount": 0,
},
}
`);
});
});

describe('When tests are added inbetween tests', () => {
it('Should remove next tests in line', () => {
const { create, test, skipWhen } = vest;

const suite = create(() => {
test('f1', () => false);
if (counter === 1) {
test('f2', () => false);
test('f3', () => false);
}

skipWhen(
() => counter === 1,
() => {
test('f4', () => false);
test('f5', () => false);
}
);
counter++;
});

const resA = suite();
expect(resA.hasErrors('f4')).toBe(true);
expect(resA.hasErrors('f5')).toBe(true);

// This is testing that fact that the next test in line after f2 and f3
// got removed. We can see it because in normal situation, the test result is
// merged into the next test result.
const resB = suite();
expect(resB.hasErrors('f4')).toBe(false);
expect(resB.hasErrors('f5')).toBe(false);
});
});
});
});
});
4 changes: 2 additions & 2 deletions packages/vest/src/core/test/lib/registerPrevRunTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import cancelOverriddenPendingTest from 'cancelOverriddenPendingTest';
import { isExcluded } from 'exclusive';
import registerTest from 'registerTest';
import runAsyncTest from 'runAsyncTest';
import { useSetTestAtCursor, useSetNextCursorAt } from 'stateHooks';
import useTestAtCursor from 'useTestAtCursor';
import { useSetNextCursorAt } from 'stateHooks';
import { useTestAtCursor, useSetTestAtCursor } from 'useTestAtCursor';

export default function registerPrevRunTest(testObject: VestTest): VestTest {
const prevRunTest = useTestAtCursor(testObject);
Expand Down
82 changes: 62 additions & 20 deletions packages/vest/src/core/test/lib/useTestAtCursor.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,76 @@
import defaultTo from 'defaultTo';
import { isNotEmpty } from 'isEmpty';
import removeElementFromArray from 'removeElementFromArray';
import { throwErrorDeferred } from 'throwError';

import VestTest from 'VestTest';
import isSameProfileTest from 'isSameProfileTest';
import {
useCursorAt,
usePrevTestObjects,
useSetTestAtCursor,
} from 'stateHooks';
import { useCursorAt, usePrevTestObjects, useTestObjects } from 'stateHooks';

export default function useTestAtCursor(initialValue: VestTest): VestTest {
const [cursorAt] = useCursorAt();
const [prevTestObjects] = usePrevTestObjects();
export function useTestAtCursor(newTestObject: VestTest): VestTest {
const [, setPrevTestObjects] = usePrevTestObjects();

if (
isNotEmpty(prevTestObjects[cursorAt]) &&
!isSameProfileTest(prevTestObjects[cursorAt], initialValue)
) {
throwErrorDeferred(`Vest Critical Error: Tests called in different order than previous run.
The test at cursor ${cursorAt} was not the same profile as the previous test.
expected: ${JSON.stringify(prevTestObjects[cursorAt])}
actual: ${JSON.stringify(initialValue)}
This usually happens when you conditionally call your tests using if/else.
This might lead to unexpected behavior in your test results.
Replacing if/else with skipWhen solves these issues.`);
let prevTest = useGetTestAtCursor();

if (shouldPurgePrevTest(prevTest, newTestObject)) {
throwTestOrderError(prevTest, newTestObject);

// TODO: This is only half of the story.
// Here we handle just the omission of tests in the middle of the test suite.
// We need to also handle a case in which tests are added in between other tests, at the
// at the moment all we can do is just remove tests until we find a matching test.
// A viable solution would be to use something like React's key prop to identify tests regardless
// of their position in the suite. https://reactjs.org/docs/lists-and-keys.html#keys
// This is most important when using test.each.
while (shouldPurgePrevTest(prevTest, newTestObject)) {
setPrevTestObjects(prevTestObjects =>
removeElementFromArray(prevTestObjects, prevTest)
);

prevTest = useGetTestAtCursor();
}
}

const nextTest = defaultTo(prevTestObjects[cursorAt], initialValue);
const nextTest = defaultTo(prevTest, newTestObject);
useSetTestAtCursor(nextTest);

return nextTest;
}

export function useSetTestAtCursor(testObject: VestTest): void {
const [cursorAt] = useCursorAt();
const [testObjects, setTestObjects] = useTestObjects();

if (testObject === testObjects[cursorAt]) {
return;
}

setTestObjects((testObjects: VestTest[]) => {
const newTestsOrder = testObjects.slice(0);
newTestsOrder[cursorAt] = testObject;
return newTestsOrder;
});
}

function useGetTestAtCursor(): VestTest {
const [cursorAt] = useCursorAt();
const [prevTestObjects] = usePrevTestObjects();

return prevTestObjects[cursorAt];
}

function shouldPurgePrevTest(prevTest: VestTest, newTest: VestTest): boolean {
return isNotEmpty(prevTest) && !isSameProfileTest(prevTest, newTest);
}

function throwTestOrderError(
prevTest: VestTest,
newTestObject: VestTest
): void {
throwErrorDeferred(`Vest Critical Error: Tests called in different order than previous run.
expected: ${prevTest.fieldName}
received: ${newTestObject.fieldName}
This usually happens when you conditionally call your tests using if/else.
This might lead to unexpected validation results.
Replacing if/else with skipWhen solves these issues.`);
}

0 comments on commit 49b72f4

Please sign in to comment.