test_interactive_widget.html (12573B)
1 <!DOCTYPE HTML> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <title>interactive-widget tests</title> 6 <script src="/tests/SimpleTest/SimpleTest.js"></script> 7 <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> 8 <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> 9 <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> 10 <style> 11 textarea { 12 height: 100px; 13 width: 100px; 14 } 15 </style> 16 </head> 17 <body> 18 <textarea></textarea> 19 <p id="display"></p> 20 <div id="content" style="display: none"></div> 21 <pre id="test"></pre> 22 <script> 23 async function getViewportMetrics() { 24 return SpecialPowers.spawn(parent, [], () => { 25 return [ content.window.innerHeight, 26 content.window.visualViewport.height, 27 content.window.visualViewport.width ]; 28 }); 29 } 30 31 let initial_window_height, initial_visual_viewport_width, initial_visual_viewport_height; 32 33 // setup a meta viewport tag in the top document. 34 add_setup(async () => { 35 // Try to close the software keyboard to invoke `documentElement.focus()`. 36 document.documentElement.focus(); 37 await SimpleTest.promiseWaitForCondition( 38 () => document.activeElement == document.documentElement, 39 "Waiting for focus"); 40 41 await SpecialPowers.spawn(parent, [], async () => { 42 const initial_scale = content.window.visualViewport.scale; 43 44 const meta = content.document.createElement("meta"); 45 meta.setAttribute("id", "interactive-widget"); 46 meta.setAttribute("name", "viewport"); 47 meta.setAttribute("content", "width=device-width, initial-scale=1, user-scalable=no"); 48 content.document.documentElement.appendChild(meta); 49 50 const eventPromise = new Promise(resolve => content.window.addEventListener("resize", resolve)); 51 // Flush the viewport change. 52 content.document.documentElement.getBoundingClientRect(); 53 54 // If this top level content is rendered as `scale < 1.0`, it means there 55 // was no meta viewport tag at all, so that adding the above meta viewport 56 // tag will fire a resize event, thus we need to wait for the event here. 57 // Otherwise, we will wait for the event in the first `resizes-content` 58 // test and the test will fail. 59 // 60 // NOTE: We need this `scale < 1.0` check for --run-until-failure option. 61 if (initial_scale < 1.0) { 62 await eventPromise; 63 } 64 }); 65 66 SimpleTest.registerCleanupFunction(async () => { 67 await SpecialPowers.spawn(parent, [], async () => { 68 const meta = content.document.querySelector("#interactive-widget"); 69 meta.setAttribute("content", ""); 70 // Flush the change above. 71 content.document.documentElement.getBoundingClientRect(); 72 // A dummy Promise to make sure that SpecialPowers.spawn's Promise will 73 // never be resolved until this script has run in the parent context. 74 await new Promise(resolve => resolve()); 75 }); 76 }); 77 [ initial_window_height, 78 initial_visual_viewport_height, initial_visual_viewport_width ] = await getViewportMetrics(); 79 ok(initial_visual_viewport_width < initial_visual_viewport_height, 80 `the visual viewport height (${initial_visual_viewport_height}) is less ` + 81 `than the visual viewport width (${initial_visual_viewport_width}), ` + 82 `it hightly suspects the virtual keyboard persists there, thus ` + 83 `we can't run this interactive-widget tests properly`); 84 }); 85 86 async function setupInteractiveWidget(aValue) { 87 await SpecialPowers.spawn(parent, [aValue], async (value) => { 88 const meta = content.document.querySelector("#interactive-widget"); 89 meta.setAttribute("content", `width=device-width, initial-scale=1, user-scalable=no, interactive-widget=${value}`); 90 91 // Flush the viewport change. 92 content.document.documentElement.getBoundingClientRect(); 93 94 // A dummy Promise to make sure that SpecialPowers.spawn's Promise will 95 // never be resolved until these script have run in the parent context. 96 await new Promise(resolve => resolve()); 97 }); 98 } 99 100 // SpecialPowers.spawn doesn't provide any reasonable way to make sure event 101 // listeners have been set in the given context (bug 1743857), so here we post 102 // a message just before setting up a resize event listener and return two 103 // Promises, one will be resolved when we received the message, the other will 104 // be resolved when we got a resize event. 105 function setupResizeEventListener(aInteractiveWidget) { 106 const ready = new Promise(resolve => { 107 window.addEventListener("message", msg => { 108 if (msg.data == "interactive-widget:ready") { 109 resolve(msg.data) 110 } 111 }, { once: true }); 112 }); 113 114 const resizePromise = SpecialPowers.spawn(parent, [aInteractiveWidget], async (interactiveWidget) => { 115 // #testframe is the iframe id where our mochitest harness loads each test 116 // document, but if this test runs solely just like ./mach test TEST_PATH, 117 // the test document gets loaded in the top level content. 118 const target = content.document.querySelector("#testframe") ? 119 content.document.querySelector("#testframe").contentWindow : content.window; 120 121 let eventPromise; 122 if (interactiveWidget == "resizes-content") { 123 eventPromise = new Promise(resolve => content.window.addEventListener("resize", resolve)); 124 } else if (interactiveWidget == "resizes-visual") { 125 eventPromise = new Promise(resolve => content.window.visualViewport.addEventListener("resize", resolve)); 126 } else { 127 ok(false, `Unexpected interactive-widget=${interactiveWidget}`); 128 } 129 target.postMessage("interactive-widget:ready", "*"); 130 await eventPromise; 131 }); 132 133 return [ ready, resizePromise ]; 134 } 135 136 // A utility function to hide the software keyboard. 137 // This function needs to be called while the software keyboard is shown on 138 // `resizes-content' or `resizes-visual` mode. 139 async function hideKeyboard() { 140 const interactiveWidget = await SpecialPowers.spawn(parent, [], () => { 141 const meta = content.document.querySelector("#interactive-widget"); 142 return meta.getAttribute("content").match(/interactive-widget=([\w-].+?)[,\s]*$/)[1]; 143 }); 144 145 let [ readyPromise, resizePromise ] = setupResizeEventListener(interactiveWidget); 146 await readyPromise; 147 148 // Tap outside the textarea to hide the software keyboard. 149 await synthesizeNativeTap(document.querySelector("textarea"), 150, 50); 150 await resizePromise; 151 152 await SimpleTest.promiseWaitForCondition( 153 async () => { 154 let [ current_window_height, current_visual_viewport_height ] = await getViewportMetrics(); 155 return current_window_height == initial_window_height && 156 current_visual_viewport_height == initial_visual_viewport_height; 157 }, 158 "Waiting for restoring the initial state"); 159 } 160 161 // `resizes-content` test 162 add_task(async () => { 163 await setupInteractiveWidget("resizes-content"); 164 165 // Setup a resize event listener in the top level document. 166 let [ readyPromise, resizePromise ] = setupResizeEventListener("resizes-content"); 167 // Make sure the event listener has been set. 168 await readyPromise; 169 170 // Tap the textarea to show the software keyboard. 171 await synthesizeNativeTap(document.querySelector("textarea"), 50, 50); 172 173 await resizePromise; 174 175 // Now the software keyboard has appeared, before running the next test we 176 // need to hide the keyboard. 177 SimpleTest.registerCurrentTaskCleanupFunction(async () => await hideKeyboard()); 178 179 await SimpleTest.promiseWaitForCondition( 180 () => document.activeElement == document.querySelector("textarea"), 181 "Waiting for focus"); 182 183 let [ window_height, visual_viewport_height ] = await getViewportMetrics(); 184 ok(window_height < initial_window_height, 185 `The layout viewport got resized to ${window_height} from ${initial_window_height}`); 186 ok(visual_viewport_height < initial_visual_viewport_height, 187 `The visual viewport got resized to ${visual_viewport_height} from ${initial_visual_viewport_height}`); 188 }); 189 190 // `resizes-visual` test 191 add_task(async () => { 192 await setupInteractiveWidget("resizes-visual"); 193 194 // Setup a resize event listener in the top level document. 195 let [ readyPromise, resizePromise ] = setupResizeEventListener("resizes-visual"); 196 // Make sure the event listener has been set. 197 await readyPromise; 198 199 // Tap the textarea to show the software keyboard. 200 await synthesizeNativeTap(document.querySelector("textarea"), 50, 50); 201 202 await resizePromise; 203 204 // Now the software keyboard has appeared, before running the next test we 205 // need to hide the keyboard. 206 SimpleTest.registerCurrentTaskCleanupFunction(async () => await hideKeyboard()); 207 208 await SimpleTest.promiseWaitForCondition( 209 () => document.activeElement == document.querySelector("textarea"), 210 "Waiting for focus"); 211 212 let [ window_height, visual_viewport_height ] = await getViewportMetrics(); 213 is(window_height, initial_window_height, 214 "The layout viewport is not resized on resizes-visual"); 215 ok(visual_viewport_height < initial_visual_viewport_height, 216 `The visual viewport got resized to ${visual_viewport_height} from ${initial_visual_viewport_height}`); 217 }); 218 219 // Append an element in the top level document that the element will be the 220 // underneath the software keyboard. 221 async function appendSpacer() { 222 await SpecialPowers.spawn(parent, [], async () => { 223 const div = content.document.createElement("div"); 224 div.setAttribute("id", "interactive-widget-test-spacer"); 225 div.style = "height: 200vh; position: absolute; top: 90vh;"; 226 content.document.body.appendChild(div); 227 228 // Flush the change. 229 content.document.documentElement.getBoundingClientRect(); 230 231 // A dummy Promise to make sure that SpecialPowers.spawn's Promise will 232 // never be resolved until these script have run in the parent context. 233 await new Promise(resolve => resolve()); 234 }); 235 236 SimpleTest.registerCurrentTaskCleanupFunction(async () => { 237 await SpecialPowers.spawn(parent, [], async () => { 238 const div = content.document.querySelector("#interactive-widget-test-spacer"); 239 div.remove(); 240 // Flush the change. 241 content.document.documentElement.getBoundingClientRect(); 242 243 // A dummy Promise to make sure that SpecialPowers.spawn's Promise will 244 // never be resolved until these script have run in the parent context. 245 await new Promise(resolve => resolve()); 246 }); 247 }); 248 } 249 250 // `overlays-content` test 251 add_task(async () => { 252 await setupInteractiveWidget("overlays-content"); 253 254 await appendSpacer(); 255 256 // Tap the textarea to show the software keyboard. 257 await synthesizeNativeTap(document.querySelector("textarea"), 50, 50); 258 259 // Now the software keyboard has appeared, before running the next test we 260 // need to hide the keyboard. 261 SimpleTest.registerCurrentTaskCleanupFunction(async () => { 262 // Switch back to `resizes-content` mode so that we can receive a resize 263 // event when the keyboard gets hidden. 264 await setupInteractiveWidget("resizes-content"); 265 await hideKeyboard(); 266 }); 267 268 await SimpleTest.promiseWaitForCondition( 269 () => document.activeElement == document.querySelector("textarea"), 270 "Waiting for focus"); 271 272 let [ window_height, visual_viewport_height ] = await getViewportMetrics(); 273 is(window_height, initial_window_height, 274 "The layout viewport is not resized on overlays-content"); 275 is(visual_viewport_height, initial_visual_viewport_height, 276 "The visual viewport is not resized on overlays-content"); 277 278 // Call a scrollIntoView() on an element underneath the keyboard and see if 279 // the current scroll position changes. 280 const scrollPosition = await SpecialPowers.spawn(parent, [], () => { 281 return content.window.scrollY; 282 }); 283 await SpecialPowers.spawn(parent, [], async () => { 284 const div = content.document.querySelector("#interactive-widget-test-spacer"); 285 div.scrollIntoView({ behavior: "instant" }); 286 287 // Though two rAFs ensure there's at least one scroll event if there is, 288 // we use two additional rAFs just in case. 289 await new Promise(resolve => content.window.requestAnimationFrame(resolve)); 290 await new Promise(resolve => content.window.requestAnimationFrame(resolve)); 291 await new Promise(resolve => content.window.requestAnimationFrame(resolve)); 292 await new Promise(resolve => content.window.requestAnimationFrame(resolve)); 293 }); 294 295 const newScrollPosition = await SpecialPowers.spawn(parent, [], () => { 296 return content.window.scrollY; 297 }); 298 is(scrollPosition, newScrollPosition, "The scrollIntoView() call has no effect"); 299 }); 300 </script> 301 </body> 302 </html>