head.js (32484B)
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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /* eslint no-unused-vars: [2, {"vars": "local", caughtErrors: "none"}] */ 8 /* import-globals-from ../../shared/test/shared-head.js */ 9 10 // Sometimes HTML pages have a `clear` function that cleans up the storage they 11 // created. To make sure it's always called, we are registering as a cleanup 12 // function, but since this needs to run before tabs are closed, we need to 13 // do this registration before importing `shared-head`, since declaration 14 // order matters. 15 registerCleanupFunction(async () => { 16 Services.cookies.removeAll(); 17 18 // Close tabs and force memory collection to happen 19 while (gBrowser.tabs.length > 1) { 20 const browser = gBrowser.selectedBrowser; 21 const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree(); 22 for (const context of contexts) { 23 await SpecialPowers.spawn(context, [], async () => { 24 const win = content.wrappedJSObject; 25 26 // Some windows (e.g., about: URLs) don't have storage available 27 try { 28 win.localStorage.clear(); 29 win.sessionStorage.clear(); 30 } catch (ex) { 31 // ignore 32 } 33 34 if (win.clear) { 35 // Do not get hung into win.clear() forever 36 await Promise.race([ 37 new Promise(r => win.setTimeout(r, 10000)), 38 win.clear(), 39 ]); 40 } 41 }); 42 } 43 44 await closeTabAndToolbox(gBrowser.selectedTab); 45 } 46 forceCollections(); 47 }); 48 49 // shared-head.js handles imports, constants, and utility functions 50 Services.scriptloader.loadSubScript( 51 "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", 52 this 53 ); 54 55 const { 56 TableWidget, 57 } = require("resource://devtools/client/shared/widgets/TableWidget.js"); 58 const { 59 LocalTabCommandsFactory, 60 } = require("resource://devtools/client/framework/local-tab-commands-factory.js"); 61 const STORAGE_PREF = "devtools.storage.enabled"; 62 const DUMPEMIT_PREF = "devtools.dump.emit"; 63 const DEBUGGERLOG_PREF = "devtools.debugger.log"; 64 65 // Allows Cache API to be working on usage `http` test page 66 const CACHES_ON_HTTP_PREF = "dom.caches.testing.enabled"; 67 const PATH = "browser/devtools/client/storage/test/"; 68 const MAIN_DOMAIN = "http://test1.example.org/" + PATH; 69 const MAIN_DOMAIN_SECURED = "https://test1.example.org/" + PATH; 70 const MAIN_DOMAIN_WITH_PORT = "http://test1.example.org:8000/" + PATH; 71 const ALT_DOMAIN = "http://sectest1.example.org/" + PATH; 72 const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH; 73 74 // GUID to be used as a separator in compound keys. This must match the same 75 // constant in devtools/server/actors/resources/storage/index.js, 76 // devtools/client/storage/ui.js and devtools/server/tests/browser/head.js 77 const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}"; 78 79 var gToolbox, gPanelWindow, gUI; 80 81 // Services.prefs.setBoolPref(DUMPEMIT_PREF, true); 82 // Services.prefs.setBoolPref(DEBUGGERLOG_PREF, true); 83 84 Services.prefs.setBoolPref(STORAGE_PREF, true); 85 Services.prefs.setBoolPref(CACHES_ON_HTTP_PREF, true); 86 registerCleanupFunction(() => { 87 gToolbox = gPanelWindow = gUI = null; 88 Services.prefs.clearUserPref(CACHES_ON_HTTP_PREF); 89 Services.prefs.clearUserPref(DEBUGGERLOG_PREF); 90 Services.prefs.clearUserPref(DUMPEMIT_PREF); 91 Services.prefs.clearUserPref(STORAGE_PREF); 92 }); 93 94 /** 95 * This generator function opens the given url in a new tab, then sets up the 96 * page by waiting for all cookies, indexedDB items etc. 97 * 98 * @param url {String} The url to be opened in the new tab 99 * @param options {Object} The tab options for the new tab 100 * 101 * @return {Promise} A promise that resolves after the tab is ready 102 */ 103 async function openTab(url, options = {}) { 104 const tab = await addTab(url, options); 105 106 const browser = gBrowser.selectedBrowser; 107 const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree(); 108 109 for (const context of contexts) { 110 await SpecialPowers.spawn(context, [], async () => { 111 const win = content.wrappedJSObject; 112 const readyState = win.document.readyState; 113 info(`Found a window: ${readyState}`); 114 if (readyState != "complete") { 115 await new Promise(resolve => { 116 const onLoad = () => { 117 win.removeEventListener("load", onLoad); 118 resolve(); 119 }; 120 win.addEventListener("load", onLoad); 121 }); 122 } 123 if (win.setup) { 124 await win.setup(); 125 } 126 }); 127 } 128 129 return tab; 130 } 131 132 /** 133 * This generator function opens the given url in a new tab, then sets up the 134 * page by waiting for all cookies, indexedDB items etc. to be created; Then 135 * opens the storage inspector and waits for the storage tree and table to be 136 * populated. 137 * 138 * @param url {String} The url to be opened in the new tab 139 * @param options {Object} The tab options for the new tab 140 * 141 * @return {Promise} A promise that resolves after storage inspector is ready 142 */ 143 async function openTabAndSetupStorage(url, options = {}) { 144 // open tab 145 await openTab(url, options); 146 147 // open storage inspector 148 return openStoragePanel(); 149 } 150 151 /** 152 * Open a toolbox with the storage panel opened by default 153 * for a given Web Extension. 154 * 155 * @param {string} addonId 156 * The ID of the Web Extension to debug. 157 */ 158 var openStoragePanelForAddon = async function (addonId) { 159 const toolbox = await gDevTools.showToolboxForWebExtension(addonId, { 160 toolId: "storage", 161 }); 162 163 info("Making sure that the toolbox's frame is focused"); 164 await SimpleTest.promiseFocus(toolbox.win); 165 166 const storage = _setupStoragePanelForTest(toolbox); 167 168 return { 169 toolbox, 170 storage, 171 }; 172 }; 173 174 /** 175 * Open the toolbox, with the storage tool visible. 176 * 177 * @param tab {XULTab} Optional, the tab for the toolbox; defaults to selected tab 178 * @param commands {Object} Optional, the commands for the toolbox; defaults to a tab commands 179 * @param hostType {Toolbox.HostType} Optional, type of host that will host the toolbox 180 * 181 * @return {Promise} a promise that resolves when the storage inspector is ready 182 */ 183 var openStoragePanel = async function ({ tab, hostType } = {}) { 184 const toolbox = await openToolboxForTab( 185 tab || gBrowser.selectedTab, 186 "storage", 187 hostType 188 ); 189 190 const storage = _setupStoragePanelForTest(toolbox); 191 192 return { 193 toolbox, 194 storage, 195 }; 196 }; 197 198 /** 199 * Set global variables needed in helper functions 200 * 201 * @param toolbox {Toolbox} 202 * @return {StoragePanel} 203 */ 204 function _setupStoragePanelForTest(toolbox) { 205 const storage = toolbox.getPanel("storage"); 206 gPanelWindow = storage.panelWindow; 207 gUI = storage.UI; 208 gToolbox = toolbox; 209 210 // The table animation flash causes some timeouts on Linux debug tests, 211 // so we disable it 212 gUI.animationsEnabled = false; 213 214 return storage; 215 } 216 217 /** 218 * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and 219 * windows. 220 */ 221 function forceCollections() { 222 Cu.forceGC(); 223 Cu.forceCC(); 224 Cu.forceShrinkingGC(); 225 } 226 227 // Sends a click event on the passed DOM node in an async manner 228 function click(node) { 229 node.scrollIntoView(); 230 231 return new Promise(resolve => { 232 // We need setTimeout here to allow any scrolling to complete before clicking 233 // the node. 234 setTimeout(() => { 235 node.click(); 236 resolve(); 237 }, 200); 238 }); 239 } 240 241 /** 242 * Recursively expand the variables view up to a given property. 243 * 244 * @param options 245 * Options for view expansion: 246 * - rootVariable: start from the given scope/variable/property. 247 * - expandTo: string made up of property names you want to expand. 248 * For example: "body.firstChild.nextSibling" given |rootVariable: 249 * document|. 250 * @return object 251 * A promise that is resolved only when the last property in |expandTo| 252 * is found, and rejected otherwise. Resolution reason is always the 253 * last property - |nextSibling| in the example above. Rejection is 254 * always the last property that was found. 255 */ 256 function variablesViewExpandTo(options) { 257 const root = options.rootVariable; 258 const expandTo = options.expandTo.split("."); 259 260 return new Promise((resolve, reject) => { 261 function getNext(prop) { 262 const name = expandTo.shift(); 263 const newProp = prop.get(name); 264 265 if (expandTo.length) { 266 ok(newProp, "found property " + name); 267 if (newProp && newProp.expand) { 268 newProp.expand(); 269 getNext(newProp); 270 } else { 271 reject(prop); 272 } 273 } else if (newProp) { 274 resolve(newProp); 275 } else { 276 reject(prop); 277 } 278 } 279 280 if (root && root.expand) { 281 root.expand(); 282 getNext(root); 283 } else { 284 resolve(root); 285 } 286 }); 287 } 288 289 /** 290 * Find variables or properties in a VariablesView instance. 291 * 292 * @param array ruleArray 293 * The array of rules you want to match. Each rule is an object with: 294 * - name (string|regexp): property name to match. 295 * - value (string|regexp): property value to match. 296 * - dontMatch (boolean): make sure the rule doesn't match any property. 297 * @param boolean parsed 298 * true if we want to test the rules in the parse value section of the 299 * storage sidebar 300 * @return object 301 * A promise object that is resolved when all the rules complete 302 * matching. The resolved callback is given an array of all the rules 303 * you wanted to check. Each rule has a new property: |matchedProp| 304 * which holds a reference to the Property object instance from the 305 * VariablesView. If the rule did not match, then |matchedProp| is 306 * undefined. 307 */ 308 function findVariableViewProperties(ruleArray, parsed) { 309 // Initialize the search. 310 function init() { 311 // If parsed is true, we are checking rules in the parsed value section of 312 // the storage sidebar. That scope uses a blank variable as a placeholder 313 // Thus, adding a blank parent to each name 314 if (parsed) { 315 ruleArray = ruleArray.map(({ name, value, dontMatch }) => { 316 return { name: "." + name, value, dontMatch }; 317 }); 318 } 319 // Separate out the rules that require expanding properties throughout the 320 // view. 321 const expandRules = []; 322 const rules = ruleArray.filter(rule => { 323 if (typeof rule.name == "string" && rule.name.indexOf(".") > -1) { 324 expandRules.push(rule); 325 return false; 326 } 327 return true; 328 }); 329 330 // Search through the view those rules that do not require any properties to 331 // be expanded. Build the array of matchers, outstanding promises to be 332 // resolved. 333 const outstanding = []; 334 335 finder(rules, gUI.view, outstanding); 336 337 // Process the rules that need to expand properties. 338 const lastStep = processExpandRules.bind(null, expandRules); 339 340 // Return the results - a promise resolved to hold the updated ruleArray. 341 const returnResults = onAllRulesMatched.bind(null, ruleArray); 342 343 return Promise.all(outstanding).then(lastStep).then(returnResults); 344 } 345 346 function onMatch(prop, rule, matched) { 347 if (matched && !rule.matchedProp) { 348 rule.matchedProp = prop; 349 } 350 } 351 352 function finder(rules, view, promises) { 353 for (const scope of view) { 354 for (const [, prop] of scope) { 355 for (const rule of rules) { 356 const matcher = matchVariablesViewProperty(prop, rule); 357 promises.push(matcher.then(onMatch.bind(null, prop, rule))); 358 } 359 } 360 } 361 } 362 363 function processExpandRules(rules) { 364 return new Promise(resolve => { 365 const rule = rules.shift(); 366 if (!rule) { 367 resolve(null); 368 } 369 370 const expandOptions = { 371 rootVariable: gUI.view.getScopeAtIndex(parsed ? 1 : 0), 372 expandTo: rule.name, 373 }; 374 375 variablesViewExpandTo(expandOptions) 376 .then( 377 function onSuccess(prop) { 378 const name = rule.name; 379 const lastName = name.split(".").pop(); 380 rule.name = lastName; 381 382 const matched = matchVariablesViewProperty(prop, rule); 383 return matched 384 .then(onMatch.bind(null, prop, rule)) 385 .then(function () { 386 rule.name = name; 387 }); 388 }, 389 function onFailure() { 390 resolve(null); 391 } 392 ) 393 .then(processExpandRules.bind(null, rules)) 394 .then(function () { 395 resolve(null); 396 }); 397 }); 398 } 399 400 function onAllRulesMatched(rules) { 401 for (const rule of rules) { 402 const matched = rule.matchedProp; 403 if (matched && !rule.dontMatch) { 404 ok(true, "rule " + rule.name + " matched for property " + matched.name); 405 } else if (matched && rule.dontMatch) { 406 ok( 407 false, 408 "rule " + rule.name + " should not match property " + matched.name 409 ); 410 } else { 411 ok(rule.dontMatch, "rule " + rule.name + " did not match any property"); 412 } 413 } 414 return rules; 415 } 416 417 return init(); 418 } 419 420 /** 421 * Check if a given Property object from the variables view matches the given 422 * rule. 423 * 424 * @param object prop 425 * The variable's view Property instance. 426 * @param object rule 427 * Rules for matching the property. See findVariableViewProperties() for 428 * details. 429 * @return object 430 * A promise that is resolved when all the checks complete. Resolution 431 * result is a boolean that tells your promise callback the match 432 * result: true or false. 433 */ 434 function matchVariablesViewProperty(prop, rule) { 435 function resolve(result) { 436 return Promise.resolve(result); 437 } 438 439 if (!prop) { 440 return resolve(false); 441 } 442 443 // Any kind of string is accepted as name, including empty ones 444 if (typeof rule.name == "string") { 445 const match = 446 rule.name instanceof RegExp 447 ? rule.name.test(prop.name) 448 : prop.name == rule.name; 449 if (!match) { 450 return resolve(false); 451 } 452 } 453 454 if ("value" in rule) { 455 let displayValue = prop.displayValue; 456 if (prop.displayValueClassName == "token-string") { 457 displayValue = displayValue.substring(1, displayValue.length - 1); 458 } 459 460 const match = 461 rule.value instanceof RegExp 462 ? rule.value.test(displayValue) 463 : displayValue == rule.value; 464 if (!match) { 465 info( 466 "rule " + 467 rule.name + 468 " did not match value, expected '" + 469 rule.value + 470 "', found '" + 471 displayValue + 472 "'" 473 ); 474 return resolve(false); 475 } 476 } 477 478 return resolve(true); 479 } 480 481 /** 482 * Click selects a row in the table. 483 * 484 * @param {[string]} ids 485 * The array id of the item in the tree 486 */ 487 async function selectTreeItem(ids) { 488 if (gUI.tree.isSelected(ids)) { 489 info(`"${ids}" is already selected, returning.`); 490 return; 491 } 492 if (!gUI.tree.exists(ids)) { 493 info(`"${ids}" does not exist, returning.`); 494 return; 495 } 496 497 // The item exists but is not selected... select it. 498 info(`Selecting "${ids}".`); 499 if (ids.length > 1) { 500 const updated = gUI.once("store-objects-updated"); 501 gUI.tree.selectedItem = ids; 502 await updated; 503 } else { 504 // If the length of the IDs array is 1, a storage type 505 // gets selected and no 'store-objects-updated' event 506 // will be fired in that case. 507 gUI.tree.selectedItem = ids; 508 } 509 } 510 511 /** 512 * Click selects a row in the table. 513 * 514 * @param {string} id 515 * The id of the row in the table widget 516 */ 517 async function selectTableItem(id) { 518 const table = gUI.table; 519 const selector = 520 ".table-widget-column#" + 521 table.uniqueId + 522 " .table-widget-cell[value='" + 523 id + 524 "']"; 525 const target = gPanelWindow.document.querySelector(selector); 526 527 ok(target, `row found with id "${id}"`); 528 529 if (!target) { 530 showAvailableIds(); 531 } 532 533 const updated = gUI.once("sidebar-updated"); 534 535 info(`selecting row "${id}"`); 536 await click(target); 537 await updated; 538 } 539 540 /** 541 * Wait for eventName on target. 542 * 543 * @param {object} target An observable object that either supports on/off or 544 * addEventListener/removeEventListener 545 * @param {string} eventName 546 * @param {boolean} useCapture Optional, for addEventListener/removeEventListener 547 * @return A promise that resolves when the event has been handled 548 */ 549 function once(target, eventName, useCapture = false) { 550 info("Waiting for event: '" + eventName + "' on " + target + "."); 551 552 return new Promise(resolve => { 553 for (const [add, remove] of [ 554 ["addEventListener", "removeEventListener"], 555 ["addListener", "removeListener"], 556 ["on", "off"], 557 ]) { 558 if (add in target && remove in target) { 559 target[add]( 560 eventName, 561 function onEvent(...aArgs) { 562 info("Got event: '" + eventName + "' on " + target + "."); 563 target[remove](eventName, onEvent, useCapture); 564 resolve(...aArgs); 565 }, 566 useCapture 567 ); 568 break; 569 } 570 } 571 }); 572 } 573 574 /** 575 * Get values for a row. 576 * 577 * @param {string} id 578 * The uniqueId of the given row. 579 * @param {boolean} includeHidden 580 * Include hidden columns. 581 * 582 * @return {object} 583 * An object of column names to values for the given row. 584 */ 585 function getRowValues(id, includeHidden = false) { 586 const cells = getRowCells(id, includeHidden); 587 const values = {}; 588 589 for (const name in cells) { 590 const cell = cells[name]; 591 592 values[name] = cell.value; 593 } 594 595 return values; 596 } 597 598 /** 599 * Get the row element for a given id 600 * 601 * @param {string} id 602 * The uniqueId of the given row. 603 * @returns {Element|null} 604 */ 605 function getRowItem(id) { 606 const doc = gPanelWindow.document; 607 const table = gUI.table; 608 return doc.querySelector( 609 `.table-widget-column#${table.uniqueId} .table-widget-cell[value='${id}']` 610 ); 611 } 612 613 /** 614 * Get cells for a row. 615 * 616 * @param {string} id 617 * The uniqueId of the given row. 618 * @param {boolean} includeHidden 619 * Include hidden columns. 620 * 621 * @return {object} 622 * An object of column names to cells for the given row. 623 */ 624 function getRowCells(id, includeHidden = false) { 625 const table = gUI.table; 626 const item = getRowItem(id); 627 628 if (!item) { 629 ok( 630 false, 631 `The row id '${id}' that was passed to getRowCells() does not ` + 632 `exist. ${getAvailableIds()}` 633 ); 634 } 635 636 const index = table.columns.get(table.uniqueId).cellNodes.indexOf(item); 637 const cells = {}; 638 639 for (const [name, column] of [...table.columns]) { 640 if (!includeHidden && column.column.parentNode.hidden) { 641 continue; 642 } 643 cells[name] = column.cellNodes[index]; 644 } 645 646 return cells; 647 } 648 649 /** 650 * Check for an empty table. 651 */ 652 function isTableEmpty() { 653 const doc = gPanelWindow.document; 654 const table = gUI.table; 655 const cells = doc.querySelectorAll( 656 ".table-widget-column#" + table.uniqueId + " .table-widget-cell" 657 ); 658 return cells.length === 0; 659 } 660 661 /** 662 * Get available ids... useful for error reporting. 663 */ 664 function getAvailableIds() { 665 const doc = gPanelWindow.document; 666 const table = gUI.table; 667 668 let out = "Available ids:\n"; 669 const cells = doc.querySelectorAll( 670 ".table-widget-column#" + table.uniqueId + " .table-widget-cell" 671 ); 672 for (const cell of cells) { 673 out += ` - ${cell.getAttribute("value")}\n`; 674 } 675 676 return out; 677 } 678 679 /** 680 * Show available ids. 681 */ 682 function showAvailableIds() { 683 info(getAvailableIds()); 684 } 685 686 /** 687 * Get a cell value. 688 * 689 * @param {string} id 690 * The uniqueId of the row. 691 * @param {string} column 692 * The id of the column 693 * 694 * @yield {string} 695 * The cell value. 696 */ 697 function getCellValue(id, column) { 698 const row = getRowValues(id, true); 699 700 if (typeof row[column] === "undefined") { 701 let out = ""; 702 for (const key in row) { 703 const value = row[key]; 704 705 out += ` - ${key} = ${value}\n`; 706 } 707 708 ok( 709 false, 710 `The column name '${column}' that was passed to ` + 711 `getCellValue() does not exist. Current column names and row ` + 712 `values are:\n${out}` 713 ); 714 } 715 716 return row[column]; 717 } 718 719 /** 720 * Edit a cell value. The cell is assumed to be in edit mode, see startCellEdit. 721 * 722 * @param {string} id 723 * The uniqueId of the row. 724 * @param {string} column 725 * The id of the column 726 * @param {string} newValue 727 * Replacement value. 728 * @param {boolean} validate 729 * Validate result? Default true. 730 * 731 * @yield {string} 732 * The uniqueId of the changed row. 733 */ 734 async function editCell(id, column, newValue, validate = true) { 735 const row = getRowCells(id, true); 736 const editableFieldsEngine = gUI.table._editableFieldsEngine; 737 738 editableFieldsEngine.edit(row[column]); 739 740 await typeWithTerminator(newValue, "KEY_Enter", validate); 741 } 742 743 /** 744 * Begin edit mode for a cell. 745 * 746 * @param {string} id 747 * The uniqueId of the row. 748 * @param {string} column 749 * The id of the column 750 * @param {boolean} selectText 751 * Select text? Default true. 752 */ 753 function startCellEdit(id, column, selectText = true) { 754 const row = getRowCells(id, true); 755 const editableFieldsEngine = gUI.table._editableFieldsEngine; 756 const cell = row[column]; 757 758 info("Selecting row " + id); 759 gUI.table.selectedRow = id; 760 761 info("Starting cell edit (" + id + ", " + column + ")"); 762 editableFieldsEngine.edit(cell); 763 764 if (!selectText) { 765 const textbox = gUI.table._editableFieldsEngine.textbox; 766 textbox.selectionEnd = textbox.selectionStart; 767 } 768 } 769 770 /** 771 * Check a cell value. 772 * 773 * @param {string} id 774 * The uniqueId of the row. 775 * @param {string} column 776 * The id of the column 777 * @param {string} expected 778 * Expected value. 779 */ 780 function checkCell(id, column, expected) { 781 is( 782 getCellValue(id, column), 783 expected, 784 column + " column has the right value for " + id 785 ); 786 } 787 788 /** 789 * Check that a cell is not in edit mode. 790 * 791 * @param {string} id 792 * The uniqueId of the row. 793 * @param {string} column 794 * The id of the column 795 */ 796 function checkCellUneditable(id, column) { 797 const row = getRowCells(id, true); 798 const cell = row[column]; 799 800 const editableFieldsEngine = gUI.table._editableFieldsEngine; 801 const textbox = editableFieldsEngine.textbox; 802 803 // When a field is being edited, the cell is hidden, and the textbox is made visible. 804 ok( 805 !cell.hidden && textbox.hidden, 806 `The cell located in column ${column} and row ${id} is not editable.` 807 ); 808 } 809 810 /** 811 * Show or hide a column. 812 * 813 * @param {string} id 814 * The uniqueId of the given column. 815 * @param {boolean} state 816 * true = show, false = hide 817 */ 818 function showColumn(id, state) { 819 const columns = gUI.table.columns; 820 const column = columns.get(id); 821 column.column.hidden = !state; 822 } 823 824 /** 825 * Toggle sort direction on a column by clicking on the column header. 826 * 827 * @param {string} id 828 * The uniqueId of the given column. 829 */ 830 function clickColumnHeader(id) { 831 const columns = gUI.table.columns; 832 const column = columns.get(id); 833 const header = column.header; 834 835 header.click(); 836 } 837 838 /** 839 * Show or hide all columns. 840 * 841 * @param {boolean} state 842 * true = show, false = hide 843 */ 844 function showAllColumns(state) { 845 const columns = gUI.table.columns; 846 847 for (const [id] of columns) { 848 showColumn(id, state); 849 } 850 } 851 852 /** 853 * Type a string in the currently selected editor and then wait for the row to 854 * be updated. 855 * 856 * @param {string} str 857 * The string to type. 858 * @param {string} terminator 859 * The terminating key e.g. KEY_Enter or KEY_Tab 860 * @param {boolean} validate 861 * Validate result? Default true. 862 */ 863 async function typeWithTerminator(str, terminator, validate = true) { 864 const editableFieldsEngine = gUI.table._editableFieldsEngine; 865 const textbox = editableFieldsEngine.textbox; 866 const colName = textbox.closest(".table-widget-column").id; 867 868 const changeExpected = str !== textbox.value; 869 870 if (!changeExpected) { 871 return editableFieldsEngine.currentTarget.getAttribute("data-id"); 872 } 873 874 info("Typing " + str); 875 EventUtils.sendString(str, gPanelWindow); 876 877 info("Pressing " + terminator); 878 EventUtils.synthesizeKey(terminator, null, gPanelWindow); 879 880 if (validate) { 881 info("Validating results... waiting for ROW_EDIT event."); 882 const uniqueId = await gUI.table.once(TableWidget.EVENTS.ROW_EDIT); 883 884 checkCell(uniqueId, colName, str); 885 return uniqueId; 886 } 887 888 return gUI.table.once(TableWidget.EVENTS.ROW_EDIT); 889 } 890 891 function getCurrentEditorValue() { 892 const editableFieldsEngine = gUI.table._editableFieldsEngine; 893 const textbox = editableFieldsEngine.textbox; 894 895 return textbox.value; 896 } 897 898 /** 899 * Press a key x times. 900 * 901 * @param {string} key 902 * The key to press e.g. VK_RETURN or VK_TAB 903 * @param {number} x 904 * The number of times to press the key. 905 * @param {object} modifiers 906 * The event modifier e.g. {shiftKey: true} 907 */ 908 function PressKeyXTimes(key, x, modifiers = {}) { 909 for (let i = 0; i < x; i++) { 910 EventUtils.synthesizeKey(key, modifiers); 911 } 912 } 913 914 /** 915 * Verify the storage inspector state: check that given type/host exists 916 * in the tree, and that the table contains rows with specified names. 917 * 918 * @param {Array} state Array of state specifications. For example, 919 * [["cookies", "example.com"], ["c1", "c2"]] means to select the 920 * "example.com" host in cookies and then verify there are "c1" and "c2" 921 * cookies (and no other ones). 922 */ 923 async function checkState(state) { 924 for (const [store, names] of state) { 925 const storeName = store.join(" > "); 926 info(`Selecting tree item ${storeName}`); 927 await selectTreeItem(store); 928 929 const items = gUI.table.items; 930 931 is( 932 items.size, 933 names.length, 934 `There is correct number of rows in ${storeName}` 935 ); 936 937 if (names.length === 0) { 938 showAvailableIds(); 939 } 940 941 for (const name of names) { 942 if (!items.has(name)) { 943 showAvailableIds(); 944 } 945 ok(items.has(name), `There is item with name '${name}' in ${storeName}`); 946 } 947 } 948 } 949 950 /** 951 * Checks if document's active element is within the given element. 952 * 953 * @param {HTMLDocument} doc document with active element in question 954 * @param {DOMNode} container element tested on focus containment 955 * @return {boolean} 956 */ 957 function containsFocus(doc, container) { 958 let elm = doc.activeElement; 959 while (elm) { 960 if (elm === container) { 961 return true; 962 } 963 elm = elm.parentNode; 964 } 965 return false; 966 } 967 968 var focusSearchBoxUsingShortcut = async function (panelWin, callback) { 969 info("Focusing search box"); 970 const searchBox = panelWin.document.getElementById("storage-searchbox"); 971 const focused = once(searchBox, "focus"); 972 973 panelWin.focus(); 974 975 const shortcut = 976 await panelWin.document.l10n.formatValue("storage-filter-key"); 977 synthesizeKeyShortcut(shortcut); 978 979 await focused; 980 981 if (callback) { 982 callback(); 983 } 984 }; 985 986 function getCookieId(name, domain, path, partitionKey = "") { 987 return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}${SEPARATOR_GUID}${partitionKey}`; 988 } 989 990 function setPermission(url, permission) { 991 const nsIPermissionManager = Ci.nsIPermissionManager; 992 993 const uri = Services.io.newURI(url); 994 const principal = Services.scriptSecurityManager.createContentPrincipal( 995 uri, 996 {} 997 ); 998 999 Cc["@mozilla.org/permissionmanager;1"] 1000 .getService(nsIPermissionManager) 1001 .addFromPrincipal(principal, permission, nsIPermissionManager.ALLOW_ACTION); 1002 } 1003 1004 function toggleSidebar() { 1005 gUI.sidebarToggleBtn.click(); 1006 } 1007 1008 function sidebarToggleVisible() { 1009 return !gUI.sidebarToggleBtn.hidden; 1010 } 1011 1012 /** 1013 * Check whether the variables view in the sidebar contains a tree. 1014 * 1015 * @param {boolean} state 1016 * Should a tree be visible? 1017 */ 1018 function sidebarParseTreeVisible(state) { 1019 if (state) { 1020 Assert.greater( 1021 gUI.view._testOnlyHierarchy.size, 1022 2, 1023 "Parse tree should be visible." 1024 ); 1025 } else { 1026 Assert.lessOrEqual( 1027 gUI.view._testOnlyHierarchy.size, 1028 2, 1029 "Parse tree should not be visible." 1030 ); 1031 } 1032 } 1033 1034 /** 1035 * Add an item. 1036 * 1037 * @param {Array} store 1038 * An array containing the path to the store to which we wish to add an 1039 * item. 1040 * @return {Promise} A Promise that resolves to the row id of the added item. 1041 */ 1042 async function performAdd(store) { 1043 const storeName = store.join(" > "); 1044 const toolbar = gPanelWindow.document.getElementById("storage-toolbar"); 1045 const type = store[0]; 1046 1047 await selectTreeItem(store); 1048 1049 const menuAdd = toolbar.querySelector("#add-button"); 1050 1051 if (menuAdd.hidden) { 1052 is( 1053 menuAdd.hidden, 1054 false, 1055 `performAdd called for ${storeName} but it is not supported` 1056 ); 1057 return ""; 1058 } 1059 1060 const eventEdit = gUI.table.once("row-edit"); 1061 const eventWait = gUI.once("store-objects-edit"); 1062 1063 menuAdd.click(); 1064 1065 const rowId = await eventEdit; 1066 await eventWait; 1067 1068 const key = type === "cookies" ? "uniqueKey" : "name"; 1069 const value = getCellValue(rowId, key); 1070 1071 is(rowId, value, `Row '${rowId}' was successfully added.`); 1072 1073 return rowId; 1074 } 1075 1076 // Cell css selector that can be used to count or select cells. 1077 // The selector is restricted to a single column to avoid counting duplicates. 1078 const CELL_SELECTOR = 1079 "#storage-table .table-widget-column:first-child .table-widget-cell"; 1080 1081 function getCellLength() { 1082 return gPanelWindow.document.querySelectorAll(CELL_SELECTOR).length; 1083 } 1084 1085 function checkCellLength(len) { 1086 is(getCellLength(), len, `Table should contain ${len} items`); 1087 } 1088 1089 async function scroll() { 1090 const $ = id => gPanelWindow.document.querySelector(id); 1091 const table = $("#storage-table .table-widget-body"); 1092 const cell = $(CELL_SELECTOR); 1093 const cellHeight = cell.getBoundingClientRect().height; 1094 1095 const onStoresUpdate = gUI.once("store-objects-updated"); 1096 table.scrollTop += cellHeight * 50; 1097 await onStoresUpdate; 1098 } 1099 1100 /** 1101 * Asserts that the given tree path exists 1102 * 1103 * @param {Document} doc 1104 * @param {Array} path 1105 * @param {boolean} isExpected 1106 */ 1107 function checkTree(doc, path, isExpected = true) { 1108 const doesExist = isInTree(doc, path); 1109 ok( 1110 isExpected ? doesExist : !doesExist, 1111 `${path.join(" > ")} is ${isExpected ? "" : "not "}in the tree` 1112 ); 1113 } 1114 1115 /** 1116 * Returns whether a tree path exists 1117 * 1118 * @param {Document} doc 1119 * @param {Array} path 1120 */ 1121 function isInTree(doc, path) { 1122 const treeId = JSON.stringify(path); 1123 return !!doc.querySelector(`[data-id='${treeId}']`); 1124 } 1125 1126 /** 1127 * Returns the label of the node for the provided tree path 1128 * 1129 * @param {Document} doc 1130 * @param {Array} path 1131 * @returns {string} 1132 */ 1133 function getTreeNodeLabel(doc, path) { 1134 const treeId = JSON.stringify(path); 1135 return doc.querySelector(`[data-id='${treeId}'] .tree-widget-item`) 1136 .textContent; 1137 } 1138 1139 /** 1140 * Checks that the pair <name, value> is displayed at the data table 1141 * 1142 * @param {string} name 1143 * @param {any} value 1144 */ 1145 function checkStorageData(name, value) { 1146 ok( 1147 hasStorageData(name, value), 1148 `Table row has an entry for: ${name} with value: ${value}` 1149 ); 1150 } 1151 1152 async function waitForStorageData(name, value) { 1153 info("Waiting for data to appear in the table"); 1154 await waitFor(() => hasStorageData(name, value)); 1155 ok(true, `Table row has an entry for: ${name} with value: ${value}`); 1156 } 1157 1158 /** 1159 * Returns whether the pair <name, value> is displayed at the data table 1160 * 1161 * @param {string} name 1162 * @param {any} value 1163 */ 1164 function hasStorageData(name, value) { 1165 return gUI.table.items.get(name)?.value === value; 1166 } 1167 1168 /** 1169 * Returns an URL of a page that uses the document-builder to generate its content 1170 * 1171 * @param {string} domain 1172 * @param {string} html 1173 * @param {string} protocol 1174 */ 1175 function buildURLWithContent(domain, html, protocol = "https") { 1176 return `${protocol}://${domain}/document-builder.sjs?html=${encodeURI(html)}`; 1177 } 1178 1179 /** 1180 * Asserts that the given cookie holds the provided value in the data table 1181 * 1182 * @param {string} name 1183 * @param {string} value 1184 */ 1185 function checkCookieData(name, value) { 1186 ok( 1187 hasCookieData(name, value), 1188 `Table row has an entry for: ${name} with value: ${value}` 1189 ); 1190 } 1191 1192 /** 1193 * Returns whether the given cookie holds the provided value in the data table 1194 * 1195 * @param {string} name 1196 * @param {string} value 1197 */ 1198 function hasCookieData(name, value) { 1199 const rows = Array.from(gUI.table.items); 1200 const cookie = rows.map(([, data]) => data).find(x => x.name === name); 1201 1202 info(`found ${cookie?.value}`); 1203 return cookie?.value === value; 1204 }