Skip to content

Conversation

LaurensBosscher
Copy link
Contributor

@LaurensBosscher LaurensBosscher commented Jul 4, 2019

What:

Utilizes the original (non-mocked) setTimeout for waitForDomChange, waitForElement and waitForElementToBeRemoved

Why:

waitForDomChange, waitForElement and waitForElementToBeRemoved will now work as expected when the user utilizes this library in a test that uses jest.useFakeTimers(). See: #300

How:

I've implemented the workaround used by wait-for-expect: https://github.com/TheBrainFamily/wait-for-expect/pull/5/files. I've added a global (global.useFakeTimers) so that we can still utilize fake timers in our test.

getSetTimeout and getClearTimeout utilize dependency injection because it's not possible to set window to undefined in jest. I would love feedback on the tests that I've added, let me know if you think there's something that could be improved.

Checklist:

  • Documentation added to the
    docs site N/A
  • Typescript definitions updated N/A
  • Tests
  • Ready to be merged

src/helpers.js Outdated
*
* see: https://github.com/testing-library/dom-testing-library/issues/300
*/
function getSetTimeout(windowObject) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've injected the windowObject as a dependency here because (as far as I know) there's no way in Jest to set the window object to be undefined. This seemed to be the best way to make this code testable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this, you could add a test for this case to https://github.com/testing-library/dom-testing-library/tree/master/src/__node_tests__

Other than that, this looks pretty good to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kentcdodds Do you mean instead of dependency injection?

The node tests are still run within Jest and the main issue I faced was that in Jest window === globals (see: jestjs/jest#3692 (comment)) and I can't set globals (and thus window) to undefined.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the a reason the code can't be:

return typeof window === 'undefined' ? global.setTimeout : window.setTimeout

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated!

Copy link
Member

@kentcdodds kentcdodds Jul 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I was saying is you no longer need the windowObject argument with the code I provided.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I get that but how would I be able to test that: "returns original getSetTimeout from global if window is undefined" without passing windowObject?

Perhaps we could chat about what the best approach to test that case would be?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returns original getSetTimeout from global if window is undefined

You can add another test to the node environment tests, as I said earlier:

Instead of this, you could add a test for this case to https://github.com/testing-library/dom-testing-library/tree/master/src/__node_tests

Copy link
Contributor Author

@LaurensBosscher LaurensBosscher Jul 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhhhh, it finally clicked this morning. The node tests are run without JSDOM and thus without a window object. I'm new to this project and the deeper internals of Jest, that wasn't clear to me.

I've updated the PR, removed the dependency injection of window and updated the tests for the node environment. Let me know what you think!

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! Just a few suggestions/comments.


describe('getSetTimeout', () => {
it('returns mocked setTimeout if global.useFakeTimers is set and jest.useFakeTimers is used', () => {
// global.useFakeTimers is set to true for all tests in tests/setup-env.js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that's the case, then isn't jest.useFakeTimers() on the next line unnecessary and jest.useRealTimers() at the end of this test breaking test isolation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, now that I think of it, why are we setting useFakeTimers to true in setup-env? I think it'd be better if most of our tests run without fake timers wouldn't it?

Copy link
Contributor Author

@LaurensBosscher LaurensBosscher Jul 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

global.useFakeTimers is a boolean that enables fake timers if someone uses jest.useFakeTimers(). So all our tests should behave exactly the same as they did before this PR :)

I think we can improve upon the name of the boolean. How about global.returnMockedTimersWhenUsingJestFakeTimers? or perhaps global.isDomTestingLibraryInternalTests?

Or would you prefer that we leave this undefined by default and only set this to true in our tests that use fake timers (https://github.com/testing-library/dom-testing-library/search?q=jest.useFakeTimers&unscoped_q=jest.useFakeTimers)?

Doing that will mean that jest.useFakeTimers will not behave as expected which might confuse contributors which are unfamiliar with the code base. I figured it would be best to follow the principe of least surprise here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait. Are you saying that if people want to take advantage of this feature they have to set global.useFakeTimers? I'd really prefer to find another way to detect whether fake timers are happening... Maybe I don't understand what's going on/what the purpose of this is 😬

Copy link
Contributor Author

@LaurensBosscher LaurensBosscher Jul 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nop, nop, exactly the opposite! :) This behaviour is enabled by default for everyone who uses dom-testing-library in their application tests, but it's disabled for dom-testing-library internal tests.

I do think that I'm explaining and documenting this rather poorly. Let me explain it to you first with a bunch of examples, I hope that clears things up! Perhaps than you give me some pointers on how exactly I can communicate this behaviour better? Your suggestions are very much welcome!

If I, as a user, use dom-testing-library, all the dom-testing library methods that I use in my application tests will use a non-mocked setTimeout. So if I write:

test('My awesome application!', async (done) =>
  const { findByText } = render(<MyApplication/>);
  
  jest.useFakeTimers();
  await findByText('My application is awesome!');
  done();
);

dom-testing-library will check for 4.5 seconds if the text 'Awesome Text' is in my application.

Without this PR, the above test will most likely fail, because findByText uses a mocked setTimeout which reduces the timeout to 1 tick at which point my application is not ready.

If I, as a contributor to dom-testing-library, write a new test, for dom-testing-library, and if I write the following test:

test('dom-testing-library internal test example', async (done) =>
   ...
  jest.useFakeTimers();
  setTimeOut(async () => {
     await findByText('Dom testing library is awesome!')
     done();
  , 999};
);

This test will use the mocked setTimeout and thus fail/succeed within <10 msecs. This is exactly the same behaviour as before this PR.

If I, as a contributor to dom-testing-library, write a new test, for dom-testing-library, and if I write the following test:

test('dom-testing-library internal test example 2', async (done) =>
   ...
  setTimeOut(async () => {
     await findByText('Dom testing library is awesome!')
     done();
  , 999};
);

This test will run for 999 msec and then run findByText and fail/succeed.

The global is used to determine if we're running the dom-testing-library internal tests.

I hope this clears things up :)

The fix for wait-for-expect is a lot smaller because they don't have any internal tests that use jest.useFakeTimers() which greatly reduces the complexity.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see what you're saying. So the whole reason that we have the global thing is for internal testing purposes.

Hmm... I'm not sure I understand why it's necessary though. Why can't DOM Testing Library's tests work the same way that users of DTL test?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I've worked it out. I'm going to make a contribution to this to improve things I think.

Co-Authored-By: Kent C. Dodds <kent+github@doddsfamily.us>

describe('getSetTimeout', () => {
it('returns mocked setTimeout if global.useFakeTimers is set and jest.useFakeTimers is used', () => {
// global.useFakeTimers is set to true for all tests in tests/setup-env.js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, now that I think of it, why are we setting useFakeTimers to true in setup-env? I think it'd be better if most of our tests run without fake timers wouldn't it?

@kentcdodds
Copy link
Member

Here we go. I think this will be better :) Requires some fanciness in the test, but I'd rather put the fanciness in the tests than in the source code :)

kentcdodds
kentcdodds previously approved these changes Jul 11, 2019
@@ -0,0 +1,85 @@
import {render} from './helpers/test-utils'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've moved all the fake timers tests to this file. The only reason we're using fake timers is for testing timeouts and we don't want this test to take forever.

@@ -1,5 +1,17 @@
import MutationObserver from '@sheerun/mutationobserver-shim'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplifying this quite a bit by not using getters and instead just creating variables. Similar to how wait-for-expect does it.

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with this.

@kentcdodds kentcdodds merged commit dae25d5 into testing-library:master Jul 11, 2019
@kentcdodds
Copy link
Member

🎉 This PR is included in version 5.5.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants