-
-
Notifications
You must be signed in to change notification settings - Fork 6.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(circus): enable writing async test event handlers #9397
Conversation
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it! This also needs docs and tests. For tests, maybe just doing something async (e.g. storing current time, then waiting one second, asserting Date.now()
is at least 1000
higher than before, and throwing if not, maybe with some console.log
s just so we know the code triggered).
I'd love to hear any thoughts from @aaronabramov, @scotthovestadt and @DiZy on how this fits into their plans for circus.
|
||
export type Event = | ||
export type Event = SyncEvent | AsyncEvent; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not make all events async?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That handful of sync events have a very specific context of happening:
error
- called either insideNodeJS.UncaughtExceptionListener
orNodeJS.UnhandledRejectionListener
. Mind that both signatures are expected to returnvoid
, not aPromise<void>
:
start_describe_definition
andfinish_describe_definition
- they are called right as you call the global functiondescribe(msg, fn)
:
You see, to make those dispatches asynchronous, I should make the global describe
asynchronous first, and that seems risky to me, because:
a. We change the signature of the global describe
function, which is too pretentious, IMO.
b. We push our users into getting those nasty errors from the linters that pop whenever you call an asynchronous function in the global scope (outside of an async function):
c. Last, but not least, errors thrown inside an asynchronous describe()
won't have the same effect if they had been thrown in a synchronous one, right? Hence, it will break this logic, which is undesirable too:
const asyncError = new ErrorWithStack(undefined, describeFn);
if (blockFn === undefined) {
asyncError.message = `Missing second argument. It must be a callback function.`;
throw asyncError;
}
// ...
-
The same goes for
add_hook
(globalsbeforeEach
,afterEach
, ...) andadd_test
(globalsit
,test
,test.todo
, ...). I was not sure it was a good idea to change their signatures, so I decided to leave those events synchronous. -
As for
setup
andinclude_test_location_in_result
, it seems you are right. I am going to push a commit that makes them async and test it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that we'll probably change these errors (hook
, describe
, test
) to fail during test run, instead of during definition - see #9515. Only matters for 2.c and 3
I'm a bit afraid that an async handleTestEvent
function won't behave correctly if we don't await
all dispatch
es. Thoughts?
(as for 2.a, it's been a long standing issue that we can't have describe(async () => {})
, but that should not change in this PR, I agree 🙂)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be a very marginal case when somebody wants to do something really asynchronous for add_hook
and add_test
. It's sad we can't do everything at once here, but I see no easy way in the spirit of this PR to accomplish the all-events-async goal. I really hope we can ignore this handful of events here. I have written the warnings in the docs already.
packages/jest-types/src/Circus.ts
Outdated
@@ -31,9 +31,17 @@ export type Hook = { | |||
timeout: number | undefined | null; | |||
}; | |||
|
|||
export type EventHandler = (event: Event, state: State) => void; | |||
export type EventHandler = (event: Event, state: State) => void | Promise<void>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export type EventHandler = (event: Event, state: State) => void | Promise<void>; | |
export type EventHandler = | |
| ((event: SyncEvent, state: State) => void) | |
| ((event: AsyncEvent, state: State) => Promise<void>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SimenB, the suggested signature does not work well enough, leading to errors here:
I think I understand your intention to make EventHandler
type stricter, so hopefully, the version I have just committed is enough to your liking too:
export interface EventHandler {
(event: AsyncEvent, state: State): void | Promise<void>;
(event: SyncEvent, state: State): void;
}
I've left an ambiguous void | Promise<void>
result for the AsyncEvent
for the backward compatibility. See why:
If some project has already implemented an EventHandler
for jest-circus
returning void
, IMO, this use case remains perfectly valid. We should not force people into rewriting the handlers necessarily to async
functions, not to mention the radical distinction between sync and async events. Such a change would feel too breaking, to my taste.
The compromise void | Promise<void>
still might affect the existing codebase of someone, but that is fixable by a light extra typing. For instance, in jest/packages/*
codebase, after the change, I had to fix signatures in those two places:
diff --git a/packages/jest-circus/src/__mocks__/testEventHandler.ts b/packages/jest-circus/src/__mocks__/testEventHandler.ts
index 23dfe2bed..ad948f8d6 100644
--- a/packages/jest-circus/src/__mocks__/testEventHandler.ts
+++ b/packages/jest-circus/src/__mocks__/testEventHandler.ts
@@ -7,7 +7,10 @@
import {Circus} from '@jest/types';
-const testEventHandler: Circus.EventHandler = (event, state) => {
+const testEventHandler: Circus.EventHandler = (
+ event: Circus.Event,
+ state: Circus.State,
+) => {
switch (event.name) {
case 'start_describe_definition':
case 'finish_describe_definition': {
diff --git a/packages/jest-circus/src/eventHandler.ts b/packages/jest-circus/src/eventHandler.ts
index 7b059983d..b947bc7d9 100644
--- a/packages/jest-circus/src/eventHandler.ts
+++ b/packages/jest-circus/src/eventHandler.ts
@@ -21,7 +21,10 @@ import {
restoreGlobalErrorHandlers,
} from './globalErrorHandlers';
-const eventHandler: Circus.EventHandler = (event, state): void => {
+const eventHandler: Circus.EventHandler = (
+ event: Circus.Event,
+ state: Circus.State,
+): void => {
That way or another, I mean just to warn you that going away from the former signature of mine:
export type EventHandler = (event: Event, state: State) => void | Promise<void>;
most likely will lead to guaranteed TypeScript errors in projects that already implement some EventHandler
s on top of jest-circus
. Please advise if this is something critical for decision-making here.
@SimenB, so, how do you think, will adding such kind of a test suffice or we need something more? import {runTest} from '../__mocks__/testUtils';
test('test event handler should be able to be async and pause the flow', () => {
const {stdout} = runTest(`
describe('slowdown via testEventHandler', () => {
let t0;
beforeEach(() => { t0 = Date.now() });
it(':slowdown(1000):', () => {});
afterEach(() => expect(Date.now() - t0).toBeGreaterThan(1000));
});
describe('no slowdown', () => {
let t0;
beforeEach(() => { t0 = Date.now() });
it('fast test', () => {});
afterEach(() => expect(Date.now() - t0).toBeLessThan(100));
});
`);
expect(stdout).toMatchSnapshot();
}); The idea is to add a couple of lines to |
I've tried pinging som fb people about their opinion in this, without much luck. I find it appealing, but I also know FB has some pretty deep integrations with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dig it
cc @scotthovestadt for FB implications |
🎉 I'd like to see a test or two plus documentation updates (look at #8344 for where to update docs) before landing this. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@SimenB , how do you think are we ready now to get merged? |
This is amazing! 🥇 |
Hey guys, is there anything missing you want us to take care of? Or can this be merged? |
Codecov Report
@@ Coverage Diff @@
## master #9397 +/- ##
==========================================
- Coverage 64.90% 64.88% -0.02%
==========================================
Files 288 288
Lines 12195 12200 +5
Branches 3024 3022 -2
==========================================
+ Hits 7915 7916 +1
- Misses 3639 3643 +4
Partials 641 641
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for keeping this updated @noomorph, and sorry about the slow response!
I haven't really heard anything back from FB peeps on this one (except for Rick approving, which is promising 🙂), so I'm thinking this is good to go. Might wait for Jest 26 just to be safe though? Going from sync to async seems like a breaking change, but maybe not since we don't care if the callee is async, it's all on our side
/cc @cpojer @DiZy @scotthovestadt @jeysal @thymikee any objections to landing this?
e2e/test-environment-circus/CircusAsyncHandleTestEventEnvironment.js
Outdated
Show resolved
Hide resolved
e2e/test-environment-circus/CircusAsyncHandleTestEventEnvironment.js
Outdated
Show resolved
Hide resolved
…ent.js Co-Authored-By: Simen Bekkhus <[email protected]>
…ent.js Co-Authored-By: Simen Bekkhus <[email protected]>
Okay, so, I hope I have addressed all the review comments at the moment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this looks great 👍
My only question now is if this would be considered a breaking change (meaning it should wait for Jest 26) or if it's safe to land.
Thoughts?
@SimenB, I am not sure to whom the question is addressed, but from my side, it looks like an extension to the existing contract, which does not break anything that existed before. The only caveat I personally see here is maybe some potential TypeScript errors (that explicit |
Right, that's my thinking as well. We're now supporting something we didn't support before, so it shouldn't break anyone. Allright, then I'm thinking I'll merge and release this tomorrow unless somebody thinks we should not 🙂 (Last ping, I swear) @cpojer @DiZy @scotthovestadt |
Thanks for the great PR and patience @noomorph! |
@noomorph available in https://github.com/facebook/jest/releases/tag/v25.3.0, looking forward to seeing what you build with it. 🙂 |
Thrilled to see the release, thanks much! Now the ball is in my court, right. Stay tuned :-) |
…pshots * upstream/master: (225 commits) docs: add CLA link to contributing docs (jestjs#9789) chore: roll new version of docs v25.3.0 chore: update changelog for release chore(jest-types): correct type testRegex for ProjectConfig (jestjs#9780) feat(circus): enable writing async test event handlers (jestjs#9397) feat: enable all babel syntax plugins (jestjs#9774) chore: add helper for getting Jest's config in e2e tests (jestjs#9770) feat: pass ESM options to transformers (jestjs#9597) chore: replace `any`s with `unknown`s (jestjs#9626) feat: pass ESM options to Babel (jestjs#9766) chore(website): add copy button the code blocks (jestjs#9750) chore: bump istanbul-reports for new uncovered lines design (jestjs#9758) chore: correct CHANGELOG.md (jestjs#9763) chore(jest-types): expose type `CacheKeyOptions` for `getCacheK… (jestjs#9762) docs: Fix simple typo, seperated -> separated (jestjs#9760) v25.2.7 chore: update changelog for release fix: drop getters and setters when diffing objects for error (jestjs#9757) chore(jest-types): correct return type of shouldRunTestSuite fo… (jestjs#9753) ...
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Summary
While
jest-circus
provideshandleTestEvent(event, state): void
interface, it is not helpful for some scenarios inherent to end-to-end testing, as in case of Detox.I'll illustrate that by a couple of examples:
Decide the fate of a test artifact
Let's say we have those in our
<setupFilesAfterEnv>.js
file:The problem here, that inside
afterEach
hook, you can't know for sure whether the entire test is passing or failing, because you are still executing its part, albeit a mere and probably the last hook.If we move our logic to
handleTestEvent
provided byjest-circus
, then everything is fine...... except for the fact that you can't simply make
handleTestEvent
asynchronous. 🙂And this is what this pull request is trying to solve.
Take a screenshot right after an error happens
If you want to take a screenshot right after a failed expectation or any other error, theoretically, you could write something like this:
You could even write a wrapper for
it
, e.g.safeIt
or monkey-patchit
. But then, there are alsobeforeEach
,afterEach
,beforeAll
andafterAll
hooks, which can be renamed tosafe...
or monkey-patched as well. In theory, there are alsotest
,test.each
and some other functions, and one could take them into the account too.But is it the right solution, when
jest-circus
provides the API for handling test failures?Again, here we hit the limitation that you can't make it actually
await
before proceeding.That leaves us with a chance that another
afterEach
hook or next E2E test is going to run while our spawned child process is making a screenshot on iOS or Android. Having a race condition, even if a hypothetical one, is not something that we would want to have when speaking of consistent post-error screenshots. I would prefer to know for sure that we await for such artifact actions to be completed before proceeding any further.Conclusion
Making
handleTestEvent
asynchronous will simplify and improve a few essential things in Detox: user setup and better post-test artifacts.detox.beforeEach
anddetox.afterEach
(or adapters) inside user code. Detox will take care of that itself viajest-circus
. The setup part will be able to shrink significantly to:Current Jest Circus: https://jenkins-oss.wixpress.com/job/multi-detox-pr/1862/
Forked Jest Circus: https://jenkins-oss.wixpress.com/job/multi-detox-pr/1861/
In this build, I deliberately fail two tests, one in
beforeEach
hook, second — init
test function, and with my fork of jest-circus I become able to get distinct post-error screenshots for both those tests:Test plan
I have tested the change with Detox e2e test suite, and superficially it works OK and I receive what I expect there. The test runner completes without any weird or suspicious messages, and the failures can be seen only in places where I expect them to be.
I have run
yarn
on the entirejest
monorepo, and it has not yielded any errors.Still, I think, I might need your guidance on how better to test this change. Your ideas are very welcome.
Looking forward to our collaboration,
Yaroslav.