intersection-observer-test-utils.js (8182B)
1 // Here's how waitForNotification works: 2 // 3 // - myTestFunction0() 4 // - waitForNotification(myTestFunction1) 5 // - requestAnimationFrame() 6 // - Modify DOM in a way that should trigger an IntersectionObserver callback. 7 // - BeginFrame 8 // - requestAnimationFrame handler runs 9 // - Second requestAnimationFrame() 10 // - Style, layout, paint 11 // - IntersectionObserver generates new notifications 12 // - Posts a task to deliver notifications 13 // - Task to deliver IntersectionObserver notifications runs 14 // - IntersectionObserver callbacks run 15 // - Second requestAnimationFrameHandler runs 16 // - step_timeout() 17 // - step_timeout handler runs 18 // - myTestFunction1() 19 // - [optional] waitForNotification(myTestFunction2) 20 // - requestAnimationFrame() 21 // - Verify newly-arrived IntersectionObserver notifications 22 // - [optional] Modify DOM to trigger new notifications 23 // 24 // Ideally, it should be sufficient to use requestAnimationFrame followed 25 // by two step_timeouts, with the first step_timeout firing in between the 26 // requestAnimationFrame handler and the task to deliver notifications. 27 // However, the precise timing of requestAnimationFrame, the generation of 28 // a new display frame (when IntersectionObserver notifications are 29 // generated), and the delivery of these events varies between engines, making 30 // this tricky to test in a non-flaky way. 31 // 32 // In particular, in WebKit, requestAnimationFrame and the generation of 33 // a display frame are two separate tasks, so a step_timeout called within 34 // requestAnimationFrame can fire before a display frame is generated. 35 // 36 // In Gecko, on the other hand, requestAnimationFrame and the generation of 37 // a display frame are a single task, and IntersectionObserver notifications 38 // are generated during this task. However, the task posted to deliver these 39 // notifications can fire after the following requestAnimationFrame. 40 // 41 // This means that in general, by the time the second requestAnimationFrame 42 // handler runs, we know that IntersectionObservations have been generated, 43 // and that a task to deliver these notifications has been posted (though 44 // possibly not yet delivered). Then, by the time the step_timeout() handler 45 // runs, these notifications have been delivered. 46 // 47 // Since waitForNotification uses a double-rAF, it is now possible that 48 // IntersectionObservers may have generated more notifications than what is 49 // under test, but have not yet scheduled the new batch of notifications for 50 // delivery. As a result, observer.takeRecords should NOT be used in tests: 51 // 52 // - myTestFunction0() 53 // - waitForNotification(myTestFunction1) 54 // - requestAnimationFrame() 55 // - Modify DOM in a way that should trigger an IntersectionObserver callback. 56 // - BeginFrame 57 // - requestAnimationFrame handler runs 58 // - Second requestAnimationFrame() 59 // - Style, layout, paint 60 // - IntersectionObserver generates a batch of notifications 61 // - Posts a task to deliver notifications 62 // - Task to deliver IntersectionObserver notifications runs 63 // - IntersectionObserver callbacks run 64 // - BeginFrame 65 // - Second requestAnimationFrameHandler runs 66 // - step_timeout() 67 // - IntersectionObserver generates another batch of notifications 68 // - Post task to deliver notifications 69 // - step_timeout handler runs 70 // - myTestFunction1() 71 // - At this point, observer.takeRecords will get the second batch of 72 // notifications. 73 function waitForNotification(t, f) { 74 return new Promise(resolve => { 75 requestAnimationFrame(function() { 76 requestAnimationFrame(function() { 77 let callback = function() { 78 resolve(); 79 if (f) { 80 f(); 81 } 82 }; 83 if (t) { 84 t.step_timeout(callback); 85 } else { 86 setTimeout(callback); 87 } 88 }); 89 }); 90 }); 91 } 92 93 // If you need to wait until the IntersectionObserver algorithm has a chance 94 // to run, but don't need to wait for delivery of the notifications... 95 function waitForFrame(t, f) { 96 return new Promise(resolve => { 97 requestAnimationFrame(function() { 98 t.step_timeout(function() { 99 resolve(); 100 if (f) { 101 f(); 102 } 103 }); 104 }); 105 }); 106 } 107 108 // The timing of when runTestCycle is called is important. It should be 109 // called: 110 // 111 // - Before or during the window load event, or 112 // - Inside of a prior runTestCycle callback, *before* any assert_* methods 113 // are called. 114 // 115 // Following these rules will ensure that the test suite will not abort before 116 // all test steps have run. 117 // 118 // If the 'delay' parameter to the IntersectionObserver constructor is used, 119 // tests will need to add the same delay to their runTestCycle invocations, to 120 // wait for notifications to be generated and delivered. 121 function runTestCycle(f, description, delay) { 122 async_test(function(t) { 123 if (delay) { 124 step_timeout(() => { 125 waitForNotification(t, t.step_func_done(f)); 126 }, delay); 127 } else { 128 waitForNotification(t, t.step_func_done(f)); 129 } 130 }, description); 131 } 132 133 // Root bounds for a root with an overflow clip as defined by: 134 // http://wicg.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle 135 function contentBounds(root) { 136 var left = root.offsetLeft + root.clientLeft; 137 var right = left + root.clientWidth; 138 var top = root.offsetTop + root.clientTop; 139 var bottom = top + root.clientHeight; 140 return [left, right, top, bottom]; 141 } 142 143 // Root bounds for a root without an overflow clip as defined by: 144 // http://wicg.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle 145 function borderBoxBounds(root) { 146 var left = root.offsetLeft; 147 var right = left + root.offsetWidth; 148 var top = root.offsetTop; 149 var bottom = top + root.offsetHeight; 150 return [left, right, top, bottom]; 151 } 152 153 function clientBounds(element) { 154 var rect = element.getBoundingClientRect(); 155 return [rect.left, rect.right, rect.top, rect.bottom]; 156 } 157 158 function rectArea(rect) { 159 return (rect.left - rect.right) * (rect.bottom - rect.top); 160 } 161 162 function checkRect(actual, expected, description, epsilon = 0) { 163 if (!expected.length) 164 return; 165 assert_approx_equals(actual.left, expected[0], epsilon, description + '.left'); 166 assert_approx_equals(actual.right, expected[1], epsilon, description + '.right'); 167 assert_approx_equals(actual.top, expected[2], epsilon, description + '.top'); 168 assert_approx_equals(actual.bottom, expected[3], epsilon, description + '.bottom'); 169 } 170 171 function checkLastEntry(entries, i, expected, epsilon = 0) { 172 assert_equals(entries.length, i + 1, 'entries.length'); 173 if (expected) { 174 checkRect( 175 entries[i].boundingClientRect, expected.slice(0, 4), 176 'entries[' + i + '].boundingClientRect', epsilon); 177 checkRect( 178 entries[i].intersectionRect, expected.slice(4, 8), 179 'entries[' + i + '].intersectionRect', epsilon); 180 checkRect( 181 entries[i].rootBounds, expected.slice(8, 12), 182 'entries[' + i + '].rootBounds', epsilon); 183 if (expected.length > 12) { 184 assert_equals( 185 entries[i].isIntersecting, expected[12], 186 'entries[' + i + '].isIntersecting'); 187 } 188 } 189 } 190 191 function checkJsonEntry(actual, expected) { 192 checkRect( 193 actual.boundingClientRect, expected.boundingClientRect, 194 'entry.boundingClientRect'); 195 checkRect( 196 actual.intersectionRect, expected.intersectionRect, 197 'entry.intersectionRect'); 198 if (actual.rootBounds == 'null') 199 assert_equals(expected.rootBounds, 'null', 'rootBounds is null'); 200 else 201 checkRect(actual.rootBounds, expected.rootBounds, 'entry.rootBounds'); 202 assert_equals(actual.isIntersecting, expected.isIntersecting); 203 assert_equals(actual.target, expected.target); 204 } 205 206 function checkJsonEntries(actual, expected, description) { 207 test(function() { 208 assert_equals(actual.length, expected.length); 209 for (var i = 0; i < actual.length; i++) 210 checkJsonEntry(actual[i], expected[i]); 211 }, description); 212 } 213 214 function checkIsIntersecting(entries, i, expected) { 215 assert_equals(entries[i].isIntersecting, expected, 216 'entries[' + i + '].target.isIntersecting equals ' + expected); 217 }