// Here's how waitForNotification works: // // - myTestFunction0() // - waitForNotification(myTestFunction1) // - requestAnimationFrame() // - Modify DOM in a way that should trigger an IntersectionObserver callback. // - BeginFrame // - requestAnimationFrame handler runs // - Second requestAnimationFrame() // - Style, layout, paint // - IntersectionObserver generates new notifications // - Posts a task to deliver notifications // - Task to deliver IntersectionObserver notifications runs // - IntersectionObserver callbacks run // - Second requestAnimationFrameHandler runs // - step_timeout() // - step_timeout handler runs // - myTestFunction1() // - [optional] waitForNotification(myTestFunction2) // - requestAnimationFrame() // - Verify newly-arrived IntersectionObserver notifications // - [optional] Modify DOM to trigger new notifications // // Ideally, it should be sufficient to use requestAnimationFrame followed // by two step_timeouts, with the first step_timeout firing in between the // requestAnimationFrame handler and the task to deliver notifications. // However, the precise timing of requestAnimationFrame, the generation of // a new display frame (when IntersectionObserver notifications are // generated), and the delivery of these events varies between engines, making // this tricky to test in a non-flaky way. // // In particular, in WebKit, requestAnimationFrame and the generation of // a display frame are two separate tasks, so a step_timeout called within // requestAnimationFrame can fire before a display frame is generated. // // In Gecko, on the other hand, requestAnimationFrame and the generation of // a display frame are a single task, and IntersectionObserver notifications // are generated during this task. However, the task posted to deliver these // notifications can fire after the following requestAnimationFrame. // // This means that in general, by the time the second requestAnimationFrame // handler runs, we know that IntersectionObservations have been generated, // and that a task to deliver these notifications has been posted (though // possibly not yet delivered). Then, by the time the step_timeout() handler // runs, these notifications have been delivered. // // Since waitForNotification uses a double-rAF, it is now possible that // IntersectionObservers may have generated more notifications than what is // under test, but have not yet scheduled the new batch of notifications for // delivery. As a result, observer.takeRecords should NOT be used in tests: // // - myTestFunction0() // - waitForNotification(myTestFunction1) // - requestAnimationFrame() // - Modify DOM in a way that should trigger an IntersectionObserver callback. // - BeginFrame // - requestAnimationFrame handler runs // - Second requestAnimationFrame() // - Style, layout, paint // - IntersectionObserver generates a batch of notifications // - Posts a task to deliver notifications // - Task to deliver IntersectionObserver notifications runs // - IntersectionObserver callbacks run // - BeginFrame // - Second requestAnimationFrameHandler runs // - step_timeout() // - IntersectionObserver generates another batch of notifications // - Post task to deliver notifications // - step_timeout handler runs // - myTestFunction1() // - At this point, observer.takeRecords will get the second batch of // notifications. function waitForNotification(t, f) { return new Promise(resolve => { requestAnimationFrame(function() { requestAnimationFrame(function() { let callback = function() { resolve(); if (f) { f(); } }; if (t) { t.step_timeout(callback); } else { setTimeout(callback); } }); }); }); } // If you need to wait until the IntersectionObserver algorithm has a chance // to run, but don't need to wait for delivery of the notifications... function waitForFrame(t, f) { return new Promise(resolve => { requestAnimationFrame(function() { t.step_timeout(function() { resolve(); if (f) { f(); } }); }); }); } // The timing of when runTestCycle is called is important. It should be // called: // // - Before or during the window load event, or // - Inside of a prior runTestCycle callback, *before* any assert_* methods // are called. // // Following these rules will ensure that the test suite will not abort before // all test steps have run. // // If the 'delay' parameter to the IntersectionObserver constructor is used, // tests will need to add the same delay to their runTestCycle invocations, to // wait for notifications to be generated and delivered. function runTestCycle(f, description, delay) { async_test(function(t) { if (delay) { step_timeout(() => { waitForNotification(t, t.step_func_done(f)); }, delay); } else { waitForNotification(t, t.step_func_done(f)); } }, description); } // Root bounds for a root with an overflow clip as defined by: // http://wicg.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle function contentBounds(root) { var left = root.offsetLeft + root.clientLeft; var right = left + root.clientWidth; var top = root.offsetTop + root.clientTop; var bottom = top + root.clientHeight; return [left, right, top, bottom]; } // Root bounds for a root without an overflow clip as defined by: // http://wicg.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle function borderBoxBounds(root) { var left = root.offsetLeft; var right = left + root.offsetWidth; var top = root.offsetTop; var bottom = top + root.offsetHeight; return [left, right, top, bottom]; } function clientBounds(element) { var rect = element.getBoundingClientRect(); return [rect.left, rect.right, rect.top, rect.bottom]; } function rectArea(rect) { return (rect.left - rect.right) * (rect.bottom - rect.top); } function checkRect(actual, expected, description, all) { if (!expected.length) return; assert_equals(actual.left | 0, expected[0] | 0, description + '.left'); assert_equals(actual.right | 0, expected[1] | 0, description + '.right'); assert_equals(actual.top | 0, expected[2] | 0, description + '.top'); assert_equals(actual.bottom | 0, expected[3] | 0, description + '.bottom'); } function checkLastEntry(entries, i, expected) { assert_equals(entries.length, i + 1, 'entries.length'); if (expected) { checkRect( entries[i].boundingClientRect, expected.slice(0, 4), 'entries[' + i + '].boundingClientRect', entries[i]); checkRect( entries[i].intersectionRect, expected.slice(4, 8), 'entries[' + i + '].intersectionRect', entries[i]); checkRect( entries[i].rootBounds, expected.slice(8, 12), 'entries[' + i + '].rootBounds', entries[i]); if (expected.length > 12) { assert_equals( entries[i].isIntersecting, expected[12], 'entries[' + i + '].isIntersecting'); } } } function checkJsonEntry(actual, expected) { checkRect( actual.boundingClientRect, expected.boundingClientRect, 'entry.boundingClientRect'); checkRect( actual.intersectionRect, expected.intersectionRect, 'entry.intersectionRect'); if (actual.rootBounds == 'null') assert_equals(expected.rootBounds, 'null', 'rootBounds is null'); else checkRect(actual.rootBounds, expected.rootBounds, 'entry.rootBounds'); assert_equals(actual.isIntersecting, expected.isIntersecting); assert_equals(actual.target, expected.target); } function checkJsonEntries(actual, expected, description) { test(function() { assert_equals(actual.length, expected.length); for (var i = 0; i < actual.length; i++) checkJsonEntry(actual[i], expected[i]); }, description); } function checkIsIntersecting(entries, i, expected) { assert_equals(entries[i].isIntersecting, expected, 'entries[' + i + '].target.isIntersecting equals ' + expected); }