Skip to content
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

fix: Stop calling waitFor callback after timeout #1271

Merged
merged 7 commits into from
Jan 8, 2024

Conversation

KevinBon
Copy link
Contributor

@KevinBon KevinBon commented Oct 3, 2023

What:

Prevent waitFor callback from being invoked even after it resolved, on waitFor timeout.

Why:

More context can be found in this issue, but with that context in mind, it's preventing test side-effects:

afterEach should be enough to "clean" the previous test from any side-effect. However, because of the callback leak issue, window.variable will remain to be 123 even after being set back to 1 for a short period of time.

afterEach(() => {
 window.variable = 1;
})
it('my test', () => {
  return waitFor(() => {
    window.variable = 123;
    throw new Error('I want it to timeout');
  })
})

How:

When the clock is mocked, only call checkCallback when finished is false

Checklist:

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

resolves #1270

@codesandbox-ci
Copy link

codesandbox-ci bot commented Oct 3, 2023

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 10a1ac7:

Sandbox Source
react-testing-library-examples Configuration

@codecov
Copy link

codecov bot commented Oct 3, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (a7b7252) 100.00% compared to head (10a1ac7) 100.00%.
Report is 4 commits behind head on alpha.

Additional details and impacted files
@@            Coverage Diff            @@
##             alpha     #1271   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           24        24           
  Lines         1038      1041    +3     
  Branches       346       347    +1     
=========================================
+ Hits          1038      1041    +3     
Flag Coverage Δ
node-14 100.00% <100.00%> (ø)
node-16 ?
node-18 100.00% <100.00%> (ø)
node-20 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@eps1lon eps1lon 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 working on the fix. However, we should rather make sure that callback is not being invoked again. The proposed fix just hides the issue.

Also: Can you provide a minimal repro against which we can test this fix? Ideally we could extract a unit test from it.

@KevinBon
Copy link
Contributor Author

KevinBon commented Oct 3, 2023

@eps1lon I changed the solution implementation.

You can find a reproduction example on this repository.

Output example:

image

It's been a while since I have used jest, so I'm not sure how to test "this behavior" on jest to satisfy the test code coverage

Don't hesitate if you have any other questions 🙇

@github-actions
Copy link

github-actions bot commented Oct 3, 2023

Uh oh! @KevinBon, the image you shared is missing helpful alt text. Check #1271 (comment).

Alt text is an invisible description that helps screen readers describe images to blind or low-vision users. If you are using markdown to display images, add your alt text inside the brackets of the markdown image.

Learn more about alt text at Basic writing and formatting syntax: images on GitHub Docs.

@KevinBon
Copy link
Contributor Author

KevinBon commented Oct 3, 2023

I created a branch to show-case that applying this patch will fix this behavior: KevinBon/jasmine-tl-waitfor-leak#1

src/wait-for.js Outdated
@@ -160,6 +163,9 @@ function waitFor(
}

function handleTimeout() {
if (finished) {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we cancel the timeout instead when we finish?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When we finish, onDone should have been called with clearTimeout (src), therefore I don't really think this one is needed, but it's more for extra-safety.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Amended in 6b41483

src/wait-for.js Outdated
@@ -134,7 +137,7 @@ function waitFor(
}

function checkCallback() {
if (promiseStatus === 'pending') return
if (finished || promiseStatus === 'pending') return
Copy link
Member

Choose a reason for hiding this comment

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

Can we cancel whatever is calling checkCallback instead?

Copy link
Contributor Author

@KevinBon KevinBon Oct 3, 2023

Choose a reason for hiding this comment

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

I would love to, but I'm afraid it might introduce breaking changes.

We'll need to move the if(finished) before checkCallback here: https://github.com/testing-library/dom-testing-library/pull/1271/files#diff-68584107d5f50eb95702a47b23bb6e0f6c2d78f9bab7456a1a4150c79229db78R84-R92

But the following comment is daunting:

// It's really important that checkCallback is run *before* we flush
// in-flight promises. To be honest, I'm not sure why, and I can't quite
// think of a way to reproduce the problem in a test, but I spent
// an entire day banging my head against a wall on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Amended in 6b41483

@@ -81,15 +81,15 @@ function waitFor(
jest.advanceTimersByTime(interval)
})

// Could have timed-out
if (finished) {
Copy link
Member

Choose a reason for hiding this comment

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

With this factoring, I wonder if we could simplify further by moving checkCallback before the advanceTimers. That way the loop condition makes sure we exit as soon as we're finished. And we can remove another checkCallback on L54.

At this point I would remove the "It's really important that checkCallback [...]" comment. We might have fixed the issue along the way. And since I'd rather ship this with the next major, we have a good opportunity to resurface the bug so that we can at least link a repro. It doesn't have to be a Jest test but somehow the reordering fixed "something". That "something" needs to be linked a t least. Doesn't need to be an automated test but it needs to be something. Otherwise we're chasing ghosts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we move checkCallback before the advanceTimers, the waitFor callback will be called twice before we advance the clock, instead of once right now.

Which will break this fix: https://github.com/testing-library/dom-testing-library/pull/1073/files

Copy link
Member

Choose a reason for hiding this comment

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

the waitFor callback will be called twice before we advance the clock, instead of once right now.

but not if we remove the checkCallback call earlier from L54, no?

Copy link
Contributor Author

@KevinBon KevinBon Oct 4, 2023

Choose a reason for hiding this comment

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

There will be a change of behavior in

).rejects.toMatchInlineSnapshot(`[Error: always throws]`)
, for a timeout smaller than the interval, it will instead be rejected with [Error: Timed out in waitFor.]

@eps1lon eps1lon changed the base branch from main to alpha October 4, 2023 08:37
@@ -292,12 +292,11 @@ test('the real timers => fake timers error shows the original stack trace when c

test('does not work after it resolves', async () => {
jest.useFakeTimers('modern')
let context = 'initial'
const contextStack = []
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or

Suggested change
const contextStack = []
const contextCallStack = []

@KevinBon
Copy link
Contributor Author

KevinBon commented Oct 4, 2023

@eps1lon I added a reproducible test for jest through ce11c0f


expect(context).toEqual('initial')
test(`when fake timer is installed, on waitFor timeout, it doesn't call the callback afterward`, async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test ensures that the fix is working

@@ -292,12 +292,11 @@ test('the real timers => fake timers error shows the original stack trace when c

test('does not work after it resolves', async () => {
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 made some changes on this test to help understand the outcome: everything that starts needs to end

@KevinBon KevinBon changed the title Fix waitfor callback leak Fix waitFor callback leak on waitFor timeout Oct 4, 2023
@KevinBon KevinBon requested a review from eps1lon October 4, 2023 17:01
@KevinBon
Copy link
Contributor Author

@eps1lon let me know if you need additional information or are unhappy with my changes 🙇

@KevinBon
Copy link
Contributor Author

@eps1lon bumping, let me know if you need anything from me. Thank you!

@eps1lon
Copy link
Member

eps1lon commented Jan 8, 2024

Thanks for the patience. Checking this out now so that we can land it ASAP.

@eps1lon eps1lon changed the title Fix waitFor callback leak on waitFor timeout fix: waitFor callback leak on waitFor timeout Jan 8, 2024
@eps1lon eps1lon changed the title fix: waitFor callback leak on waitFor timeout fix: Stop calling waitFor callback after timeout Jan 8, 2024
Copy link
Member

@eps1lon eps1lon left a comment

Choose a reason for hiding this comment

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

Looks great, thank you. I verified the tests are actually failing on main

eps1lon
eps1lon previously approved these changes Jan 8, 2024
Copy link
Member

@eps1lon eps1lon left a comment

Choose a reason for hiding this comment

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

Looks great, thank you. I verified the tests are actually failing on main

@eps1lon eps1lon changed the base branch from alpha to main January 8, 2024 20:10
@eps1lon eps1lon dismissed their stale review January 8, 2024 20:10

The base branch was changed.

@eps1lon
Copy link
Member

eps1lon commented Jan 8, 2024

@all-contributors add @KevinBon for code, bugs

Copy link
Contributor

@eps1lon

I've put up a pull request to add @KevinBon! 🎉

@eps1lon eps1lon merged commit 9aaf715 into testing-library:main Jan 8, 2024
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Leaking waitFor callback against jasmine when the clock is mocked
3 participants