From 2d28ea7622c61d47969f86a20149482bcba34ae1 Mon Sep 17 00:00:00 2001 From: Evyatar Date: Mon, 25 Sep 2023 02:50:55 +0300 Subject: [PATCH] feat(vest): Add AbortSignal as a parameter to test (#1079) --- .../core/isolate/IsolateTest/IsolateTest.ts | 2 + .../src/core/isolate/IsolateTest/VestTest.ts | 1 + packages/vest/src/core/test/TestTypes.ts | 4 +- .../__snapshots__/IsolateTest.test.ts.snap | 18 ++++ .../__tests__/__snapshots__/test.test.ts.snap | 27 ++++++ .../vest/src/core/test/__tests__/test.test.ts | 2 +- .../__tests__/testFunctionPayload.test.ts | 83 +++++++++++++++++++ .../core/test/testLevelFlowControl/runTest.ts | 4 +- .../SuiteSerializer.test.ts.snap | 2 +- .../__snapshots__/staticSuite.test.ts.snap | 36 ++++++++ website/docs/writing_tests/async_tests.md | 20 +++++ 11 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 packages/vest/src/core/test/__tests__/testFunctionPayload.test.ts diff --git a/packages/vest/src/core/isolate/IsolateTest/IsolateTest.ts b/packages/vest/src/core/isolate/IsolateTest/IsolateTest.ts index 84fea2062..afb4fa2ab 100644 --- a/packages/vest/src/core/isolate/IsolateTest/IsolateTest.ts +++ b/packages/vest/src/core/isolate/IsolateTest/IsolateTest.ts @@ -47,6 +47,7 @@ export function IsolateTestBase() { return { severity: TestSeverity.Error, status: TestStatus.UNTESTED, + abortController: new AbortController(), }; } @@ -57,6 +58,7 @@ export type IsolateTestPayload< severity: TestSeverity; status: TestStatus; asyncTest?: AsyncTest; + abortController: AbortController; }; type CommonTestFields< diff --git a/packages/vest/src/core/isolate/IsolateTest/VestTest.ts b/packages/vest/src/core/isolate/IsolateTest/VestTest.ts index a87725d86..ee65bac1e 100644 --- a/packages/vest/src/core/isolate/IsolateTest/VestTest.ts +++ b/packages/vest/src/core/isolate/IsolateTest/VestTest.ts @@ -142,6 +142,7 @@ export class VestTest { static cancel(test: TIsolateTest): void { VestTest.setStatus(test, TestStatus.CANCELED); + VestTest.getData(test).abortController.abort(TestStatus.CANCELED); } static omit(test: TIsolateTest): void { diff --git a/packages/vest/src/core/test/TestTypes.ts b/packages/vest/src/core/test/TestTypes.ts index becacb5f4..3b966e1bf 100644 --- a/packages/vest/src/core/test/TestTypes.ts +++ b/packages/vest/src/core/test/TestTypes.ts @@ -2,7 +2,9 @@ import { Maybe } from 'vest-utils'; import { TFieldName } from 'SuiteResultTypes'; -export type TestFn = () => TestResult; +type TestFnPayload = { signal: AbortSignal }; + +export type TestFn = (payload: TestFnPayload) => TestResult; export type AsyncTest = Promise; export type TestResult = Maybe; diff --git a/packages/vest/src/core/test/__tests__/__snapshots__/IsolateTest.test.ts.snap b/packages/vest/src/core/test/__tests__/__snapshots__/IsolateTest.test.ts.snap index f6af009a1..908366a58 100644 --- a/packages/vest/src/core/test/__tests__/__snapshots__/IsolateTest.test.ts.snap +++ b/packages/vest/src/core/test/__tests__/__snapshots__/IsolateTest.test.ts.snap @@ -5,6 +5,15 @@ exports[`IsolateTest TestObject constructor 1`] = ` "$type": "UnitTest", "children": [], "data": { + "abortController": AbortController { + Symbol(signal): AbortSignal { + Symbol(kEvents): Map {}, + Symbol(events.maxEventTargetListeners): 10, + Symbol(events.maxEventTargetListenersWarned): false, + Symbol(kAborted): false, + Symbol(kReason): undefined, + }, + }, "fieldName": "unicycle", "message": "I am Root.", "severity": "error", @@ -24,6 +33,15 @@ exports[`IsolateTest testObject.warn Should mark the test as warning 1`] = ` "$type": "UnitTest", "children": [], "data": { + "abortController": AbortController { + Symbol(signal): AbortSignal { + Symbol(kEvents): Map {}, + Symbol(events.maxEventTargetListeners): 10, + Symbol(events.maxEventTargetListenersWarned): false, + Symbol(kAborted): false, + Symbol(kReason): undefined, + }, + }, "fieldName": "unicycle", "message": "I am Root.", "severity": "warning", diff --git a/packages/vest/src/core/test/__tests__/__snapshots__/test.test.ts.snap b/packages/vest/src/core/test/__tests__/__snapshots__/test.test.ts.snap index 9fda6e6da..07db22bb4 100644 --- a/packages/vest/src/core/test/__tests__/__snapshots__/test.test.ts.snap +++ b/packages/vest/src/core/test/__tests__/__snapshots__/test.test.ts.snap @@ -6,6 +6,15 @@ exports[`Test Vest's \`test\` function test params creates a test without a key "allowReorder": undefined, "children": null, "data": { + "abortController": AbortController { + Symbol(signal): AbortSignal { + Symbol(kEvents): Map {}, + Symbol(events.maxEventTargetListeners): 10, + Symbol(events.maxEventTargetListenersWarned): false, + Symbol(kAborted): false, + Symbol(kReason): undefined, + }, + }, "fieldName": "field_name", "message": "failure message", "severity": "error", @@ -73,6 +82,15 @@ exports[`Test Vest's \`test\` function test params creates a test without a mess "allowReorder": undefined, "children": null, "data": { + "abortController": AbortController { + Symbol(signal): AbortSignal { + Symbol(kEvents): Map {}, + Symbol(events.maxEventTargetListeners): 10, + Symbol(events.maxEventTargetListenersWarned): false, + Symbol(kAborted): false, + Symbol(kReason): undefined, + }, + }, "fieldName": "field_name", "severity": "error", "status": "PASSING", @@ -141,6 +159,15 @@ exports[`Test Vest's \`test\` function test params creates a test without a mess "allowReorder": undefined, "children": null, "data": { + "abortController": AbortController { + Symbol(signal): AbortSignal { + Symbol(kEvents): Map {}, + Symbol(events.maxEventTargetListeners): 10, + Symbol(events.maxEventTargetListenersWarned): false, + Symbol(kAborted): false, + Symbol(kReason): undefined, + }, + }, "fieldName": "field_name", "severity": "error", "status": "PASSING", diff --git a/packages/vest/src/core/test/__tests__/test.test.ts b/packages/vest/src/core/test/__tests__/test.test.ts index e75401367..a60229813 100644 --- a/packages/vest/src/core/test/__tests__/test.test.ts +++ b/packages/vest/src/core/test/__tests__/test.test.ts @@ -193,7 +193,7 @@ describe("Test Vest's `test` function", () => { expect(testObject.key).toBe('keyboardcat'); expect(testObject.data.message).toBe('failure message'); expect(IsolateSerializer.serialize(testObject)).toMatchInlineSnapshot( - `"{"$":"Test","D":{"severity":"error","status":"PASSING","fieldName":"field_name","message":"failure message"},"k":"keyboardcat"}"` + `"{"$":"Test","D":{"severity":"error","status":"PASSING","abortController":{},"fieldName":"field_name","message":"failure message"},"k":"keyboardcat"}"` ); }); diff --git a/packages/vest/src/core/test/__tests__/testFunctionPayload.test.ts b/packages/vest/src/core/test/__tests__/testFunctionPayload.test.ts new file mode 100644 index 000000000..e34248f8a --- /dev/null +++ b/packages/vest/src/core/test/__tests__/testFunctionPayload.test.ts @@ -0,0 +1,83 @@ +import * as vest from 'vest'; + +describe('Test Function Payload', () => { + describe('AbortSignal', () => { + it('Should pass abort signal to test functions', () => { + const testFnSync = jest.fn(); + const testFnAsync = jest.fn().mockResolvedValue(undefined); + const suite = vest.create(() => { + vest.test('field_1', testFnSync); + vest.test('field_2', testFnAsync); + }); + suite(); + + expect(callPayload(testFnSync).signal).toBeInstanceOf(AbortSignal); + expect(callPayload(testFnAsync).signal).toBeInstanceOf(AbortSignal); + }); + + describe('When test is not canceled', () => { + it('Should proceed without aborting the test', async () => { + const testFn = jest.fn().mockResolvedValue(undefined); + const suite = vest.create(() => { + vest.test('field_1', testFn); + }); + suite(); + + await expect(callPayload(testFn).signal.aborted).toBe(false); + }); + }); + + describe('When test is canceled', () => { + it('Should abort the test', async () => { + const testFn = jest.fn().mockResolvedValue(undefined); + const suite = vest.create(() => { + vest.test('field_1', testFn); + }); + suite(); + suite(); + + await expect(callPayload(testFn).signal.aborted).toBe(true); + await expect(callPayload(testFn, 1, 0).signal.aborted).toBe(false); + }); + + it('Should set the reason to `canceled`', async () => { + const testFn = jest.fn().mockResolvedValue(undefined); + const suite = vest.create(() => { + vest.test('field_1', testFn); + }); + suite(); + suite(); + + await expect(callPayload(testFn).signal.reason).toBe('CANCELED'); + }); + }); + + describe('Multiple async tests', () => { + it('Should abort only the canceled test', async () => { + const testFn1 = jest.fn().mockResolvedValue(undefined); + const testFn2 = jest.fn().mockResolvedValue(undefined); + + const suite = vest.create((only?: string) => { + vest.only(only); + + vest.test('field_1', testFn1); + vest.test('field_2', testFn2); + }); + + suite(); + suite('field_1'); + + await expect(callPayload(testFn1).signal.aborted).toBe(true); + expect(callPayload(testFn2).signal.aborted).toBe(false); + }); + }); + }); +}); + +function callPayload( + fn: jest.Mock, + call: number = 0, + arg: number = 0 +) { + return fn.mock.calls[call][arg]; +} diff --git a/packages/vest/src/core/test/testLevelFlowControl/runTest.ts b/packages/vest/src/core/test/testLevelFlowControl/runTest.ts index f9ea2870c..75020789e 100644 --- a/packages/vest/src/core/test/testLevelFlowControl/runTest.ts +++ b/packages/vest/src/core/test/testLevelFlowControl/runTest.ts @@ -40,10 +40,10 @@ function runSyncTest(testObject: TIsolateTest): TestResult { return SuiteContext.run({ currentTest: testObject }, () => { let result: TestResult; - const { message, testFn } = VestTest.getData(testObject); + const { message, testFn, abortController } = VestTest.getData(testObject); try { - result = testFn(); + result = testFn({ signal: abortController.signal }); } catch (error) { if (shouldUseErrorAsMessage(message, error)) { VestTest.getData(testObject).message = error; diff --git a/packages/vest/src/exports/__tests__/__snapshots__/SuiteSerializer.test.ts.snap b/packages/vest/src/exports/__tests__/__snapshots__/SuiteSerializer.test.ts.snap index 7b418ded9..fe12a2c5b 100644 --- a/packages/vest/src/exports/__tests__/__snapshots__/SuiteSerializer.test.ts.snap +++ b/packages/vest/src/exports/__tests__/__snapshots__/SuiteSerializer.test.ts.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SuiteSerializer Should produce a valid serialized dump 1`] = `"{"children":[{"$":"Focused","D":{"focusMode":0,"match":["field_1"],"matchAll":false}},{"$":"Test","D":{"severity":"error","status":"FAILED","fieldName":"field_1","message":"field_1_message"}},{"$":"Test","D":{"severity":"error","status":"SKIPPED","fieldName":"field_2","message":"field_2_message"}},{"children":[{"$":"Test","D":{"severity":"error","status":"SKIPPED","fieldName":"field_3","groupName":"group_1","message":"field_3_message_1"}},{"$":"Test","D":{"severity":"error","status":"SKIPPED","fieldName":"field_3","groupName":"group_1","message":"field_3_message_2"}},{"$":"Test","D":{"severity":"error","status":"SKIPPED","fieldName":"field_4","groupName":"group_1","message":"field_4_message"}}],"$":"Group","D":{}},{"children":[{"$":"Test","D":{"severity":"error","status":"SKIPPED","fieldName":"field_5","message":"field_5_message"}}],"$":"SkipWhen","D":{}}],"$":"Suite","D":{"optional":{}}}"`; +exports[`SuiteSerializer Should produce a valid serialized dump 1`] = `"{"children":[{"$":"Focused","D":{"focusMode":0,"match":["field_1"],"matchAll":false}},{"$":"Test","D":{"severity":"error","status":"FAILED","abortController":{},"fieldName":"field_1","message":"field_1_message"}},{"$":"Test","D":{"severity":"error","status":"SKIPPED","abortController":{},"fieldName":"field_2","message":"field_2_message"}},{"children":[{"$":"Test","D":{"severity":"error","status":"SKIPPED","abortController":{},"fieldName":"field_3","groupName":"group_1","message":"field_3_message_1"}},{"$":"Test","D":{"severity":"error","status":"SKIPPED","abortController":{},"fieldName":"field_3","groupName":"group_1","message":"field_3_message_2"}},{"$":"Test","D":{"severity":"error","status":"SKIPPED","abortController":{},"fieldName":"field_4","groupName":"group_1","message":"field_4_message"}}],"$":"Group","D":{}},{"children":[{"$":"Test","D":{"severity":"error","status":"SKIPPED","abortController":{},"fieldName":"field_5","message":"field_5_message"}}],"$":"SkipWhen","D":{}}],"$":"Suite","D":{"optional":{}}}"`; diff --git a/packages/vest/src/suite/__tests__/__snapshots__/staticSuite.test.ts.snap b/packages/vest/src/suite/__tests__/__snapshots__/staticSuite.test.ts.snap index 5e2ada75e..c81bc15b1 100644 --- a/packages/vest/src/suite/__tests__/__snapshots__/staticSuite.test.ts.snap +++ b/packages/vest/src/suite/__tests__/__snapshots__/staticSuite.test.ts.snap @@ -10,6 +10,15 @@ exports[`staticSuite dump should output a dump of the suite 1`] = ` "allowReorder": undefined, "children": null, "data": { + "abortController": AbortController { + Symbol(signal): AbortSignal { + Symbol(kEvents): Map {}, + Symbol(events.maxEventTargetListeners): 10, + Symbol(events.maxEventTargetListenersWarned): false, + Symbol(kAborted): false, + Symbol(kReason): undefined, + }, + }, "fieldName": "t1", "severity": "error", "status": "FAILED", @@ -25,6 +34,15 @@ exports[`staticSuite dump should output a dump of the suite 1`] = ` "allowReorder": undefined, "children": null, "data": { + "abortController": AbortController { + Symbol(signal): AbortSignal { + Symbol(kEvents): Map {}, + Symbol(events.maxEventTargetListeners): 10, + Symbol(events.maxEventTargetListenersWarned): false, + Symbol(kAborted): false, + Symbol(kReason): undefined, + }, + }, "fieldName": "t2", "severity": "error", "status": "FAILED", @@ -44,6 +62,15 @@ exports[`staticSuite dump should output a dump of the suite 1`] = ` "allowReorder": undefined, "children": null, "data": { + "abortController": AbortController { + Symbol(signal): AbortSignal { + Symbol(kEvents): Map {}, + Symbol(events.maxEventTargetListeners): 10, + Symbol(events.maxEventTargetListenersWarned): false, + Symbol(kAborted): false, + Symbol(kReason): undefined, + }, + }, "fieldName": "t1", "groupName": "g1", "severity": "error", @@ -60,6 +87,15 @@ exports[`staticSuite dump should output a dump of the suite 1`] = ` "allowReorder": undefined, "children": null, "data": { + "abortController": AbortController { + Symbol(signal): AbortSignal { + Symbol(kEvents): Map {}, + Symbol(events.maxEventTargetListeners): 10, + Symbol(events.maxEventTargetListenersWarned): false, + Symbol(kAborted): false, + Symbol(kReason): undefined, + }, + }, "fieldName": "t3", "groupName": "g1", "severity": "error", diff --git a/website/docs/writing_tests/async_tests.md b/website/docs/writing_tests/async_tests.md index e1f980922..527c2e268 100644 --- a/website/docs/writing_tests/async_tests.md +++ b/website/docs/writing_tests/async_tests.md @@ -20,3 +20,23 @@ test('name', 'Already Taken', async () => { return await doesUserExist(user); }); ``` + +## Using AbortSignal + +> Since 5.1.0 + +Each test function is passed an object with a `signal` property. This signal is an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) which can be used to terminate your async operations once a test is canceled. + +The AbortSignal has a boolean `aborted` property, by which you can determine whether the test was canceled or not. + +A test gets canceled when running the same test again before its previous run has completed. + +You can use the AbortSignal to stop the execution of your async test, or pass it to your fetch request. + +[More on AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). + +```js +test('name', 'Already Taken', async ({ signal }) => { + // ... +}); +```