test_DOM.js (14595B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const { dom } = ChromeUtils.importESModule( 6 "chrome://remote/content/shared/DOM.sys.mjs" 7 ); 8 const { NodeCache } = ChromeUtils.importESModule( 9 "chrome://remote/content/shared/webdriver/NodeCache.sys.mjs" 10 ); 11 12 class MockElement { 13 constructor(tagName, attrs = {}) { 14 this.tagName = tagName; 15 this.localName = tagName; 16 17 this.isConnected = false; 18 this.ownerGlobal = { 19 document: { 20 isActive() { 21 return true; 22 }, 23 }, 24 }; 25 26 for (let attr in attrs) { 27 this[attr] = attrs[attr]; 28 } 29 } 30 31 get nodeType() { 32 return 1; 33 } 34 35 get ELEMENT_NODE() { 36 return 1; 37 } 38 39 // this is a severely limited CSS selector 40 // that only supports lists of tag names 41 matches(selector) { 42 let tags = selector.split(","); 43 return tags.includes(this.localName); 44 } 45 } 46 47 class MockXULElement extends MockElement { 48 constructor(tagName, attrs = {}) { 49 super(tagName, attrs); 50 this.namespaceURI = XUL_NS; 51 52 if (typeof this.ownerDocument == "undefined") { 53 this.ownerDocument = {}; 54 } 55 if (typeof this.ownerDocument.documentElement == "undefined") { 56 this.ownerDocument.documentElement = { namespaceURI: XUL_NS }; 57 } 58 } 59 } 60 61 const xulEl = new MockXULElement("text"); 62 63 const domElInPrivilegedDocument = new MockElement("input", { 64 nodePrincipal: { isSystemPrincipal: true }, 65 }); 66 const xulElInPrivilegedDocument = new MockXULElement("text", { 67 nodePrincipal: { isSystemPrincipal: true }, 68 }); 69 70 function setupTest() { 71 const browser = Services.appShell.createWindowlessBrowser(false); 72 73 browser.document.body.innerHTML = ` 74 <div id="foo" style="margin: 50px"> 75 <iframe></iframe> 76 <video></video> 77 <svg xmlns="http://www.w3.org/2000/svg"></svg> 78 <form> 79 <button/> 80 <input/> 81 <fieldset> 82 <legend><input id="first"/></legend> 83 <legend><input id="second"/></legend> 84 </fieldset> 85 <select> 86 <optgroup> 87 <option id="in-group">foo</options> 88 </optgroup> 89 <option id="no-group">bar</option> 90 </select> 91 <textarea></textarea> 92 </form> 93 </div> 94 `; 95 96 const divEl = browser.document.querySelector("div"); 97 const svgEl = browser.document.querySelector("svg"); 98 const videoEl = browser.document.querySelector("video"); 99 const shadowRoot = videoEl.openOrClosedShadowRoot; 100 101 const buttonEl = browser.document.querySelector("button"); 102 const fieldsetEl = browser.document.querySelector("fieldset"); 103 const inputEl = browser.document.querySelector("input"); 104 const optgroupEl = browser.document.querySelector("optgroup"); 105 const optionInGroupEl = browser.document.querySelector("option#in-group"); 106 const optionNoGroupEl = browser.document.querySelector("option#no-group"); 107 const selectEl = browser.document.querySelector("select"); 108 const textareaEl = browser.document.querySelector("textarea"); 109 110 const iframeEl = browser.document.querySelector("iframe"); 111 const childEl = iframeEl.contentDocument.createElement("div"); 112 iframeEl.contentDocument.body.appendChild(childEl); 113 114 return { 115 browser, 116 buttonEl, 117 childEl, 118 divEl, 119 inputEl, 120 fieldsetEl, 121 iframeEl, 122 nodeCache: new NodeCache(), 123 optgroupEl, 124 optionInGroupEl, 125 optionNoGroupEl, 126 selectEl, 127 shadowRoot, 128 svgEl, 129 textareaEl, 130 videoEl, 131 }; 132 } 133 134 add_task(function test_findClosest() { 135 const { divEl, videoEl } = setupTest(); 136 137 equal(dom.findClosest(divEl, "foo"), null); 138 equal(dom.findClosest(videoEl, "div"), divEl); 139 }); 140 141 add_task(function test_isSelected() { 142 const { browser, divEl } = setupTest(); 143 144 const checkbox = browser.document.createElement("input"); 145 checkbox.setAttribute("type", "checkbox"); 146 147 ok(!dom.isSelected(checkbox)); 148 checkbox.checked = true; 149 ok(dom.isSelected(checkbox)); 150 151 // selected is not a property of <input type=checkbox> 152 checkbox.selected = true; 153 checkbox.checked = false; 154 ok(!dom.isSelected(checkbox)); 155 156 const option = browser.document.createElement("option"); 157 158 ok(!dom.isSelected(option)); 159 option.selected = true; 160 ok(dom.isSelected(option)); 161 162 // checked is not a property of <option> 163 option.checked = true; 164 option.selected = false; 165 ok(!dom.isSelected(option)); 166 167 // anything else should not be selected 168 for (const type of [undefined, null, "foo", true, [], {}, divEl]) { 169 ok(!dom.isSelected(type)); 170 } 171 }); 172 173 add_task(function test_isElement() { 174 const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); 175 176 ok(dom.isElement(divEl)); 177 ok(dom.isElement(svgEl)); 178 ok(dom.isElement(xulEl)); 179 ok(dom.isElement(domElInPrivilegedDocument)); 180 ok(dom.isElement(xulElInPrivilegedDocument)); 181 182 ok(!dom.isElement(shadowRoot)); 183 ok(!dom.isElement(divEl.ownerGlobal)); 184 ok(!dom.isElement(iframeEl.contentWindow)); 185 186 for (const type of [true, 42, {}, [], undefined, null]) { 187 ok(!dom.isElement(type)); 188 } 189 }); 190 191 add_task(function test_isDOMElement() { 192 const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); 193 194 ok(dom.isDOMElement(divEl)); 195 ok(dom.isDOMElement(svgEl)); 196 ok(dom.isDOMElement(domElInPrivilegedDocument)); 197 198 ok(!dom.isDOMElement(shadowRoot)); 199 ok(!dom.isDOMElement(divEl.ownerGlobal)); 200 ok(!dom.isDOMElement(iframeEl.contentWindow)); 201 ok(!dom.isDOMElement(xulEl)); 202 ok(!dom.isDOMElement(xulElInPrivilegedDocument)); 203 204 for (const type of [true, 42, "foo", {}, [], undefined, null]) { 205 ok(!dom.isDOMElement(type)); 206 } 207 }); 208 209 add_task(function test_isXULElement() { 210 const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); 211 212 ok(dom.isXULElement(xulEl)); 213 ok(dom.isXULElement(xulElInPrivilegedDocument)); 214 215 ok(!dom.isXULElement(divEl)); 216 ok(!dom.isXULElement(domElInPrivilegedDocument)); 217 ok(!dom.isXULElement(svgEl)); 218 ok(!dom.isXULElement(shadowRoot)); 219 ok(!dom.isXULElement(divEl.ownerGlobal)); 220 ok(!dom.isXULElement(iframeEl.contentWindow)); 221 222 for (const type of [true, 42, "foo", {}, [], undefined, null]) { 223 ok(!dom.isXULElement(type)); 224 } 225 }); 226 227 add_task(function test_isDOMWindow() { 228 const { divEl, iframeEl, shadowRoot, svgEl } = setupTest(); 229 230 ok(dom.isDOMWindow(divEl.ownerGlobal)); 231 ok(dom.isDOMWindow(iframeEl.contentWindow)); 232 233 ok(!dom.isDOMWindow(divEl)); 234 ok(!dom.isDOMWindow(svgEl)); 235 ok(!dom.isDOMWindow(shadowRoot)); 236 ok(!dom.isDOMWindow(domElInPrivilegedDocument)); 237 ok(!dom.isDOMWindow(xulEl)); 238 ok(!dom.isDOMWindow(xulElInPrivilegedDocument)); 239 240 for (const type of [true, 42, {}, [], undefined, null]) { 241 ok(!dom.isDOMWindow(type)); 242 } 243 }); 244 245 add_task(function test_isShadowRoot() { 246 const { browser, divEl, iframeEl, shadowRoot, svgEl } = setupTest(); 247 248 ok(dom.isShadowRoot(shadowRoot)); 249 250 ok(!dom.isShadowRoot(divEl)); 251 ok(!dom.isShadowRoot(svgEl)); 252 ok(!dom.isShadowRoot(divEl.ownerGlobal)); 253 ok(!dom.isShadowRoot(iframeEl.contentWindow)); 254 ok(!dom.isShadowRoot(xulEl)); 255 ok(!dom.isShadowRoot(domElInPrivilegedDocument)); 256 ok(!dom.isShadowRoot(xulElInPrivilegedDocument)); 257 258 for (const type of [true, 42, "foo", {}, [], undefined, null]) { 259 ok(!dom.isShadowRoot(type)); 260 } 261 262 const documentFragment = browser.document.createDocumentFragment(); 263 ok(!dom.isShadowRoot(documentFragment)); 264 }); 265 266 add_task(function test_isReadOnly() { 267 const { browser, divEl, textareaEl } = setupTest(); 268 269 const input = browser.document.createElement("input"); 270 input.readOnly = true; 271 ok(dom.isReadOnly(input)); 272 273 textareaEl.readOnly = true; 274 ok(dom.isReadOnly(textareaEl)); 275 276 ok(!dom.isReadOnly(divEl)); 277 divEl.readOnly = true; 278 ok(!dom.isReadOnly(divEl)); 279 280 ok(!dom.isReadOnly(null)); 281 }); 282 283 add_task(function test_isDisabledSelect() { 284 const { optgroupEl, optionInGroupEl, optionNoGroupEl, selectEl } = 285 setupTest(); 286 287 optionNoGroupEl.disabled = true; 288 ok(dom.isDisabled(optionNoGroupEl)); 289 optionNoGroupEl.disabled = false; 290 ok(!dom.isDisabled(optionNoGroupEl)); 291 292 optgroupEl.disabled = true; 293 ok(dom.isDisabled(optgroupEl)); 294 ok(dom.isDisabled(optionInGroupEl)); 295 optgroupEl.disabled = false; 296 ok(!dom.isDisabled(optgroupEl)); 297 ok(!dom.isDisabled(optionInGroupEl)); 298 299 selectEl.disabled = true; 300 ok(dom.isDisabled(selectEl)); 301 ok(dom.isDisabled(optgroupEl)); 302 ok(dom.isDisabled(optionNoGroupEl)); 303 selectEl.disabled = false; 304 ok(!dom.isDisabled(selectEl)); 305 ok(!dom.isDisabled(optgroupEl)); 306 ok(!dom.isDisabled(optionNoGroupEl)); 307 }); 308 309 add_task(function test_isDisabledFormControl() { 310 const { buttonEl, fieldsetEl, inputEl, selectEl, textareaEl } = setupTest(); 311 312 for (const elem of [buttonEl, inputEl, selectEl, textareaEl]) { 313 elem.disabled = true; 314 ok(dom.isDisabled(elem)); 315 elem.disabled = false; 316 ok(!dom.isDisabled(elem)); 317 } 318 319 const inputs = fieldsetEl.querySelectorAll("input"); 320 fieldsetEl.disabled = true; 321 ok(dom.isDisabled(fieldsetEl)); 322 ok(!dom.isDisabled(inputs[0])); 323 ok(dom.isDisabled(inputs[1])); 324 fieldsetEl.disabled = false; 325 ok(!dom.isDisabled(fieldsetEl)); 326 ok(!dom.isDisabled(inputs[0])); 327 ok(!dom.isDisabled(inputs[1])); 328 }); 329 330 add_task(function test_isDisabledElement() { 331 const { divEl, svgEl } = setupTest(); 332 const mockXulEl = new MockXULElement("browser", { disabled: true }); 333 334 for (const elem of [divEl, svgEl, mockXulEl]) { 335 ok(!dom.isDisabled(elem)); 336 elem.disabled = true; 337 ok(!dom.isDisabled(elem)); 338 } 339 }); 340 341 add_task(function test_isDisabledNoDOMElement() { 342 ok(!dom.isDisabled()); 343 344 for (const obj of [null, undefined, 42, "", {}, []]) { 345 ok(!dom.isDisabled(obj)); 346 } 347 }); 348 349 add_task(function test_isEditingHost() { 350 const { browser, divEl, svgEl } = setupTest(); 351 352 ok(!dom.isEditingHost(null)); 353 354 ok(!dom.isEditingHost(divEl)); 355 divEl.contentEditable = true; 356 ok(dom.isEditingHost(divEl)); 357 358 ok(!dom.isEditingHost(svgEl)); 359 browser.document.designMode = "on"; 360 ok(dom.isEditingHost(svgEl)); 361 }); 362 363 add_task(function test_isEditable() { 364 const { browser, divEl, svgEl, textareaEl } = setupTest(); 365 366 ok(!dom.isEditable(null)); 367 368 for (let type of [ 369 "checkbox", 370 "radio", 371 "hidden", 372 "submit", 373 "button", 374 "image", 375 ]) { 376 const input = browser.document.createElement("input"); 377 input.setAttribute("type", type); 378 379 ok(!dom.isEditable(input)); 380 } 381 382 const input = browser.document.createElement("input"); 383 ok(dom.isEditable(input)); 384 input.setAttribute("type", "text"); 385 ok(dom.isEditable(input)); 386 387 ok(dom.isEditable(textareaEl)); 388 389 const textareaDisabled = browser.document.createElement("textarea"); 390 textareaDisabled.disabled = true; 391 ok(!dom.isEditable(textareaDisabled)); 392 393 const textareaReadOnly = browser.document.createElement("textarea"); 394 textareaReadOnly.readOnly = true; 395 ok(!dom.isEditable(textareaReadOnly)); 396 397 ok(!dom.isEditable(divEl)); 398 divEl.contentEditable = true; 399 ok(dom.isEditable(divEl)); 400 401 ok(!dom.isEditable(svgEl)); 402 browser.document.designMode = "on"; 403 ok(dom.isEditable(svgEl)); 404 }); 405 406 add_task(function test_isMutableFormControlElement() { 407 const { browser, divEl, textareaEl } = setupTest(); 408 409 ok(!dom.isMutableFormControl(null)); 410 411 ok(dom.isMutableFormControl(textareaEl)); 412 413 const textareaDisabled = browser.document.createElement("textarea"); 414 textareaDisabled.disabled = true; 415 ok(!dom.isMutableFormControl(textareaDisabled)); 416 417 const textareaReadOnly = browser.document.createElement("textarea"); 418 textareaReadOnly.readOnly = true; 419 ok(!dom.isMutableFormControl(textareaReadOnly)); 420 421 const mutableStates = new Set([ 422 "color", 423 "date", 424 "datetime-local", 425 "email", 426 "file", 427 "month", 428 "number", 429 "password", 430 "range", 431 "search", 432 "tel", 433 "text", 434 "url", 435 "week", 436 ]); 437 for (const type of mutableStates) { 438 const input = browser.document.createElement("input"); 439 input.setAttribute("type", type); 440 ok(dom.isMutableFormControl(input)); 441 } 442 443 const inputHidden = browser.document.createElement("input"); 444 inputHidden.setAttribute("type", "hidden"); 445 ok(!dom.isMutableFormControl(inputHidden)); 446 447 ok(!dom.isMutableFormControl(divEl)); 448 divEl.contentEditable = true; 449 ok(!dom.isMutableFormControl(divEl)); 450 browser.document.designMode = "on"; 451 ok(!dom.isMutableFormControl(divEl)); 452 }); 453 454 add_task(function test_coordinates() { 455 const { divEl } = setupTest(); 456 457 let coords = dom.coordinates(divEl); 458 ok(coords.hasOwnProperty("x")); 459 ok(coords.hasOwnProperty("y")); 460 equal(typeof coords.x, "number"); 461 equal(typeof coords.y, "number"); 462 463 deepEqual(dom.coordinates(divEl), { x: 0, y: 0 }); 464 deepEqual(dom.coordinates(divEl, 10, 10), { x: 10, y: 10 }); 465 deepEqual(dom.coordinates(divEl, -5, -5), { x: -5, y: -5 }); 466 467 Assert.throws(() => dom.coordinates(null), /node is null/); 468 469 Assert.throws( 470 () => dom.coordinates(divEl, "string", undefined), 471 /Offset must be a number/ 472 ); 473 Assert.throws( 474 () => dom.coordinates(divEl, undefined, "string"), 475 /Offset must be a number/ 476 ); 477 Assert.throws( 478 () => dom.coordinates(divEl, "string", "string"), 479 /Offset must be a number/ 480 ); 481 Assert.throws( 482 () => dom.coordinates(divEl, {}, undefined), 483 /Offset must be a number/ 484 ); 485 Assert.throws( 486 () => dom.coordinates(divEl, undefined, {}), 487 /Offset must be a number/ 488 ); 489 Assert.throws( 490 () => dom.coordinates(divEl, {}, {}), 491 /Offset must be a number/ 492 ); 493 Assert.throws( 494 () => dom.coordinates(divEl, [], undefined), 495 /Offset must be a number/ 496 ); 497 Assert.throws( 498 () => dom.coordinates(divEl, undefined, []), 499 /Offset must be a number/ 500 ); 501 Assert.throws( 502 () => dom.coordinates(divEl, [], []), 503 /Offset must be a number/ 504 ); 505 }); 506 507 add_task(function test_isDetached() { 508 const { childEl, iframeEl } = setupTest(); 509 510 let detachedShadowRoot = childEl.attachShadow({ mode: "open" }); 511 detachedShadowRoot.innerHTML = "<input></input>"; 512 513 // Connected to the DOM 514 ok(!dom.isDetached(detachedShadowRoot)); 515 516 // Node document (ownerDocument) is not the active document 517 iframeEl.remove(); 518 ok(dom.isDetached(detachedShadowRoot)); 519 520 // host element is stale (eg. not connected) 521 detachedShadowRoot.host.remove(); 522 equal(childEl.isConnected, false); 523 ok(dom.isDetached(detachedShadowRoot)); 524 }); 525 526 add_task(function test_isStale() { 527 const { childEl, iframeEl } = setupTest(); 528 529 // Connected to the DOM 530 ok(!dom.isStale(childEl)); 531 532 // Not part of the active document 533 iframeEl.remove(); 534 ok(dom.isStale(childEl)); 535 536 // Not connected to the DOM 537 childEl.remove(); 538 ok(dom.isStale(childEl)); 539 });