Testing transitions in v4?

341 views
Skip to first unread message

mfitzpa...@gmail.com

unread,
Aug 30, 2016, 1:50:36 AM8/30/16
to d3-js
Hello,

I'm working on migrating some d3 visualizations from v3 to v4, as well as the unit tests that go along with them. However, it appears that due to the timer-related changes made to transitions in v4 (https://github.com/d3/d3/blob/master/CHANGES.md#transitions-d3-transition), I haven't been able to find a good replacement for how transition unit tests were handled in v3.

In v3, one could simply mock and advance the value of Date.now to skip ahead in the transition and verify that the transition had moved all of the elements to their expected positions. See this fiddle for an example:


In the above fiddle, a set of <p> tags are set up to be transitioned over 15 seconds, and the 'for' loop at the bottom of the code advances through a few time intervals, verifying the font-size of the <p> tags at each iteration. You can imagine this strategy being used for a more complex visualization unit test, where certain elements are expected to be positioned at specific locations based on test input.

Now, if I convert the above fiddle to v4, here is the result:


Unlike in v3, advancing the time (via performance.now, as v4 uses performance.now internally instead of Date.now) does not actually advance the transition, so throughout the entire test, the value of the font-size is 0px, causing all of the assertions to fail. Of course I could just run the transition normally, but that would cause a simple test case to run for however long the duration of the transition is, instead of the instantaneous check we could do in v3.

The root cause of this appears to be that d3.timerFlush uses a cached value of 'clockNow' (https://github.com/d3/d3-timer/blob/master/src/timer.js#L61), instead of re-calculating 'clockNow' when d3.timerFlush is called, thus leading to an incorrect value of 'clockNow' if performance.now has been explicitly advanced to a different time.

Has anyone run into this issue, or have a possible workaround? One workaround is to search the DOM for nodes with transitions, look into the private __transition__ property, and advance each node's transition using its 'tween' property (see https://bl.ocks.org/veltman/23460413ea085c024bf8), but this seems like a lot to do, compared to the simplicity of v3.

I think the real solution here would be to invalidate the 'clockNow' cache in d3.timerFlush by calling "clearNow();" as the first line of the function (https://github.com/d3/d3-timer/blob/master/src/timer.js#L57), such that the next line's now() call would update value of 'clockNow' to the correct time (https://github.com/d3/d3-timer/blob/master/src/timer.js#L14). Does that sound like a reasonable solution for a pull request?

Thanks!

Mike Bostock

unread,
Aug 30, 2016, 10:23:24 AM8/30/16
to d3...@googlegroups.com
Option 1 is to use an asynchronous unit testing framework, such as tape. This is how d3-transition’s tests are implemented (in conjunction with JSDOM).

Option 2 is to redefine both performance.now and requestAnimationFrame to control how the timers are invoked and the apparent time. See the d3-timer implementation for details.

I would not be in favor of exposing new public APIs when reasonable alternate means are available, especially just for testing.

mfitzpa...@gmail.com

unread,
Sep 1, 2016, 6:50:11 AM9/1/16
to d3-js, mi...@ocks.org
Thanks for the quick response Mike!

I had actually explored both of those options earlier, but neither seemed ideal because:

1.) By asynchronous testing, I'm assuming this involves letting the transition run in real time. Unfortunately this wouldn't work if there are a large number of these tests (or long transition durations), as the test cases would take a very long time to run.

2.) I think Option 2 would be ideal, except for one issue.

performance.now can be successfully redefined within a test, because the 'clock' variable (https://github.com/d3/d3-timer/blob/master/src/timer.js#L10) just points to 'performance'.

However, requestAnimationFrame cannot be redefined from within a test, because the 'setFrame' variable (https://github.com/d3/d3-timer/blob/master/src/timer.js#L11) points directly to 'requestAnimationFrame'. Therefore, if I do the following at the beginning of a unit test:

window.requestAnimationFrame = function(callback) { /** My custom logic. */ };

This will have no effect on the 'setFrame' variable - it's too late, the original version of 'requestAnimationFrame' has already been used in the assignment of 'setFrame'.

One possible workaround for this would be to override 'requestAnimationFrame' before d3 is loaded, but that doesn't seem ideal, as that would mean that all unit tests would be affected, and we wouldn't be able to selectively apply the requestAnimationFrame override for only the relevant tests.

A small internal change which I believe could fix this, would be to just have a private function (not a public API) in d3-timer which returns requestAnimationFrame for internal use, instead of storing it on initialization:

function getSetFrame() {
  return typeof requestAnimationFrame === "function" ? requestAnimationFrame : function(f) { setTimeout(f, 17); };
}

Let me know your thoughts on this, or if there is something I'm missing here.

Thanks!

Mike Bostock

unread,
Sep 1, 2016, 1:24:57 PM9/1/16
to mfitzpa...@gmail.com, d3-js
If you don’t want to set the global requestAnimationFrame within your test runner, you can create a custom build of D3 for your tests that allows you to override the requestAnimationFrame locally. I don’t think we need new public API or runtime changes just to support this sort of testing.
Reply all
Reply to author
Forward
0 new messages