gecko-dev/devtools/client/storage/test/head.js
Tim Huang 90ac2e75ba Bug 1263324 - Part 2: Add test cases to test context id for devtools storage inspector. r=miker
--HG--
extra : rebase_source : 35f785c38a6ea50ad0dd2d67fbe2771bcb65d8d2
2017-01-24 14:45:42 +08:00

922 lines
25 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 ../../framework/test/shared-head.js */
// shared-head.js handles imports, constants, and utility functions
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
this);
const {TableWidget} = require("devtools/client/shared/widgets/TableWidget");
const SPLIT_CONSOLE_PREF = "devtools.toolbox.splitconsoleEnabled";
const STORAGE_PREF = "devtools.storage.enabled";
const DOM_CACHE = "dom.caches.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 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/storage.js,
// devtools/client/storage/ui.js and devtools/server/tests/browser/head.js
const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}";
var gToolbox, gPanelWindow, gWindow, 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 = gWindow = gUI = null;
Services.prefs.clearUserPref(CACHES_ON_HTTP_PREF);
Services.prefs.clearUserPref(DEBUGGERLOG_PREF);
Services.prefs.clearUserPref(DOM_CACHE);
Services.prefs.clearUserPref(DUMPEMIT_PREF);
Services.prefs.clearUserPref(SPLIT_CONSOLE_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
*/
function* openTab(url, options = {}) {
let tab = yield addTab(url, options);
let content = tab.linkedBrowser.contentWindow;
gWindow = content.wrappedJSObject;
// Setup the async storages in main window and for all its iframes
yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
/**
* Get all windows including frames recursively.
*
* @param {Window} [baseWindow]
* The base window at which to start looking for child windows
* (optional).
* @return {Set}
* A set of windows.
*/
function getAllWindows(baseWindow) {
let windows = new Set();
let _getAllWindows = function (win) {
windows.add(win.wrappedJSObject);
for (let i = 0; i < win.length; i++) {
_getAllWindows(win[i]);
}
};
_getAllWindows(baseWindow);
return windows;
}
let windows = getAllWindows(content);
for (let win of windows) {
let readyState = win.document.readyState;
info(`Found a window: ${readyState}`);
if (readyState != "complete") {
yield new Promise(resolve => {
let onLoad = () => {
win.removeEventListener("load", onLoad);
resolve();
};
win.addEventListener("load", onLoad);
});
}
if (win.setup) {
yield 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
*/
function* openTabAndSetupStorage(url, options = {}) {
// open tab
yield openTab(url, options);
// open storage inspector
return yield openStoragePanel();
}
/**
* Open the toolbox, with the storage tool visible.
*
* @param cb {Function} Optional callback, if you don't want to use the returned
* promise
*
* @return {Promise} a promise that resolves when the storage inspector is ready
*/
var openStoragePanel = Task.async(function* (cb) {
info("Opening the storage inspector");
let target = TargetFactory.forTab(gBrowser.selectedTab);
let storage, toolbox;
// Checking if the toolbox and the storage are already loaded
// The storage-updated event should only be waited for if the storage
// isn't loaded yet
toolbox = gDevTools.getToolbox(target);
if (toolbox) {
storage = toolbox.getPanel("storage");
if (storage) {
gPanelWindow = storage.panelWindow;
gUI = storage.UI;
gToolbox = toolbox;
info("Toolbox and storage already open");
if (cb) {
return cb(storage, toolbox);
}
return {
toolbox: toolbox,
storage: storage
};
}
}
info("Opening the toolbox");
toolbox = yield gDevTools.showToolbox(target, "storage");
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;
info("Waiting for the stores to update");
yield gUI.once("store-objects-updated");
yield waitForToolboxFrameFocus(toolbox);
if (cb) {
return cb(storage, toolbox);
}
return {
toolbox: toolbox,
storage: storage
};
});
/**
* Wait for the toolbox frame to receive focus after it loads
*
* @param toolbox {Toolbox}
*
* @return a promise that resolves when focus has been received
*/
function waitForToolboxFrameFocus(toolbox) {
info("Making sure that the toolbox's frame is focused");
let def = promise.defer();
waitForFocus(def.resolve, toolbox.win);
return def.promise;
}
/**
* Forces GC, CC and Shrinking GC to get rid of disconnected docshells and
* windows.
*/
function forceCollections() {
Cu.forceGC();
Cu.forceCC();
Cu.forceShrinkingGC();
}
/**
* Cleans up and finishes the test
*/
function* finishTests() {
// Bug 1233497 makes it so that we can no longer yield CPOWs from Tasks.
// We work around this by calling clear() via a ContentTask instead.
while (gBrowser.tabs.length > 1) {
yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () {
/**
* Get all windows including frames recursively.
*
* @param {Window} [baseWindow]
* The base window at which to start looking for child windows
* (optional).
* @return {Set}
* A set of windows.
*/
function getAllWindows(baseWindow) {
let windows = new Set();
let _getAllWindows = function (win) {
windows.add(win.wrappedJSObject);
for (let i = 0; i < win.length; i++) {
_getAllWindows(win[i]);
}
};
_getAllWindows(baseWindow);
return windows;
}
let windows = getAllWindows(content);
for (let win of windows) {
// Some windows (e.g., about: URLs) don't have storage available
try {
win.localStorage.clear();
win.sessionStorage.clear();
} catch (ex) {
// ignore
}
if (win.clear) {
yield win.clear();
}
}
});
yield closeTabAndToolbox(gBrowser.selectedTab);
}
Services.cookies.removeAll();
forceCollections();
finish();
}
// Sends a click event on the passed DOM node in an async manner
function* click(node) {
let def = promise.defer();
node.scrollIntoView();
// We need setTimeout here to allow any scrolling to complete before clicking
// the node.
setTimeout(() => {
node.click();
def.resolve();
}, 200);
return def;
}
/**
* 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) {
let root = options.rootVariable;
let expandTo = options.expandTo.split(".");
let lastDeferred = promise.defer();
function getNext(prop) {
let name = expandTo.shift();
let newProp = prop.get(name);
if (expandTo.length > 0) {
ok(newProp, "found property " + name);
if (newProp && newProp.expand) {
newProp.expand();
getNext(newProp);
} else {
lastDeferred.reject(prop);
}
} else if (newProp) {
lastDeferred.resolve(newProp);
} else {
lastDeferred.reject(prop);
}
}
if (root && root.expand) {
root.expand();
getNext(root);
} else {
lastDeferred.resolve(root);
}
return lastDeferred.promise;
}
/**
* 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.
let expandRules = [];
let 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.
let outstanding = [];
finder(rules, gUI.view, outstanding);
// Process the rules that need to expand properties.
let lastStep = processExpandRules.bind(null, expandRules);
// Return the results - a promise resolved to hold the updated ruleArray.
let 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 (let scope of view) {
for (let [, prop] of scope) {
for (let rule of rules) {
let matcher = matchVariablesViewProperty(prop, rule);
promises.push(matcher.then(onMatch.bind(null, prop, rule)));
}
}
}
}
function processExpandRules(rules) {
let rule = rules.shift();
if (!rule) {
return promise.resolve(null);
}
let deferred = promise.defer();
let expandOptions = {
rootVariable: gUI.view.getScopeAtIndex(parsed ? 1 : 0),
expandTo: rule.name
};
variablesViewExpandTo(expandOptions).then(function onSuccess(prop) {
let name = rule.name;
let lastName = name.split(".").pop();
rule.name = lastName;
let matched = matchVariablesViewProperty(prop, rule);
return matched.then(onMatch.bind(null, prop, rule)).then(function () {
rule.name = name;
});
}, function onFailure() {
return promise.resolve(null);
}).then(processExpandRules.bind(null, rules)).then(function () {
deferred.resolve(null);
});
return deferred.promise;
}
function onAllRulesMatched(rules) {
for (let rule of rules) {
let 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);
}
if (rule.name) {
let 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);
}
let 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
*/
function* selectTreeItem(ids) {
/* If this item is already selected, return */
if (gUI.tree.isSelected(ids)) {
return;
}
let updated = gUI.once("store-objects-updated");
gUI.tree.selectedItem = ids;
yield updated;
}
/**
* Click selects a row in the table.
*
* @param {String} id
* The id of the row in the table widget
*/
function* selectTableItem(id) {
let table = gUI.table;
let selector = ".table-widget-column#" + table.uniqueId +
" .table-widget-cell[value='" + id + "']";
let target = gPanelWindow.document.querySelector(selector);
ok(target, "table item found with ids " + id);
if (!target) {
showAvailableIds();
}
yield click(target);
yield gUI.once("sidebar-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] 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 + ".");
let deferred = promise.defer();
for (let [add, remove] of [
["addEventListener", "removeEventListener"],
["addListener", "removeListener"],
["on", "off"]
]) {
if ((add in target) && (remove in target)) {
target[add](eventName, function onEvent(...aArgs) {
target[remove](eventName, onEvent, useCapture);
deferred.resolve.apply(deferred, aArgs);
}, useCapture);
break;
}
}
return deferred.promise;
}
/**
* 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) {
let cells = getRowCells(id, includeHidden);
let values = {};
for (let name in cells) {
let 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) {
let doc = gPanelWindow.document;
let table = gUI.table;
let item = doc.querySelector(".table-widget-column#" + table.uniqueId +
" .table-widget-cell[value='" + id + "']");
if (!item) {
ok(false, "Row id '" + id + "' exists");
showAvailableIds();
}
let index = table.columns.get(table.uniqueId).cellNodes.indexOf(item);
let cells = {};
for (let [name, column] of [...table.columns]) {
if (!includeHidden && column.column.parentNode.hidden) {
continue;
}
cells[name] = column.cellNodes[index];
}
return cells;
}
/**
* Show available ids.
*/
function showAvailableIds() {
let doc = gPanelWindow.document;
let table = gUI.table;
info("Available ids:");
let cells = doc.querySelectorAll(".table-widget-column#" + table.uniqueId +
" .table-widget-cell");
for (let cell of cells) {
info(" - " + cell.getAttribute("value"));
}
}
/**
* 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) {
let row = getRowValues(id, true);
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.
*/
function* editCell(id, column, newValue, validate = true) {
let row = getRowCells(id, true);
let editableFieldsEngine = gUI.table._editableFieldsEngine;
editableFieldsEngine.edit(row[column]);
yield typeWithTerminator(newValue, "VK_RETURN", 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) {
let row = getRowCells(id, true);
let editableFieldsEngine = gUI.table._editableFieldsEngine;
let cell = row[column];
info("Selecting row " + id);
gUI.table.selectedRow = id;
info("Starting cell edit (" + id + ", " + column + ")");
editableFieldsEngine.edit(cell);
if (!selectText) {
let 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);
}
/**
* 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) {
let columns = gUI.table.columns;
let column = columns.get(id);
if (state) {
column.wrapper.removeAttribute("hidden");
} else {
column.wrapper.setAttribute("hidden", true);
}
}
/**
* Show or hide all columns.
*
* @param {Boolean} state
* true = show, false = hide
*/
function showAllColumns(state) {
let columns = gUI.table.columns;
for (let [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. VK_RETURN or VK_TAB
* @param {Boolean} validate
* Validate result? Default true.
*/
function* typeWithTerminator(str, terminator, validate = true) {
let editableFieldsEngine = gUI.table._editableFieldsEngine;
let textbox = editableFieldsEngine.textbox;
let colName = textbox.closest(".table-widget-column").id;
let changeExpected = str !== textbox.value;
if (!changeExpected) {
return editableFieldsEngine.currentTarget.getAttribute("data-id");
}
info("Typing " + str);
EventUtils.sendString(str);
info("Pressing " + terminator);
EventUtils.synthesizeKey(terminator, {});
if (validate) {
info("Validating results... waiting for ROW_EDIT event.");
let uniqueId = yield gUI.table.once(TableWidget.EVENTS.ROW_EDIT);
checkCell(uniqueId, colName, str);
return uniqueId;
}
return yield gUI.table.once(TableWidget.EVENTS.ROW_EDIT);
}
function getCurrentEditorValue() {
let editableFieldsEngine = gUI.table._editableFieldsEngine;
let 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).
*/
function* checkState(state) {
for (let [store, names] of state) {
let storeName = store.join(" > ");
info(`Selecting tree item ${storeName}`);
yield selectTreeItem(store);
let items = gUI.table.items;
is(items.size, names.length,
`There is correct number of rows in ${storeName}`);
if (names.length === 0) {
showAvailableIds();
}
for (let name of names) {
ok(items.has(name),
`There is item with name '${name}' in ${storeName}`);
if (!items.has(name)) {
showAvailableIds();
}
}
}
}
/**
* 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 = Task.async(function* (panelWin, callback) {
info("Focusing search box");
let searchBox = panelWin.document.getElementById("storage-searchbox");
let focused = once(searchBox, "focus");
panelWin.focus();
let strings = Services.strings.createBundle(
"chrome://devtools/locale/storage.properties");
synthesizeKeyShortcut(strings.GetStringFromName("storage.filter.key"));
yield focused;
if (callback) {
callback();
}
});
function getCookieId(name, domain, path) {
return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`;
}
function setPermission(url, permission) {
const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
let uri = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService)
.newURI(url);
let ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
.getService(Ci.nsIScriptSecurityManager);
let principal = ssm.createCodebasePrincipal(uri, {});
Components.classes["@mozilla.org/permissionmanager;1"]
.getService(nsIPermissionManager)
.addFromPrincipal(principal, permission,
nsIPermissionManager.ALLOW_ACTION);
}