-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
Introducing "it.yield", a convenient way to test promise code #9601
Conversation
expect(is3pThrottled(win)).to.be.false; | ||
incrementLoadingAds(win); | ||
expect(is3pThrottled(win)).to.be.true; | ||
clock.tick(999); | ||
yield; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is genius, but I don't fully understand how it works. Or this is a bit awkward.
Wouldn't it be better to always yield a promise instead of relying on the magic setInterval shortcircuiting in the trivial case?
If you return a promise from incrementLoadingAds
that would work for this test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When code runs to ayield
statement in a Generator, it stops and yield to other tasks/micro tasks in the event loop to run. It will resume once the Generator.next() is called.
Calling Generator.next()
in a setInterval makes sure the remaining work always get queued to the end of event loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super cool, just learned about generators and yield
. 😄
Could await
and Promises could achieve the same goal (but perhaps read more naturally)? E.g. in the test below:
let loadAds = incrementLoadingAds(win); // returns Promise
expect(is3pThrottled(win)).to.be.true;
await loadAds; // replaces invocation of resolver and `yield`
expect(is3pThrottled(win)).to.be.false;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
depending on the use cases. here we do have a handle of the promise, but in many real scenario, we don't.
also, await
is not ES6 feature.
yield
is actually nature to me. it means to yield
to other code (other tasks in the event loop) to run first.
test/_init_tests.js
Outdated
} | ||
return realIt(message, done => { | ||
const iterator = runnable(); | ||
const interval = setInterval(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome! Would fix tests with callback hell and can simplify many promise related tests!
One question I have: I know in our code we use setTimeout
to delay callback. are the callback in setTimeout
micro task as well?
Does the setInterval
work in the following scenario:
param = false
promise = new Promise(resolve => {resolver = resolve});
promise.then(() => {param = true})
doSomething = () => {setTimeout(resolver)}
doSomething();
yield
expect(param).to.be.true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setTimeout is not a microtask.
but your example should work. because yield
flushes the eventloop, including resolver
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm I tried
it('should work ', function* () {
let param = true;
let resolver;
const promise = new Promise(resolve => {resolver = resolve;});
promise.then(() => {
param = false;
});
const doSomething = () => {setTimeout(resolver);};
doSomething();
yield;
expect(param).to.be.false;
});
and it won't pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zhouyx I added your test case into my test set, it works.
test/_init_tests.js
Outdated
|| runnable.constructor.name !== 'GeneratorFunction') { | ||
return realIt(message, runnable); | ||
} | ||
return realIt(message, done => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a done
parameter prevents these tests from returning a promise as a final resolution value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to block this until I'm overruled by @dvoytenko or @cramforce. This is going to fail in a large majority of cases (a promise chain, not just a simple, single promise). In general, this works because setInterval
happens after promise resolution. But that's an internal operation and not defined by the browser.
From what I can tell, there's two motivations behind this PR:
- Callback hell
- Async resolution
This kind of solves callback hell. But it does nothing to guarantee async resolution happens properly (my main objection). We can use this, but we have to add a bit more code to support. @cramforce's suggestion is actually the optimal solution (so is @choumx's, because async
/await
can be handled by Generators), and we can use this to get there.
If we yield a promise instance, we can wait upon the promise's resolution to happen and send back the value (this is what co does). This handles callback hell (we no longer need nested then
blocks), and it solves async resolution (the yielded promise is guaranteed to resolve before the test code is run again). If the promise we yield is the promise the src code is waiting on, then we get perfection.
it('co like', function* () {
const promise = srcCode.method();
const result = yield promise;
expect(result).to.be(something);
});
test/_init_tests.js
Outdated
} | ||
return realIt(message, done => { | ||
const iterator = runnable(); | ||
const interval = setInterval(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setTimeout
every time the function yields would be more appropriate. This can end up causing the test code to run again before it's really intended.
@lannka I think this is really cool and I don't see much in terms of possibility of flakes, except for one case that I don't fully yet understand. Imagine, this example:
This should be guaranteed to be always stable with native promises. But there's a possibility that, if a polyfill implemented via event queue (or native implemented incorrectly via event queue) we'd get the following situation:
So it's possible that My second comment - it'd be cool if
|
@jridgewell no. This works because setInterval will append remaining work into the tail of the event queue. thinking it as a way to flush the event queue. Can you come up with a test case that will fail this? |
That's my point. This is an internal procedure, not defined by the spec. Any promise that waits on XHR, or itself uses @dvoytenko's code above is not defined even for the native Promise implementation. It could be result in |
Lets make it a standard to always yield to an actual promise. We can make a helper like |
I added tests. They run successfully under latest Chrome, Safari, FF. Saucelabs is somehow not working for me right now, so didn't check on older browsers. PTAL @cramforce not quite get what you mean by the |
I think he meant you can create a Promise that resolves after a macro-task:
You can avoid |
@choumx how would you refactor the above callback hell using your |
|
@choumx this seems like the same :-) just a more manual way. Talked with @jridgewell offline. His concern is about the potential undocumented race condition between promise.resolve and setTimeout/setInterval. But in fact, without introducing this helper, the problem exists if we ever want to use setTimeout to do a later check. verify(false);
clock.tick(100); // this triggers a timer and then a promise chain
setTimeout(() => {
verify(true);
done();
}) |
This would be fine: yield macroTask();
verify(false);
clock.tick(1);
yield macroTask();
verify(true); That almost solves my objections because we're awaiting a promise. It's still a race condition (and so async resolution is not guaranteed) but we're at least on the right track. The best solution is still going to be getting access to the source code's promise. |
I don't want either of those (the current or the proposed). Using |
I'd love to see a live example of those flakes. If I understand correctly, your idea is to have both promise + setTimeout to make sure remaining work is append at the end of the loop, no matter how browser treats promise and setTimeout. If so, I can add additional Promise.resolve into my setInterval to keep the neat syntax. |
@jridgewell as we discussed offline previously, that is totally a different story. That test was flaky because the promise chain has a real XHR over the network in it. Timeout 5ms obviously isn't enough for some of the times. This test should be categorized as an integration test, where a "polling", or Again, I also don't like the fact that we fixed the test by exposing an "test only" private field. Ideally, we'd keep those implementation details away from an integration test. This introduced helper is not planed to be used in such case. If we really want to, we can change it to a unit test, where we mock out the XHR and resolve it explicitly:
That way, we separated the concerns, which is the key of a unit test. |
It's using a stubbed fetch. It's flakey because the race between |
To be very specific, the problem is this:
A Job is a micro-task, and To demonstrate in code, take my PJs Promise implementation (which AMP uses to polyfill): // https://github.com/jridgewell/PJs/blob/b265fded45dbd06ae8a64ee0ccde29b1e3b8f760/promise.js#L423-L430
function flush() {
// notice this rechecks `queue.length` every time,
// meaning anything enqueued keeps it going
for (var i = 0; i < queue.length; i++) {
var fn = queue[i];
queue[i] = null;
fn(); // `fn` is a job
}
} I use exhaustive evaluation because it's easy, and it makes feel like promises are just as fast as nodebacks. But I could have easily done: function flush() {
var current = queue;
// queue has been redefined, so anything enqueued is appended to this new array,
// and not the one I'm currently iterating.
queue = [];
for (var i = 0; i < current.length; i++) {
var fn = queue[i];
fn();
}
} In this case, the queue is not exhausted and must wait till the next tick before anything the next promise in the chain is resolved. It's up to the implementation to decide on what "initiated sometime after completion" means. It's not even defined that the current job queue has to be exhausted, I could pause mid queue: function flush() {
// I'll arbitrarily stop at 1000 jobs,
// because we're spending too much time here.
// Maybe I'll implement a `performance.now` comparison to keep this under 16ms?
// Totally up to me.
for (var i = 0; i < 1000; i++) {
var fn = queue[i];
fn();
}
queue.splice(0, 1000);
} With this, my problem with Async resolution is undefined. And this means the use of If we instead yield the promise we want to wait on, we solve everything this PR wants.
|
test/_init_tests.js
Outdated
@@ -214,14 +214,35 @@ it = function(message, runnable) { // eslint-disable-line no-native-reassign, no | |||
return realIt(message, runnable); | |||
} | |||
return realIt(message, done => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can refactor this so Promises take care of everything:
// Adapted from Babel's asyncToGenerator
return realIt(message, () => {
return new Promise((resolve, reject) => {
const gen = runnable();
function step(key, arg) {
let info;
try {
info = gen[key](arg);
} catch (error) {
reject(error);
return;
}
const { value, done } = info;
if (done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _next(value) {
step("next", value);
}
function _throw(err) {
step("throw", err);
}
_next();
});
});
Differences include:
- Promises handle async (through assimilation)
- Yielded promises can reject, going into the generators throw path
- We avoid using
done
Forbid node-style done callback in mocha it tests #8966 - Less closures
test/_init_tests.js
Outdated
// With this, we can do: `yield 50`, which blocks the test for 50ms | ||
// We should rarely need this in unit test, use with caution, as it | ||
// usually brings test flackiness. | ||
const timeout = (typeof state.value === 'number') ? state.value : 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are we running a timeout if there's no timeout?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, i should probably document that.
test/_init_tests.js
Outdated
return fn(message, runnable); | ||
} | ||
return fn(message, done => { | ||
const runner = (iterator, result) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can refactor this so Promises take care of everything:
// Adapted from Babel's asyncToGenerator
return realIt(message, () => {
return new Promise((resolve, reject) => {
const gen = runnable();
function step(key, arg) {
let info;
try {
info = gen[key](arg);
} catch (error) {
reject(error);
return;
}
const { value, done } = info;
if (done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _next(value) {
step("next", value);
}
function _throw(err) {
step("throw", err);
}
_next();
});
});
Differences include:
- Promises handle async (through assimilation)
- Yielded promises can reject, going into the generators throw path
- We avoid using
done
Forbid node-style done callback in mocha it tests #8966 - Less closures
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just want to understand the benefit better:
Promises handle async (through assimilation)
Sorry, what does that mean? :-)
Yielded promises can reject, going into the generators throw path
This is not a problem. I should have added anyway.
We avoid using done #8966
I handled this by using a try-catch block. But in your impl, there's also a try-catch, is that there for the same reason?
Less closures
If I count correctly, all the 3 functions are closures step
, _next
, _throw
. Not sure how it is less than the done
impl.
Also, I still think there is value to support setTimeout. We will add boilerplate code to turn a setTimeout to promise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, what does that mean? :-)
Promise assimilation is:
Promise.resolve(1).then(value => assert(value, 1));
Promise.resolve(somePromiseThatResolvesTo1).then(value => assert(value, 1));
Promises are designed to be interoperable through assimilation (they do almost exactly what you do with the value.then === function
check), but in a overspecified-spec way.
I handled this by using a try-catch block
Done is actually not handled by the try-catch. If the generator threw, that's handled by it. But the problem is the final result value (when iterator.done = true
) isn't waited on (it could be a promise).
But in your impl, there's also a try-catch, is that there for the same reason?
My try-catch and yours are equivalent, but neither handles the done
callback (see above).
not sure how it is less than the done impl.
Multiple closures are used when the yielded value is a promise (the then blocks). This one's really a nit-pick.
Also, I still think there is value to support setTimeout
I split off the changes on concurrent-load.js and test-concurrent-load-js, leaving only A recap of the discussions so far: We agreed:
Still debating:
My arguments are:
I suggest we get this PR in and monitor the flakiness of the tests introduced from this PR on Travis. @everyone thoughts? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whether make the impl a promise based or done based. The decision will be highly affected by the decision of the previous item.
We can support the setTimeout
API and remove done
. It's really about duplication.
Microtask seems to be a widely accepted concept. I also created tests and they all works on my local browsers
See #9601 (comment).
Exposing internal promise state only for test violates test principals. The more we couple the implementation code with test code
I wonder if we can just accumulate all Promise instances. Something like:
it(() => {
collectPromises();
src.somethingThatsAsync();
const allDone = Promises.all(stopCollectingPromises());
yield allDone;
// async stuff done
});
testing/yield.js
Outdated
* Check test-yield.js for how-to. | ||
*/ | ||
export function installYieldIt(realIt) { | ||
it = enableYield.bind(null, realIt); // eslint-disable-line no-native-reassign, no-undef |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
global.it = ...
. we could do it.yields
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can do either:
it.yields('message', function*() {
});
or
it('message', function*() {
});
I'm in favor of the latter because it makes the other syntax like it.only
and it.skip
unchanged.
testing/yield.js
Outdated
return done(e); | ||
} | ||
if (state.done) { | ||
return done(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to wait for the state's value
to complete.
if (state.done) {
Promise.resolve(state.value).then(() => done(), done);
return;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch
testing/yield.js
Outdated
const _runner = runner.bind(null, iterator); | ||
if (isPromise(state.value)) { | ||
// With this, we can do: `const result = yield promise;` | ||
state.value.then(_runner).catch(done); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This catch
clause is incorrect. If a Promise is yielded, and it rejects, it should be passed into Generator#throw
. Also, it can be in the same then block, not after in a catch block:
state.value.then(_runner, (error) => {
// this will basically duplicate everything in runner
try {
result = iterator.throw(error);
} catch (e) {
return done(e);
}
//...
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I still don't quite understand this part. Why is it necessary to throw the error to iterator, instead of just finish the test case immediately?
I think it's harmless to have an iterator hang there without iterating to the end, no? Or is it for better error reporting?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not for error reporting, but so we can take full advantage of the generator's functionality. Imagine:
it('regular code', function() {
return somePromiseThatRejects.then(() => {
throw new Error('UNREACHABLE');
}, err => {
expect(err.message).to.contain('something');
});
});
As it is now, this cannot be written into our yield style, because the promise would reject and kill the test. Instead, we want something like:
it('generator yield', function*() {
try {
yield somePromiseThatRejects;
throw new Error('UNREACHABLE');
} catch (e) {
expect(e.message).to.contain('something');
}
});
That's throwing the error into the iterator, allowing us to test all our promise tests as sync generators.
testing/yield.js
Outdated
// We should rarely need this in unit test, use with caution, as it | ||
// usually brings test flakiness. | ||
const timeout = (typeof state.value === 'number') ? state.value : 0; | ||
setTimeout(_runner, timeout); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still strongly against this.
@jridgewell So it's not a "undefined behavior", we should be able to use it, at least on modern browsers and well implemented polyfills. Nevertheless, I will go with your previous suggestion, since it's more explicit and less magic behind the scene.
Any other concerns? |
I mentioned that in #9601 (comment). The undefined part is not that it's a microtask, but that running the microtasks (especially newly queued ones) isn't guaranteed in this tick.
Awesome. I'm on board with this, as long as it's explicit and ugly in test code. The
Nope, I look forward to using |
@jridgewell I've updated the code. Please take a look. Didn't get it to return a promise because:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One issue, then LGTM.
testing/yield.js
Outdated
} catch (e) { | ||
// catch any assertion errors and pass to `done` | ||
// otherwise the messages are swallowed | ||
return done(e); | ||
} | ||
if (state.done) { | ||
return done(); | ||
Promise.resolve(state.value).then(() => done(), _throw); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect, the iterator is done and pushing the error into it won't be handled. This should be then(() => done(), done)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point. fixed.
PS. Why didn't we just use it('async', async () => {
try {
await somePromise;
} catch (e) {
// promise rejected...
}
const value = await anythingEvenIfItsNotAPromise;
assert(value);
}); |
@jridgewell well, essentially we wrote a polyfill of async/await using generator to be able to run in our current project setup. |
But it should just work because we compile our test code with Babel. |
@jridgewell it does not work right away. maybe doable with preset-es2017. But I was hesitating: is it good to have different babel setting in production vs test? Or in other words, are we ready to moving to es2017? |
…ject#9601) * Introduce yield. * Address comments. * fix done state error handling
* Bidtellect amp implementation… (ampproject#9518) * Bidtellect amp implementation… * removing renderStartImplemented… * spaces? * adding example * Fix test-amp-ad-network-doubleclick-impl (ampproject#9645) * Optimizations to speed up Travis PR builds (ampproject#9626) Travis builds have become slow during busy periods of the day due to various reasons. This PR does the following: Reorganizes build tasks across fewer build shards Reduces the number of VMs used by PR and master builds to 2 No longer does a full gulp dist for unit tests (Possible to do after ampproject#9404) Reduces the total CPU time used for PR builds to < 20 mins Fixes ampproject#9500 Fixes ampproject#9387 * Animations: full on-action API (ampproject#9641) * Animations: full on-action API * review fixes * Fix Validator tests on Travis, which now uses Node v4 (ampproject#9654) * Amp-imgur : Implement imgur embed (ampproject#9405) (ampproject#9434) * Amp-imgur : Implement imgur embed (ampproject#9405) * amp-imgur test code modified * amp-imgur validation code changed * fix amp-imgur lint check * Remove unused Layour variables * Test code modified * Ingegration amp-imgur extension * [amp-imgur] add resize logic * Remove 3p iframes and add resize message listener * Fix some code with reviews * change some syntax * change validation rules * add find event source * [amp-imgur] remove unnecessary code and add object check * remove spec_name from validation code * Remove unnecessary code and add owners file * Change required tag name to in validation file * data-imgurid -> data-imgur-id * Add whitelist for amp-imgur * remove unused AmpImgur import in test file * remove unused preconnect callback * Update amp-cors-requests.md (ampproject#9636) Added updates and corrections per Dima's feedback. * Expose login url building to amp access service (ampproject#9300) * Animations: support style keyframes (ampproject#9647) * Animations: support style keyframes * docs * review fixes * lints * Animations: subtargets format (ampproject#9655) * Animations: subtargets format * fix docs * Fix extern for integration test (ampproject#9657) * Remove whitelisted link for PR 9434 (ampproject#9656) * Add callouts for CORS + cleanup (ampproject#9591) * Add callouts for CORS + cleanup * Update amp-access.md * Doubleclick Fast Fetch: Send default safeframe version on ad request (ampproject#9613) * Send default safeframe version on ad request * fix test failure * amp-ima-video: Fixes some undefined variables in compiled extension (ampproject#9617) * Add experimental input-debounced to the actions and event doc (ampproject#9650) * Copy and rename compiled script directly for deprecated version (ampproject#9587) * Remove A4A Dependency on ads/_config.js (ampproject#9462) * Added doubleclick specific config * Modified other networks * Update MockA4AImpl * Remove unneeded config lines * Add cid, remove renderStartImplemented * Fix lint complaint * Changed from mandatory config to overrrideable method * Addressed feedback, modified getAdCid * Addressed additional feedback * Responded to feedback * Removed unnneeded guard against null value * Changed signature of function * Fixed lint issues * addressed comments * Addressed comments, fixed broken test * Change signature / update tests * Fix wrong contents and white spaces in amp-imgur.md (ampproject#9658) * Performance: separate ini-load from first visible ini-load (ampproject#9665) * amp-bind: Fix more integration tests (ampproject#9598) * more bind test fixes * include object.assign polyfill * move polyfills to separate file * replace use of map() from bind-validator with ownProperty() * fix types and test * Stop using IOS elastic scroll when slidescroll takes over animation (Only for the NON SNAP-POINTS flow) (ampproject#9668) * removing overflow touch on no-scroll for ios * Stop using IOS elastic scroll when slidescroll takes over animation (Only for the NON SNAP-POINTS flow) * make sure css is compiled before entry points (ampproject#9643) * update npm package version (ampproject#9671) * A4A: expose visibilityCsi (ampproject#9667) * A4A: expose visibilityCsi * tests * amp-bind: Fix embedding in FIF (ampproject#9541) * move element service changes from ampproject#9447 * store element service ids in extension struct * add unit test, fix other tests * Fix null value binding (ampproject#9674) * Add amp-form's submit action to the docs (ampproject#9675) * Write cookie for ad-cid. (ampproject#9594) * Write cookie for ad-cid. * fix tests * amp-access-laterpay fixes (ampproject#9633) * Add support for LaterPay subscriptions * Fix canonical link on laterpay example * Tweak default styling * Add an example locale message for the header * Fix indentation of JsDoc (ampproject#9677) It should be indented at the same level as the method it's documenting. * Add example to amp iframe docs (ampproject#9660) * Change image to static link hosted on ampproject.org * Update amp-iframe doc w example + gen cleanup * Adds SFG experiment branches to doubleclick-a4a-config.js. (ampproject#9662) * Added experiment branches corresponding to exp=a4a:(5|6). * Removed test file. * Removed changes to yarn.lock + minor fixes. * Fixed adsense-a4a-config.js. * Fixed return statement. * Removed reference to style tag for opacity (ampproject#9634) As style tag for `opacity` was replaced with `amp-boilerplate (back in this [commit](ampproject@0a056ca), updated the text to reflect those changes. * De-flake amp-bind integration tests (ampproject#9683) * split bind fixtures into one file per extension * fix width/height binding bug, avoid race condition w/ dynamic containers * actually, just skip the tests affected by race condition for now * Remove timer calls to prevent future flakiness (ampproject#9478) * Remove timer calls to prevent future flakiness * Revert the line order change * Poll instead of using stub callback constructor * Add jsdoc and rename variables * amp-animation: polyfill partial keyframes from CSS (ampproject#9689) * Animations: whitelist offset distance and regroup condition types (ampproject#9688) * Use Docker containers in Travis (ampproject#9666) * clean up web-worker experiment (ampproject#9706) * Load examiner.js when #development=2 (ampproject#9680) * Load examiner.js when #development=1 * address comments * Use development=2 * Fix presubmit * Implementation of amp-ad-exit (ampproject#9390) * Implementation of the amp-ad-exit component for managing ad navigation (ampproject#8515). Changes from the I2I: - Only RANDOM, CLICK_X, and CLICK_Y variables can be used, alongside custom vars. - It doesn't work well with <a> tags - the tap action doesn't trigger on middle clicks. Recommendation is to use other elements. - ClickLocationFilter for border click protection is not yet implemented. It will be added in a future change. The component does some lightweight validation of the JSON config when it's built. This may be overkill. Tested: gulp test --files 'extensions/amp-ad-exit/0.1/test/*' gulp check-types * Update JSDoc for some functions. Fix lint issue (ignore unused parameters in an interface method). * sinon.spy -> sandbox.spy * Add amp-ad-exit to the list of allowed extensions in A4A pages and fix validation rules. Address PR: add experiment flag; use camelCase for config properties address some comments Instantiate a new Filter for each spec. * Instantiate a new Filter for each spec. * remove duplicate doctype tag in example * Make ActionEventDef public for type annotations. * Add filter factory. * fix event var * update type annotation for Filter.filter * address PR comments * Move filter config validation to the ctor. Remove Filter.buildCallback(). * camelCase in documentation examples * Introducing "it.yield", a convenient way to test promise code (ampproject#9601) * Introduce yield. * Address comments. * fix done state error handling * Document amp-access as a special target (ampproject#9568) `amp-access` is a special target that's not mentioned in this document. It's special because you can't give an arbitrary ID to it, i.e., you can't just change the id of the `amp-access` script tag to `amp-access2` and expect `on="tap:amp-access2.login"` to work. Given that the "actions" for `amp-access` is a bit dynamic depending on the structure of the `amp-access` JSON, instead of listing out the actions in a tabular form, a reference to the `amp-access` documentation is added. * Run presubmit tests soon after building (ampproject#9716) * Revert "Use Docker containers in Travis (ampproject#9666)" (ampproject#9717) This reverts commit 98ecb9b. It turns out that while using Docker containers instead of VMs on Travis reduces VM startup time, it has significantly increased build and test time, increasing the overall PR check round trip from < 20 mins to > 30 mins. Compare https://travis-ci.org/ampproject/amphtml/jobs/238464404 (Docker) against https://travis-ci.org/ampproject/amphtml/jobs/238462066 (VM). Fixes ampproject#9651 * Validator Rollup (ampproject#9719) * Add validator tests for amp-imgur. * Disallow amp-embed as child of amp-app-banner. * Minor cleanup, Make sure our set numbers are consistent in javascript. * amp-state: Allow both `src` and script child (ampproject#9721) * adapt code from ampproject#8897 * also_requires_attr should be in trigger * Remove experiment for input-debounced. Update docs. (ampproject#9724) * Validator Rollup (ampproject#9727) * Minor cleanup of amp-ad-exit * Disallow amp-ad/amp-embed as children of amp ad containers when data-multi-size is present. * Revision bump * Enabling dynamic queryparam addition to anchour links (ampproject#9684) * Enabling queryparam addition to anchour links * Code review changes * Additional cache urls (ampproject#9733) * Significantly speed up gulp build --css-only (ampproject#9726) Prior to this PR, gulp build --css-only would take ~ 1 minute to run, and end up building a bunch of js files in addition to compiling css. After this PR, gulp build --css-only takes ~ 0.5 seconds to run (a 100x speed up), and the only output files in build/ after running it are .css, and .css.js files. Resulting speeds on my workstation: gulp build --css-only: ~500ms gulp build: ~1m 45s gulp dist --fortesting: ~3m 30s Fixes ampproject#9640 * add amp-analytics Facebook Pixel support (ampproject#9449) * add amp-analytics Facebook Pixel support * add amp-analytics Facebook Pixel support * fix test on extensions/amp-analytics/0.1/test/vendor-requests.json * add Facebook Pixel standard events https://developers.facebook.com/docs/ads-for-websites/pixel-events/v2.9#events * Update vendors.js fix conflicts * Update vendors.js fix conflicts * Update vendor-requests.json fix conflicts * Update vendor-requests.json fix conflicts * Update vendor-requests.json * Update analytics.amp.html * Update analytics-vendors.amp.html * Update vendors.js * Update analytics-vendors.amp.html * fix lint error * update * facebook pixel * Update vendor-requests.json * Update analytics.amp.html * fix lint error * fix lint error * fix alphabetical order * Update yarn.lock * AMP-ad supports Seznam Imedia ad network (ampproject#8893) * AMP-ad supports Seznam Imedia ad network * Fixed requested changes * Fixed requested changes * Render API implemented * Improved placing ads * Fixed requested changes * Render API fixed context * Renamed IM.cz to Imedia * Described JSON parameters
We're often facing tests like this:
Example A
Hack A: We are smart, so we figured we can:
Example B
Hack B:
Hack B+: This can easily go out of control:
Introducing
it.yield
, no more callback hell! :/cc @cramforce @dvoytenko
(One caveat: Safari 9 does not support Generator)