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

When running in Jest, code assumes setImmediate and clearImmediate #914

Closed
SimenB opened this issue Mar 20, 2021 · 12 comments · Fixed by #916
Closed

When running in Jest, code assumes setImmediate and clearImmediate #914

SimenB opened this issue Mar 20, 2021 · 12 comments · Fixed by #916
Labels
bug Something isn't working help wanted Extra attention is needed needs discussion We need to discuss this to come up with a good solution

Comments

@SimenB
Copy link
Contributor

SimenB commented Mar 20, 2021

Relevant code or config:

require('@testing-libray/react')

What you did:

Ran the test without global setImmediate and clearImmediate defined

What happened:

image

Reproduction:

See jestjs/jest#11222. Essentially, delete setImmediate and clearImmediate from global in the JSDOM env Jest sets up.

Problem description:

It throws runtime errors at require/import-time

Suggested solution:

Do a typeof setImmediate !== 'undefined' before using it.

@kentcdodds kentcdodds added bug Something isn't working good first issue Good for newcomers help wanted Extra attention is needed needs discussion We need to discuss this to come up with a good solution and removed good first issue Good for newcomers labels Mar 20, 2021
@kentcdodds
Copy link
Member

If I'm not mistaken, the only place we rely on a global setImmediate is here:

const timerAPI = {
clearImmediate,
clearInterval,
clearTimeout,
setImmediate,
setInterval,
setTimeout,
}

That could probably be changed to use the setImmediatePolyfill and we could probably create a clearImmediate pollyfill as well.

@craigkovatch
Copy link

What is it used for? setImmediate is only supported by IE10/11, and not on any kind of standards plan. To me it seems an odd choice to polyfill something that won’t ever exist natively in a modern browser.

@gaearon
Copy link

gaearon commented Mar 25, 2021

So it seems like the place that uses this (linked above) is just this helper which is meant to runWithJestRealTimers. But if Jest removes setImmediate in jsdom (which is the plan), then there's no need for setImmediate to be in that list.

Therefore, the fix is just to change this code to be resilient to setImmediate not existing. I don't think any polyfilling is needed there.

@renatoalencar
Copy link
Contributor

What is it used for? setImmediate is only supported by IE10/11, and not on any kind of standards plan. To me it seems an odd choice to polyfill something that won’t ever exist natively in a modern browser.

setImmediate is supported by Node.js and it looks like is used to wait for changes in the DOM. There's already a polyfill in the code but used in other part of the code. It looks like this issue comes from this function runWithJestRealTimers, which really doesn't make sense to patch with a polyfill.

@gaearon
Copy link

gaearon commented Mar 25, 2021

Proposed fix:

  const timerAPI = {
    clearInterval,
    clearTimeout,
    setInterval,
    setTimeout,
  }
  if (typeof setImmediate === 'function') {
    timerAPI.setImmediate = setImmediate;
  }
  if (typeof clearImmediate === 'function') {
    timerAPI.clearImmediate = clearImmediate;
  }
  // ...

@renatoalencar
Copy link
Contributor

Proposed fix:

  const timerAPI = {
    clearInterval,
    clearTimeout,
    setInterval,
    setTimeout,
  }
  if (typeof setImmediate === 'function') {
    timerAPI.setImmediate = setImmediate;
  }
  if (typeof clearImmediate === 'function') {
    timerAPI.clearImmediate = clearImmediate;
  }
  // ...

This function depends on setImmediate to work properly in order to work, I think this could probably use the polyfill declared below in the same file.

function waitForDomChange({
  container = getDocument(),
  timeout = getConfig().asyncUtilTimeout,
  mutationObserverOptions = {
    subtree: true,
    childList: true,
    attributes: true,
    characterData: true,
  },
} = {}) {
  if (!hasWarned) {
    hasWarned = true
    console.warn(
      `\`waitForDomChange\` has been deprecated. Use \`waitFor\` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.`,
    )
  }
  return new Promise((resolve, reject) => {
    const timer = setTimeout(onTimeout, timeout)
    const {MutationObserver} = getWindowFromNode(container)
    const observer = new MutationObserver(onMutation)
    runWithRealTimers(() =>
      observer.observe(container, mutationObserverOptions),
    )

    function onDone(error, result) {
      clearTimeout(timer)
      setImmediate(() => observer.disconnect())
      if (error) {
        reject(error)
      } else {
        resolve(result)
      }
    }

    function onMutation(mutationsList) {
      onDone(null, mutationsList)
    }

    function onTimeout() {
      onDone(new Error('Timed out in waitForDomChange.'), null)
    }
  })
}

@facundop3
Copy link

Proposed fix:

  const timerAPI = {
    clearInterval,
    clearTimeout,
    setInterval,
    setTimeout,
  }
  if (typeof setImmediate === 'function') {
    timerAPI.setImmediate = setImmediate;
  }
  if (typeof clearImmediate === 'function') {
    timerAPI.clearImmediate = clearImmediate;
  }
  // ...

This function depends on setImmediate to work properly in order to work, I think this could probably use the polyfill declared below in the same file.

function waitForDomChange({
  container = getDocument(),
  timeout = getConfig().asyncUtilTimeout,
  mutationObserverOptions = {
    subtree: true,
    childList: true,
    attributes: true,
    characterData: true,
  },
} = {}) {
  if (!hasWarned) {
    hasWarned = true
    console.warn(
      `\`waitForDomChange\` has been deprecated. Use \`waitFor\` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.`,
    )
  }
  return new Promise((resolve, reject) => {
    const timer = setTimeout(onTimeout, timeout)
    const {MutationObserver} = getWindowFromNode(container)
    const observer = new MutationObserver(onMutation)
    runWithRealTimers(() =>
      observer.observe(container, mutationObserverOptions),
    )

    function onDone(error, result) {
      clearTimeout(timer)
      setImmediate(() => observer.disconnect())
      if (error) {
        reject(error)
      } else {
        resolve(result)
      }
    }

    function onMutation(mutationsList) {
      onDone(null, mutationsList)
    }

    function onTimeout() {
      onDone(new Error('Timed out in waitForDomChange.'), null)
    }
  })
}

@renatoalencar is it possible to switch the setImmediate in favour of setTimeout(()=> observer.disconnect(), 0) in waitForDomChange implementation ?

@renatoalencar
Copy link
Contributor

Proposed fix:

  const timerAPI = {
    clearInterval,
    clearTimeout,
    setInterval,
    setTimeout,
  }
  if (typeof setImmediate === 'function') {
    timerAPI.setImmediate = setImmediate;
  }
  if (typeof clearImmediate === 'function') {
    timerAPI.clearImmediate = clearImmediate;
  }
  // ...

This function depends on setImmediate to work properly in order to work, I think this could probably use the polyfill declared below in the same file.

function waitForDomChange({
  container = getDocument(),
  timeout = getConfig().asyncUtilTimeout,
  mutationObserverOptions = {
    subtree: true,
    childList: true,
    attributes: true,
    characterData: true,
  },
} = {}) {
  if (!hasWarned) {
    hasWarned = true
    console.warn(
      `\`waitForDomChange\` has been deprecated. Use \`waitFor\` instead: https://testing-library.com/docs/dom-testing-library/api-async#waitfor.`,
    )
  }
  return new Promise((resolve, reject) => {
    const timer = setTimeout(onTimeout, timeout)
    const {MutationObserver} = getWindowFromNode(container)
    const observer = new MutationObserver(onMutation)
    runWithRealTimers(() =>
      observer.observe(container, mutationObserverOptions),
    )

    function onDone(error, result) {
      clearTimeout(timer)
      setImmediate(() => observer.disconnect())
      if (error) {
        reject(error)
      } else {
        resolve(result)
      }
    }

    function onMutation(mutationsList) {
      onDone(null, mutationsList)
    }

    function onTimeout() {
      onDone(new Error('Timed out in waitForDomChange.'), null)
    }
  })
}

@renatoalencar is it possible to switch the setImmediate in favour of setTimeout(()=> observer.disconnect(), 0) in waitForDomChange implementation ?

I'm currently trying to see if there's any way to break this, and actually works just fine. Let me do some more tests and see.

@eps1lon
Copy link
Member

eps1lon commented Mar 25, 2021

It would be awesome if somebody could verify if this issue is resolved when using https://pkg.csb.dev/testing-library/dom-testing-library/commit/2781ebca/@testing-library/dom e.g. by adding

"resolutions": {
  "**/@testing-library/dom": "https://pkg.csb.dev/testing-library/dom-testing-library/commit/2781ebca/@testing-library/dom"
}

to their package.json. This only works with yarn. Don't know how you could do it with npm.

@eps1lon eps1lon linked a pull request Mar 25, 2021 that will close this issue
4 tasks
@kentcdodds
Copy link
Member

@all-contributors please add @SimenB for bugs

@allcontributors
Copy link
Contributor

@kentcdodds

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

@SimenB
Copy link
Contributor Author

SimenB commented Mar 25, 2021

Thanks for all your help here, folks! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working help wanted Extra attention is needed needs discussion We need to discuss this to come up with a good solution
Projects
None yet
7 participants