RealOrangeOne / react-native-mock

A fully mocked and test-friendly version of react native (maintainers wanted)
MIT License
570 stars 153 forks source link

Need a way to reset _animationFrame count #133

Closed jasonfma closed 7 years ago

jasonfma commented 7 years ago

We came across this issue doing snapshot testing with Animated components and enzyme-to-json. There is a property that gets generated called _animationFrame that keeps incrementing throughout the entire test run.

This makes our snapshots fragile because if we add a test or change the order of the tests, the snapshots now have a different _animationFrame property.

I dug into the code and it looks like react-native-mock is getting that value from requestAnimationFrame which is from a library called raf. I'm not sure what the right solution here is but I will post our workaround below.

example test breakage:

AssertionError: expected value to match snapshot default state render
      + expected - actual

                         "_animation": TimingAnimation {
                           "__active": true,
                           "__isInteraction": true,
                           "__onEnd": [Function anonymous],
      -                    "_animationFrame": 2,
      +                    "_animationFrame": 7,
                           "_delay": 0,
                           "_duration": 200,
                           "_easing": [Function anonymous],
                           "_fromValue": 0,
                               "_animation": TimingAnimation {
                                 "__active": true,
                                 "__isInteraction": true,
                                 "__onEnd": [Function anonymous],
      -                          "_animationFrame": 2,
      +                          "_animationFrame": 7,
                                 "_delay": 0,
                                 "_duration": 200,
                                 "_easing": [Function anonymous],
                                 "_fromValue": 0,
jasonfma commented 7 years ago

Our workaround right now is to copy the source from the raf npm module and add a reset call that clears it's queue and resets the counter. Then we call it before every test.

/* eslint-disable */
// taken from https://github.com/chrisdickinson/raf version 3.3.2
var now = require('performance-now')
  , root = typeof window === 'undefined' ? global : window
  , vendors = ['moz', 'webkit']
  , suffix = 'AnimationFrame'
  , raf = root['request' + suffix]
  , caf = root['cancel' + suffix] || root['cancelRequest' + suffix]

for(var i = 0; !raf && i < vendors.length; i++) {
  raf = root[vendors[i] + 'Request' + suffix]
  caf = root[vendors[i] + 'Cancel' + suffix]
    || root[vendors[i] + 'CancelRequest' + suffix]
}

// Some versions of FF have rAF but not cAF
if(!raf || !caf) {
  var last = 0
    , id = 0
    , queue = []
    , frameDuration = 1000 / 60

  raf = function(callback) {
    if(queue.length === 0) {
      var _now = now()
        , next = Math.max(0, frameDuration - (_now - last))
      last = next + _now
      setTimeout(function() {
        var cp = queue.slice(0)
        // Clear queue here to prevent
        // callbacks from appending listeners
        // to the current frame's queue
        queue.length = 0
        for(var i = 0; i < cp.length; i++) {
          if(!cp[i].cancelled) {
            try{
              cp[i].callback(last)
            } catch(e) {
              setTimeout(function() { throw e }, 0)
            }
          }
        }
      }, Math.round(next))
    }
    queue.push({
      handle: ++id,
      callback: callback,
      cancelled: false
    })
    return id;
  }

  caf = function(handle) {
    for(var i = 0; i < queue.length; i++) {
      if(queue[i].handle === handle) {
        queue[i].cancelled = true
      }
    }
  }

  resetQueue = function() {     <----------------------------- custom reset function
    queue = [];
    id = 0;
    last = 0;
  }
}

module.exports = function(fn) {
  // Wrap in a new function to prevent
  // `cancel` potentially being assigned
  // to the native rAF function
  return raf.call(root, fn)
}
module.exports.cancel = function() {
  caf.apply(root, arguments)
}
module.exports.reset = function() {
  resetQueue.apply(root, arguments)
}
module.exports.polyfill = function() {
  root.requestAnimationFrame = raf
  root.cancelAnimationFrame = caf
}
/* eslint-enable */

Inside our init.js for mocha tests:

...
// this mock raf needs to happen before react-native-mock is required
const raf = require('./testHelpers/mockRaf');
mockery.enable({
  warnOnUnregistered: false,
});
mockery.registerMock('raf', raf);

require('react-native-mock/mock');
...

beforeEach(() => {
  raf.reset();
});

...
RealOrangeOne commented 7 years ago

This is a strange and unfortunate issue, but i'm not sure of the correct solution, or if it's anything to do with react-native-mock. This looks like a great patch, but it's probably better submitted to raf as a hidden API.