1185 lines
32 KiB
JavaScript
1185 lines
32 KiB
JavaScript
/* 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 () => {
|
|
Services.cookies.removeAll();
|
|
|
|
// Close tabs and force memory collection to happen
|
|
while (gBrowser.tabs.length > 1) {
|
|
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(),
|
|
]);
|
|
}
|
|
});
|
|
}
|
|
|
|
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, partitionKey = "") {
|
|
return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}${SEPARATOR_GUID}${partitionKey}`;
|
|
}
|
|
|
|
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._testOnlyHierarchy.size,
|
|
2,
|
|
"Parse tree should be visible."
|
|
);
|
|
} else {
|
|
Assert.lessOrEqual(
|
|
gUI.view._testOnlyHierarchy.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;
|
|
}
|