diff options
Diffstat (limited to 'devtools/client/storage/test/head.js')
-rw-r--r-- | devtools/client/storage/test/head.js | 1186 |
1 files changed, 1186 insertions, 0 deletions
diff --git a/devtools/client/storage/test/head.js b/devtools/client/storage/test/head.js new file mode 100644 index 0000000000..f50f573342 --- /dev/null +++ b/devtools/client/storage/test/head.js @@ -0,0 +1,1186 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ +/* import-globals-from ../../shared/test/shared-head.js */ + +// Sometimes HTML pages have a `clear` function that cleans up the storage they +// created. To make sure it's always called, we are registering as a cleanup +// function, but since this needs to run before tabs are closed, we need to +// do this registration before importing `shared-head`, since declaration +// order matters. +registerCleanupFunction(async () => { + const browser = gBrowser.selectedBrowser; + const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree(); + for (const context of contexts) { + await SpecialPowers.spawn(context, [], async () => { + const win = content.wrappedJSObject; + + // Some windows (e.g., about: URLs) don't have storage available + try { + win.localStorage.clear(); + win.sessionStorage.clear(); + } catch (ex) { + // ignore + } + + if (win.clear) { + // Do not get hung into win.clear() forever + await Promise.race([ + new Promise(r => win.setTimeout(r, 10000)), + win.clear(), + ]); + } + }); + } + + Services.cookies.removeAll(); + + // Close tabs and force memory collection to happen + while (gBrowser.tabs.length > 1) { + await closeTabAndToolbox(gBrowser.selectedTab); + } + forceCollections(); +}); + +// shared-head.js handles imports, constants, and utility functions +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this +); + +const { + TableWidget, +} = require("resource://devtools/client/shared/widgets/TableWidget.js"); +const { + LocalTabCommandsFactory, +} = require("resource://devtools/client/framework/local-tab-commands-factory.js"); +const STORAGE_PREF = "devtools.storage.enabled"; +const DUMPEMIT_PREF = "devtools.dump.emit"; +const DEBUGGERLOG_PREF = "devtools.debugger.log"; + +// Allows Cache API to be working on usage `http` test page +const CACHES_ON_HTTP_PREF = "dom.caches.testing.enabled"; +const PATH = "browser/devtools/client/storage/test/"; +const MAIN_DOMAIN = "http://test1.example.org/" + PATH; +const MAIN_DOMAIN_SECURED = "https://test1.example.org/" + PATH; +const MAIN_DOMAIN_WITH_PORT = "http://test1.example.org:8000/" + PATH; +const ALT_DOMAIN = "http://sectest1.example.org/" + PATH; +const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH; + +// GUID to be used as a separator in compound keys. This must match the same +// constant in devtools/server/actors/resources/storage/index.js, +// devtools/client/storage/ui.js and devtools/server/tests/browser/head.js +const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}"; + +var gToolbox, gPanelWindow, gUI; + +// Services.prefs.setBoolPref(DUMPEMIT_PREF, true); +// Services.prefs.setBoolPref(DEBUGGERLOG_PREF, true); + +Services.prefs.setBoolPref(STORAGE_PREF, true); +Services.prefs.setBoolPref(CACHES_ON_HTTP_PREF, true); +registerCleanupFunction(() => { + gToolbox = gPanelWindow = gUI = null; + Services.prefs.clearUserPref(CACHES_ON_HTTP_PREF); + Services.prefs.clearUserPref(DEBUGGERLOG_PREF); + Services.prefs.clearUserPref(DUMPEMIT_PREF); + Services.prefs.clearUserPref(STORAGE_PREF); +}); + +/** + * This generator function opens the given url in a new tab, then sets up the + * page by waiting for all cookies, indexedDB items etc. + * + * @param url {String} The url to be opened in the new tab + * @param options {Object} The tab options for the new tab + * + * @return {Promise} A promise that resolves after the tab is ready + */ +async function openTab(url, options = {}) { + const tab = await addTab(url, options); + + const browser = gBrowser.selectedBrowser; + const contexts = browser.browsingContext.getAllBrowsingContextsInSubtree(); + + for (const context of contexts) { + await SpecialPowers.spawn(context, [], async () => { + const win = content.wrappedJSObject; + const readyState = win.document.readyState; + info(`Found a window: ${readyState}`); + if (readyState != "complete") { + await new Promise(resolve => { + const onLoad = () => { + win.removeEventListener("load", onLoad); + resolve(); + }; + win.addEventListener("load", onLoad); + }); + } + if (win.setup) { + await win.setup(); + } + }); + } + + return tab; +} + +/** + * This generator function opens the given url in a new tab, then sets up the + * page by waiting for all cookies, indexedDB items etc. to be created; Then + * opens the storage inspector and waits for the storage tree and table to be + * populated. + * + * @param url {String} The url to be opened in the new tab + * @param options {Object} The tab options for the new tab + * + * @return {Promise} A promise that resolves after storage inspector is ready + */ +async function openTabAndSetupStorage(url, options = {}) { + // open tab + await openTab(url, options); + + // open storage inspector + return openStoragePanel(); +} + +/** + * Open a toolbox with the storage panel opened by default + * for a given Web Extension. + * + * @param {String} addonId + * The ID of the Web Extension to debug. + */ +var openStoragePanelForAddon = async function (addonId) { + const toolbox = await gDevTools.showToolboxForWebExtension(addonId, { + toolId: "storage", + }); + + info("Making sure that the toolbox's frame is focused"); + await SimpleTest.promiseFocus(toolbox.win); + + const storage = _setupStoragePanelForTest(toolbox); + + return { + toolbox, + storage, + }; +}; + +/** + * Open the toolbox, with the storage tool visible. + * + * @param tab {XULTab} Optional, the tab for the toolbox; defaults to selected tab + * @param commands {Object} Optional, the commands for the toolbox; defaults to a tab commands + * @param hostType {Toolbox.HostType} Optional, type of host that will host the toolbox + * + * @return {Promise} a promise that resolves when the storage inspector is ready + */ +var openStoragePanel = async function ({ tab, hostType } = {}) { + const toolbox = await openToolboxForTab( + tab || gBrowser.selectedTab, + "storage", + hostType + ); + + const storage = _setupStoragePanelForTest(toolbox); + + return { + toolbox, + storage, + }; +}; + +/** + * Set global variables needed in helper functions + * + * @param toolbox {Toolbox} + * @return {StoragePanel} + */ +function _setupStoragePanelForTest(toolbox) { + const storage = toolbox.getPanel("storage"); + gPanelWindow = storage.panelWindow; + gUI = storage.UI; + gToolbox = toolbox; + + // The table animation flash causes some timeouts on Linux debug tests, + // so we disable it + gUI.animationsEnabled = false; + + return storage; +} + +/** + * Forces GC, CC and Shrinking GC to get rid of disconnected docshells and + * windows. + */ +function forceCollections() { + Cu.forceGC(); + Cu.forceCC(); + Cu.forceShrinkingGC(); +} + +// Sends a click event on the passed DOM node in an async manner +function click(node) { + node.scrollIntoView(); + + return new Promise(resolve => { + // We need setTimeout here to allow any scrolling to complete before clicking + // the node. + setTimeout(() => { + node.click(); + resolve(); + }, 200); + }); +} + +/** + * Recursively expand the variables view up to a given property. + * + * @param options + * Options for view expansion: + * - rootVariable: start from the given scope/variable/property. + * - expandTo: string made up of property names you want to expand. + * For example: "body.firstChild.nextSibling" given |rootVariable: + * document|. + * @return object + * A promise that is resolved only when the last property in |expandTo| + * is found, and rejected otherwise. Resolution reason is always the + * last property - |nextSibling| in the example above. Rejection is + * always the last property that was found. + */ +function variablesViewExpandTo(options) { + const root = options.rootVariable; + const expandTo = options.expandTo.split("."); + + return new Promise((resolve, reject) => { + function getNext(prop) { + const name = expandTo.shift(); + const newProp = prop.get(name); + + if (expandTo.length) { + ok(newProp, "found property " + name); + if (newProp && newProp.expand) { + newProp.expand(); + getNext(newProp); + } else { + reject(prop); + } + } else if (newProp) { + resolve(newProp); + } else { + reject(prop); + } + } + + if (root && root.expand) { + root.expand(); + getNext(root); + } else { + resolve(root); + } + }); +} + +/** + * Find variables or properties in a VariablesView instance. + * + * @param array ruleArray + * The array of rules you want to match. Each rule is an object with: + * - name (string|regexp): property name to match. + * - value (string|regexp): property value to match. + * - dontMatch (boolean): make sure the rule doesn't match any property. + * @param boolean parsed + * true if we want to test the rules in the parse value section of the + * storage sidebar + * @return object + * A promise object that is resolved when all the rules complete + * matching. The resolved callback is given an array of all the rules + * you wanted to check. Each rule has a new property: |matchedProp| + * which holds a reference to the Property object instance from the + * VariablesView. If the rule did not match, then |matchedProp| is + * undefined. + */ +function findVariableViewProperties(ruleArray, parsed) { + // Initialize the search. + function init() { + // If parsed is true, we are checking rules in the parsed value section of + // the storage sidebar. That scope uses a blank variable as a placeholder + // Thus, adding a blank parent to each name + if (parsed) { + ruleArray = ruleArray.map(({ name, value, dontMatch }) => { + return { name: "." + name, value, dontMatch }; + }); + } + // Separate out the rules that require expanding properties throughout the + // view. + const expandRules = []; + const rules = ruleArray.filter(rule => { + if (typeof rule.name == "string" && rule.name.indexOf(".") > -1) { + expandRules.push(rule); + return false; + } + return true; + }); + + // Search through the view those rules that do not require any properties to + // be expanded. Build the array of matchers, outstanding promises to be + // resolved. + const outstanding = []; + + finder(rules, gUI.view, outstanding); + + // Process the rules that need to expand properties. + const lastStep = processExpandRules.bind(null, expandRules); + + // Return the results - a promise resolved to hold the updated ruleArray. + const returnResults = onAllRulesMatched.bind(null, ruleArray); + + return Promise.all(outstanding).then(lastStep).then(returnResults); + } + + function onMatch(prop, rule, matched) { + if (matched && !rule.matchedProp) { + rule.matchedProp = prop; + } + } + + function finder(rules, view, promises) { + for (const scope of view) { + for (const [, prop] of scope) { + for (const rule of rules) { + const matcher = matchVariablesViewProperty(prop, rule); + promises.push(matcher.then(onMatch.bind(null, prop, rule))); + } + } + } + } + + function processExpandRules(rules) { + return new Promise(resolve => { + const rule = rules.shift(); + if (!rule) { + resolve(null); + } + + const expandOptions = { + rootVariable: gUI.view.getScopeAtIndex(parsed ? 1 : 0), + expandTo: rule.name, + }; + + variablesViewExpandTo(expandOptions) + .then( + function onSuccess(prop) { + const name = rule.name; + const lastName = name.split(".").pop(); + rule.name = lastName; + + const matched = matchVariablesViewProperty(prop, rule); + return matched + .then(onMatch.bind(null, prop, rule)) + .then(function () { + rule.name = name; + }); + }, + function onFailure() { + resolve(null); + } + ) + .then(processExpandRules.bind(null, rules)) + .then(function () { + resolve(null); + }); + }); + } + + function onAllRulesMatched(rules) { + for (const rule of rules) { + const matched = rule.matchedProp; + if (matched && !rule.dontMatch) { + ok(true, "rule " + rule.name + " matched for property " + matched.name); + } else if (matched && rule.dontMatch) { + ok( + false, + "rule " + rule.name + " should not match property " + matched.name + ); + } else { + ok(rule.dontMatch, "rule " + rule.name + " did not match any property"); + } + } + return rules; + } + + return init(); +} + +/** + * Check if a given Property object from the variables view matches the given + * rule. + * + * @param object prop + * The variable's view Property instance. + * @param object rule + * Rules for matching the property. See findVariableViewProperties() for + * details. + * @return object + * A promise that is resolved when all the checks complete. Resolution + * result is a boolean that tells your promise callback the match + * result: true or false. + */ +function matchVariablesViewProperty(prop, rule) { + function resolve(result) { + return Promise.resolve(result); + } + + if (!prop) { + return resolve(false); + } + + // Any kind of string is accepted as name, including empty ones + if (typeof rule.name == "string") { + const match = + rule.name instanceof RegExp + ? rule.name.test(prop.name) + : prop.name == rule.name; + if (!match) { + return resolve(false); + } + } + + if ("value" in rule) { + let displayValue = prop.displayValue; + if (prop.displayValueClassName == "token-string") { + displayValue = displayValue.substring(1, displayValue.length - 1); + } + + const match = + rule.value instanceof RegExp + ? rule.value.test(displayValue) + : displayValue == rule.value; + if (!match) { + info( + "rule " + + rule.name + + " did not match value, expected '" + + rule.value + + "', found '" + + displayValue + + "'" + ); + return resolve(false); + } + } + + return resolve(true); +} + +/** + * Click selects a row in the table. + * + * @param {[String]} ids + * The array id of the item in the tree + */ +async function selectTreeItem(ids) { + if (gUI.tree.isSelected(ids)) { + info(`"${ids}" is already selected, returning.`); + return; + } + if (!gUI.tree.exists(ids)) { + info(`"${ids}" does not exist, returning.`); + return; + } + + // The item exists but is not selected... select it. + info(`Selecting "${ids}".`); + if (ids.length > 1) { + const updated = gUI.once("store-objects-updated"); + gUI.tree.selectedItem = ids; + await updated; + } else { + // If the length of the IDs array is 1, a storage type + // gets selected and no 'store-objects-updated' event + // will be fired in that case. + gUI.tree.selectedItem = ids; + } +} + +/** + * Click selects a row in the table. + * + * @param {String} id + * The id of the row in the table widget + */ +async function selectTableItem(id) { + const table = gUI.table; + const selector = + ".table-widget-column#" + + table.uniqueId + + " .table-widget-cell[value='" + + id + + "']"; + const target = gPanelWindow.document.querySelector(selector); + + ok(target, `row found with id "${id}"`); + + if (!target) { + showAvailableIds(); + } + + const updated = gUI.once("sidebar-updated"); + + info(`selecting row "${id}"`); + await click(target); + await updated; +} + +/** + * Wait for eventName on target. + * @param {Object} target An observable object that either supports on/off or + * addEventListener/removeEventListener + * @param {String} eventName + * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener + * @return A promise that resolves when the event has been handled + */ +function once(target, eventName, useCapture = false) { + info("Waiting for event: '" + eventName + "' on " + target + "."); + + return new Promise(resolve => { + for (const [add, remove] of [ + ["addEventListener", "removeEventListener"], + ["addListener", "removeListener"], + ["on", "off"], + ]) { + if (add in target && remove in target) { + target[add]( + eventName, + function onEvent(...aArgs) { + info("Got event: '" + eventName + "' on " + target + "."); + target[remove](eventName, onEvent, useCapture); + resolve(...aArgs); + }, + useCapture + ); + break; + } + } + }); +} + +/** + * Get values for a row. + * + * @param {String} id + * The uniqueId of the given row. + * @param {Boolean} includeHidden + * Include hidden columns. + * + * @return {Object} + * An object of column names to values for the given row. + */ +function getRowValues(id, includeHidden = false) { + const cells = getRowCells(id, includeHidden); + const values = {}; + + for (const name in cells) { + const cell = cells[name]; + + values[name] = cell.value; + } + + return values; +} + +/** + * Get cells for a row. + * + * @param {String} id + * The uniqueId of the given row. + * @param {Boolean} includeHidden + * Include hidden columns. + * + * @return {Object} + * An object of column names to cells for the given row. + */ +function getRowCells(id, includeHidden = false) { + const doc = gPanelWindow.document; + const table = gUI.table; + const item = doc.querySelector( + ".table-widget-column#" + + table.uniqueId + + " .table-widget-cell[value='" + + id + + "']" + ); + + if (!item) { + ok( + false, + `The row id '${id}' that was passed to getRowCells() does not ` + + `exist. ${getAvailableIds()}` + ); + } + + const index = table.columns.get(table.uniqueId).cellNodes.indexOf(item); + const cells = {}; + + for (const [name, column] of [...table.columns]) { + if (!includeHidden && column.column.parentNode.hidden) { + continue; + } + cells[name] = column.cellNodes[index]; + } + + return cells; +} + +/** + * Check for an empty table. + */ +function isTableEmpty() { + const doc = gPanelWindow.document; + const table = gUI.table; + const cells = doc.querySelectorAll( + ".table-widget-column#" + table.uniqueId + " .table-widget-cell" + ); + return cells.length === 0; +} + +/** + * Get available ids... useful for error reporting. + */ +function getAvailableIds() { + const doc = gPanelWindow.document; + const table = gUI.table; + + let out = "Available ids:\n"; + const cells = doc.querySelectorAll( + ".table-widget-column#" + table.uniqueId + " .table-widget-cell" + ); + for (const cell of cells) { + out += ` - ${cell.getAttribute("value")}\n`; + } + + return out; +} + +/** + * Show available ids. + */ +function showAvailableIds() { + info(getAvailableIds()); +} + +/** + * Get a cell value. + * + * @param {String} id + * The uniqueId of the row. + * @param {String} column + * The id of the column + * + * @yield {String} + * The cell value. + */ +function getCellValue(id, column) { + const row = getRowValues(id, true); + + if (typeof row[column] === "undefined") { + let out = ""; + for (const key in row) { + const value = row[key]; + + out += ` - ${key} = ${value}\n`; + } + + ok( + false, + `The column name '${column}' that was passed to ` + + `getCellValue() does not exist. Current column names and row ` + + `values are:\n${out}` + ); + } + + return row[column]; +} + +/** + * Edit a cell value. The cell is assumed to be in edit mode, see startCellEdit. + * + * @param {String} id + * The uniqueId of the row. + * @param {String} column + * The id of the column + * @param {String} newValue + * Replacement value. + * @param {Boolean} validate + * Validate result? Default true. + * + * @yield {String} + * The uniqueId of the changed row. + */ +async function editCell(id, column, newValue, validate = true) { + const row = getRowCells(id, true); + const editableFieldsEngine = gUI.table._editableFieldsEngine; + + editableFieldsEngine.edit(row[column]); + + await typeWithTerminator(newValue, "KEY_Enter", validate); +} + +/** + * Begin edit mode for a cell. + * + * @param {String} id + * The uniqueId of the row. + * @param {String} column + * The id of the column + * @param {Boolean} selectText + * Select text? Default true. + */ +function startCellEdit(id, column, selectText = true) { + const row = getRowCells(id, true); + const editableFieldsEngine = gUI.table._editableFieldsEngine; + const cell = row[column]; + + info("Selecting row " + id); + gUI.table.selectedRow = id; + + info("Starting cell edit (" + id + ", " + column + ")"); + editableFieldsEngine.edit(cell); + + if (!selectText) { + const textbox = gUI.table._editableFieldsEngine.textbox; + textbox.selectionEnd = textbox.selectionStart; + } +} + +/** + * Check a cell value. + * + * @param {String} id + * The uniqueId of the row. + * @param {String} column + * The id of the column + * @param {String} expected + * Expected value. + */ +function checkCell(id, column, expected) { + is( + getCellValue(id, column), + expected, + column + " column has the right value for " + id + ); +} + +/** + * Check that a cell is not in edit mode. + * + * @param {String} id + * The uniqueId of the row. + * @param {String} column + * The id of the column + */ +function checkCellUneditable(id, column) { + const row = getRowCells(id, true); + const cell = row[column]; + + const editableFieldsEngine = gUI.table._editableFieldsEngine; + const textbox = editableFieldsEngine.textbox; + + // When a field is being edited, the cell is hidden, and the textbox is made visible. + ok( + !cell.hidden && textbox.hidden, + `The cell located in column ${column} and row ${id} is not editable.` + ); +} + +/** + * Show or hide a column. + * + * @param {String} id + * The uniqueId of the given column. + * @param {Boolean} state + * true = show, false = hide + */ +function showColumn(id, state) { + const columns = gUI.table.columns; + const column = columns.get(id); + column.column.hidden = !state; +} + +/** + * Toggle sort direction on a column by clicking on the column header. + * + * @param {String} id + * The uniqueId of the given column. + */ +function clickColumnHeader(id) { + const columns = gUI.table.columns; + const column = columns.get(id); + const header = column.header; + + header.click(); +} + +/** + * Show or hide all columns. + * + * @param {Boolean} state + * true = show, false = hide + */ +function showAllColumns(state) { + const columns = gUI.table.columns; + + for (const [id] of columns) { + showColumn(id, state); + } +} + +/** + * Type a string in the currently selected editor and then wait for the row to + * be updated. + * + * @param {String} str + * The string to type. + * @param {String} terminator + * The terminating key e.g. KEY_Enter or KEY_Tab + * @param {Boolean} validate + * Validate result? Default true. + */ +async function typeWithTerminator(str, terminator, validate = true) { + const editableFieldsEngine = gUI.table._editableFieldsEngine; + const textbox = editableFieldsEngine.textbox; + const colName = textbox.closest(".table-widget-column").id; + + const changeExpected = str !== textbox.value; + + if (!changeExpected) { + return editableFieldsEngine.currentTarget.getAttribute("data-id"); + } + + info("Typing " + str); + EventUtils.sendString(str, gPanelWindow); + + info("Pressing " + terminator); + EventUtils.synthesizeKey(terminator, null, gPanelWindow); + + if (validate) { + info("Validating results... waiting for ROW_EDIT event."); + const uniqueId = await gUI.table.once(TableWidget.EVENTS.ROW_EDIT); + + checkCell(uniqueId, colName, str); + return uniqueId; + } + + return gUI.table.once(TableWidget.EVENTS.ROW_EDIT); +} + +function getCurrentEditorValue() { + const editableFieldsEngine = gUI.table._editableFieldsEngine; + const textbox = editableFieldsEngine.textbox; + + return textbox.value; +} + +/** + * Press a key x times. + * + * @param {String} key + * The key to press e.g. VK_RETURN or VK_TAB + * @param {Number} x + * The number of times to press the key. + * @param {Object} modifiers + * The event modifier e.g. {shiftKey: true} + */ +function PressKeyXTimes(key, x, modifiers = {}) { + for (let i = 0; i < x; i++) { + EventUtils.synthesizeKey(key, modifiers); + } +} + +/** + * Verify the storage inspector state: check that given type/host exists + * in the tree, and that the table contains rows with specified names. + * + * @param {Array} state Array of state specifications. For example, + * [["cookies", "example.com"], ["c1", "c2"]] means to select the + * "example.com" host in cookies and then verify there are "c1" and "c2" + * cookies (and no other ones). + */ +async function checkState(state) { + for (const [store, names] of state) { + const storeName = store.join(" > "); + info(`Selecting tree item ${storeName}`); + await selectTreeItem(store); + + const items = gUI.table.items; + + is( + items.size, + names.length, + `There is correct number of rows in ${storeName}` + ); + + if (names.length === 0) { + showAvailableIds(); + } + + for (const name of names) { + if (!items.has(name)) { + showAvailableIds(); + } + ok(items.has(name), `There is item with name '${name}' in ${storeName}`); + } + } +} + +/** + * Checks if document's active element is within the given element. + * @param {HTMLDocument} doc document with active element in question + * @param {DOMNode} container element tested on focus containment + * @return {Boolean} + */ +function containsFocus(doc, container) { + let elm = doc.activeElement; + while (elm) { + if (elm === container) { + return true; + } + elm = elm.parentNode; + } + return false; +} + +var focusSearchBoxUsingShortcut = async function (panelWin, callback) { + info("Focusing search box"); + const searchBox = panelWin.document.getElementById("storage-searchbox"); + const focused = once(searchBox, "focus"); + + panelWin.focus(); + + const shortcut = await panelWin.document.l10n.formatValue( + "storage-filter-key" + ); + synthesizeKeyShortcut(shortcut); + + await focused; + + if (callback) { + callback(); + } +}; + +function getCookieId(name, domain, path) { + return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`; +} + +function setPermission(url, permission) { + const nsIPermissionManager = Ci.nsIPermissionManager; + + const uri = Services.io.newURI(url); + const principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + + Cc["@mozilla.org/permissionmanager;1"] + .getService(nsIPermissionManager) + .addFromPrincipal(principal, permission, nsIPermissionManager.ALLOW_ACTION); +} + +function toggleSidebar() { + gUI.sidebarToggleBtn.click(); +} + +function sidebarToggleVisible() { + return !gUI.sidebarToggleBtn.hidden; +} + +/** + * Check whether the variables view in the sidebar contains a tree. + * + * @param {Boolean} state + * Should a tree be visible? + */ +function sidebarParseTreeVisible(state) { + if (state) { + Assert.greater( + gUI.view._currHierarchy.size, + 2, + "Parse tree should be visible." + ); + } else { + Assert.lessOrEqual( + gUI.view._currHierarchy.size, + 2, + "Parse tree should not be visible." + ); + } +} + +/** + * Add an item. + * @param {Array} store + * An array containing the path to the store to which we wish to add an + * item. + * @return {Promise} A Promise that resolves to the row id of the added item. + */ +async function performAdd(store) { + const storeName = store.join(" > "); + const toolbar = gPanelWindow.document.getElementById("storage-toolbar"); + const type = store[0]; + + await selectTreeItem(store); + + const menuAdd = toolbar.querySelector("#add-button"); + + if (menuAdd.hidden) { + is( + menuAdd.hidden, + false, + `performAdd called for ${storeName} but it is not supported` + ); + return ""; + } + + const eventEdit = gUI.table.once("row-edit"); + const eventWait = gUI.once("store-objects-edit"); + + menuAdd.click(); + + const rowId = await eventEdit; + await eventWait; + + const key = type === "cookies" ? "uniqueKey" : "name"; + const value = getCellValue(rowId, key); + + is(rowId, value, `Row '${rowId}' was successfully added.`); + + return rowId; +} + +// Cell css selector that can be used to count or select cells. +// The selector is restricted to a single column to avoid counting duplicates. +const CELL_SELECTOR = + "#storage-table .table-widget-column:first-child .table-widget-cell"; + +function getCellLength() { + return gPanelWindow.document.querySelectorAll(CELL_SELECTOR).length; +} + +function checkCellLength(len) { + is(getCellLength(), len, `Table should contain ${len} items`); +} + +async function scroll() { + const $ = id => gPanelWindow.document.querySelector(id); + const table = $("#storage-table .table-widget-body"); + const cell = $(CELL_SELECTOR); + const cellHeight = cell.getBoundingClientRect().height; + + const onStoresUpdate = gUI.once("store-objects-updated"); + table.scrollTop += cellHeight * 50; + await onStoresUpdate; +} + +/** + * Asserts that the given tree path exists + * @param {Document} doc + * @param {Array} path + * @param {Boolean} isExpected + */ +function checkTree(doc, path, isExpected = true) { + const doesExist = isInTree(doc, path); + ok( + isExpected ? doesExist : !doesExist, + `${path.join(" > ")} is ${isExpected ? "" : "not "}in the tree` + ); +} + +/** + * Returns whether a tree path exists + * @param {Document} doc + * @param {Array} path + */ +function isInTree(doc, path) { + const treeId = JSON.stringify(path); + return !!doc.querySelector(`[data-id='${treeId}']`); +} + +/** + * Returns the label of the node for the provided tree path + * @param {Document} doc + * @param {Array} path + * @returns {String} + */ +function getTreeNodeLabel(doc, path) { + const treeId = JSON.stringify(path); + return doc.querySelector(`[data-id='${treeId}'] .tree-widget-item`) + .textContent; +} + +/** + * Checks that the pair <name, value> is displayed at the data table + * @param {String} name + * @param {any} value + */ +function checkStorageData(name, value) { + ok( + hasStorageData(name, value), + `Table row has an entry for: ${name} with value: ${value}` + ); +} + +async function waitForStorageData(name, value) { + info("Waiting for data to appear in the table"); + await waitFor(() => hasStorageData(name, value)); + ok(true, `Table row has an entry for: ${name} with value: ${value}`); +} + +/** + * Returns whether the pair <name, value> is displayed at the data table + * @param {String} name + * @param {any} value + */ +function hasStorageData(name, value) { + return gUI.table.items.get(name)?.value === value; +} + +/** + * Returns an URL of a page that uses the document-builder to generate its content + * @param {String} domain + * @param {String} html + * @param {String} protocol + */ +function buildURLWithContent(domain, html, protocol = "https") { + return `${protocol}://${domain}/document-builder.sjs?html=${encodeURI(html)}`; +} + +/** + * Asserts that the given cookie holds the provided value in the data table + * @param {String} name + * @param {String} value + */ +function checkCookieData(name, value) { + ok( + hasCookieData(name, value), + `Table row has an entry for: ${name} with value: ${value}` + ); +} + +/** + * Returns whether the given cookie holds the provided value in the data table + * @param {String} name + * @param {String} value + */ +function hasCookieData(name, value) { + const rows = Array.from(gUI.table.items); + const cookie = rows.map(([, data]) => data).find(x => x.name === name); + + info(`found ${cookie?.value}`); + return cookie?.value === value; +} |