Jump to content
Search Community

MonkeyPatching Tweens/Timelines for automated testing

UncommonDave test
Moderator Tag

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts

We're currently using TweenMax and TimelineMax extensively in our app, and we're also running a suite of unit tests against the interface via mocha and we've discovered that one of the major factors pushing our automated test times up is that GSAP appears to still be doing a lot of work, even when we monkeyPatch the tween/timeline durations down to 0.

 

I haven't cracked open the GSAP lib to explore the inner workings yet, but my first thought was to just patch to(), from(), and fromTo() with nearly empty functions that will handle the callbacks. Although I still need to move the animated elements to their ending position, so maybe I could clone the .set() method and extend it to execute callbacks? But I have no idea if that's opening up a can of worms...

 

So I was curious if this is something the GSAP team has already addressed in the past... maybe you guys already built out a solution or have some branch somewhere that handles all the potential animation types and all the various callbacks for both tweens and timelines, but has been optimized for an automated testing environment?

Link to comment
Share on other sites

Well, a set() is exactly the same as a to() with a zero duration, so that won't help much :)

 

It might seem like it'd be simple to just immediately set the properties to their end values, but an animation engine is a very different beast than something like jQuery.css(). Keep in mind that:

  1. Even a zero-duration tween (set()) must record start/end values internally so that it can render at its starting state, ending state, or anywhere inbetween. Example: drop a set() onto a timeline and the playhead could be placed before, on, or after that spot and things must render properly.
  2. GSAP is highly optimized for runtime performance which can sometimes mean a bit of a tradeoff on the initialization code. Example: transforms are parsed and the components (x, y, scaleX, scaleY, rotation, skew, etc.) are separated out and stored independently in a cached mechanism which makes it very fast to work with on every tick. 
  3. Overwriting is pretty critical in an animation engine, so GSAP must store some information about each tween and the properties it's controlling so that when another tween on that object starts, it can find that information EXTREMELY quickly and kill whatever aspects are necessary. Like if you're halfway through an "x" tween when another one gets fired off.

There's a lot of optimized logic in place that, while it might look costly when you're only doing mocha tests with zero-duration tweens, actually make it possible for animations to run with butter smoothness at runtime. I wouldn't really recommend monkey-patching GSAP with mostly-empty functions like you're describing because it kinda defeats the purpose of the testing in some ways. In other words, you wouldn't actually be testing the real stuff - you'd be testing your hacked versions of the methods. See what I mean?

 

I think it could get pretty messy too because you'd have to make a lot of very specific modifications to accommodate all the various values that plugins can handle. It's easy enough to just set "top" and "left" or "opacity" CSS properties in a monkey-patched set(), but how would you handle drawSVG, morphSVG, autoAlpha, svgOrigin, special transforms like x/y/rotation/scale, or the various browser inconsistencies that are handled automagically under the hood for transform-origin and a bunch of other stuff? I worry that you'd either miss stuff, or once you tackle it all perfectly, you'd end up with something that takes almost as long to process in your tests :)

 

I wish I had an easier answer for you. Maybe just get a faster computer to make the tests run quicker ;)

  • Like 2
Link to comment
Share on other sites

Thanks for the response, although most of your stated concerns aren't things that provide any real value to us within the context of automated tests.

 

Quote

I wouldn't really recommend monkey-patching GSAP with mostly-empty functions like you're describing because it kinda defeats the purpose of the testing in some ways.

 

We're not actually interested in testing GSAP itself, because it's already a solid product and I assume you guys are doing your part to keep it that way. We only want to test our own code, and for testing purposes the only things we really care about are the starting position, the ending position, and executing the callbacks in the correct order. So if there isn't any simple or native way of bypassing those internal optimizations to maximize for speed, then leaving the functions intact is not very useful for us.

 

Quote

how would you handle drawSVG, morphSVG, autoAlpha, svgOrigin, special transforms like x/y/rotation/scale

 

Well, ideally we'd just defer to GSAP's native handling for anything we couldn't appropriately patch. But the vast majority of our animations are not those more complex cases, and we stand to gain a lot by optimizing the very common .to()/.from() tweens. We will eventually have several thousand unit tests and we intend to make them capable of running in just a few seconds. It's an important goal for us that has ramifications on our overall development cycle.

 

Quote

or the various browser inconsistencies 

 

There are no browser inconsistencies, because there is no browser. We're using jsDom to create a virtual dom and we run our unit tests in that environment.

 

Quote

Maybe just get a faster computer to make the tests run quicker

 

It's not really just a matter of getting a faster computer because the use-case isn't just for myself as an individual to occasionally run a small set of tests on my local machine. We're going to end up with thousands of tests running hundreds of times per day on dozens of different machines, both locally and on AWS. To optimize this cycle as much as possible we want to monkeypatch GSAP so it does the absolute bare minimum for our testing process to work, but no more.

 

The real value of GSAP is that it helps create a superior user experience for real humans using real browsers. There is essentially no value when running for virtual users in a virtual dom, but there is a lot of additional value in giving GSAP the capacity to "get out of the way" in this case. So we're going to take a run at this, and I'll post updates/questions as we go.

Link to comment
Share on other sites

Hi,

 

Couldn't help to pinch in with a simple suggestion, and perhaps I'm overlooking the real issue in question. If I am please correct me and excuse me for not understanding the conversation.

 

Is the problem that you need to run the tweens in order to check for callbacks and the elements' position?. What I think I understand for your posts is that, your test suite is taking far too long by waiting for a, let's say 5 seconds tween to be completed to check that everything actually happened. If that's the case why don't you use either seek(), or progress(), or time() to advance your GSAP instances to the end and check the values and callbacks. All those methods suppress callbacks by default, but you can pass a second parameter to trigger the callbacks between the current and final positions. You can do that at runtime after defining your instances or even at a specific moment or event of your code:

 

var t = TweenLite.to(e, 1, {x:100, onStart:startCallback, onComplete:completeCallback});

// then move the playhead to the end triggering all the callbacks
t.progress(1, false);

// using time
t.time(t.duration(), false);

// using seek
t.seek(t.duration(), false);

 

Again, sorry if I'm misunderstanding the problem, if so... sorry.

 

Happy Tweening!!

Link to comment
Share on other sites

Thanks for chiming in, Rodrigo. If I understood @UncommonDave correctly, he wasn't saying that the problem was the duration of the tweens. He's not waiting for things to finish normally - he's literally making all of their durations 0 (just for testing), but there's still a lot of processing time due to all of what GSAP must do (even with durations of 0) since there are so many tests that must be run. 

 

Is that correct, @UncommonDave?

  • Like 2
Link to comment
Share on other sites

Yep, as I said I'm not convinced that my answer is what the issue is about. just never did a lot of unit test or end-to-end on gsap instances and the end values of the properties they affect, mostly checked that the instance was created by checking some specific properties. Also I haven't done a lot of testing on virtual DOM, mostly browser or Node, so I was attracted to the thread because of that. I've never used a lot of testing on the values because of my blind faith in GSAP, I just create the instances and I know that id something is not working is because I blew it at some point.

 

I posted that answer with the hope that the phrase:"Sometimes the simplest solution is the best solution" could become true in it :D

 

I'll keep waiting on @UncommonDave's posts to see how it develops.

 

Happy Tweening!!

  • Like 1
Link to comment
Share on other sites

@Rodrigo

 

So yea, as Jack was saying, we've already patched the duration args for all the tween and timeline instances to 0. So the next thing we're trying is to squeeze additional speed out by circumventing the internal optimizations that GSAP performs, even for 0 duration animations.

 

This GSAP overhead usually ends up manifesting in just a handful of ms (something between 10-30ms seems pretty common for us right now), but because we already have several hundred tests (and that number is growing quickly) that involve a part of the interface that does some sort of animating, the cumulative gain adds up. And we have an internal goal that involves making our suite run REALLY fast, so patching GSAP will be a big step toward that goal.

 

On another note.... I personally have only been exposed to test driven development for a couple of years now. But I've found a lot of motivation and inspiration for the pattern by watching YouTube lectures from Robert C. Martin (Uncle Bob). I encourage any serious engineer to listen to at least a couple of his talks... he's a pretty smart guy.

 

Here's a good one: https://www.youtube.com/watch?v=BSaAMQVq01E

 

  • Like 2
Link to comment
Share on other sites

  • 1 month later...

Thought I should post what we've ended up with so far. This has been running for a couple weeks and seems to be covering our needs for now, although I know there are still some holes especially if we needed to test any timelines with more sophisticated construction processes or out-of-sequence methods appended (using position offsets). I should also note that this decreased the overall running time of our front-end test suite by well over 30%, so this has been a pretty successful addition.

 

Timeline is still missing most of the handlers (most notably on my radar are the "progress" and "yoyo" methods) and we didn't fully patch the fromTo tween (just zero'd out the duration) but this is still an early V1 to get something in place.

 

function getPatchedTween() {
    let tween = ($el, dur, opts = {}) => {
        isDefinedExecute(opts.onStart);
        isDefinedExecute(opts.onComplete);
        return tween;
    };

    tween.delay = () => tween;
    tween.kill = () => tween;

    return tween;
};

function muteDuration(func) {
    return function() {
        if (arguments[1]) {
            arguments[1] = 0;
        }

        return func.apply(this, arguments);
    };
};

function isDefinedExecute(callback, context = null, args = []) {
    let isDefined = (val) => {
          return val !== undefined && val !== null;
        },
        isFunction = (obj) => {
          return !!(obj && obj.constructor && obj.call && obj.apply);
        };

    if (isDefined(callback) && isFunction(callback)) {
        return callback.apply(context, args);
    } else {
        return false;
    }
};

TweenMax.from = getPatchedTween();
TweenMax.to = getPatchedTween();
TweenMax.fromTo = muteDuration(TweenMax.fromTo);

TweenLite.from = getPatchedTween();
TweenLite.to = getPatchedTween();
TweenLite.fromTo = muteDuration(TweenLite.fromTo);

class TimelinePatched {
    constructor(options = {}) {
        this.callbacks = [];
        this.options = options;
        this.paused = this.options.paused || this.options.autoplay === false;

        this.from = this.callbackTween.bind(this);
        this.to = this.callbackTween.bind(this);
        this.add = this.callbackAdd.bind(this);
        this.fromTo = ($el, dur, opts, opts2, position) => this.callbackTween($el, dur, opts, position);
      
      	if (!this.paused) {
            setTimeout(this.play.bind(this));
        }
    }

    callbackTween($el, dur, opts, position) {
        return this.callbackAdd(() => getPatchedTween()($el, dur, opts), position);
    }

    callbackAdd(func, position) {
        this.callbacks.push({position, func: func});
        this.callbacks.sort((a, b) => {
            return a.position - b.position;
        });

        if (!this.paused) {
            func();
        }

        return this;
    }

    play() {
        this.paused = false;
        isDefinedExecute(this.options.onStart, this);
        
        for (let cb of this.callbacks) {
            cb.func();
        }

        isDefinedExecute(this.options.onComplete, this);
    }

    pause() {
        this.paused = true;
    }

    isActive() {
        return false;
    }

    reverse() {
        for (let i = this.callbacks.length - 1; i > -1; i--) {
            this.callbacks[i].func();
        }
        isDefinedExecute(this.options.onReverseComplete);
    }
}

TimelineMax = TimelineLite = TimelinePatched;

 

I should also add that I had to remove a little bit of embedded utility methods and other options that are very specific to our use of this. I'm hoping I didn't break something during translation.

  • Like 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...