Files
2020-09-24 08:10:23 +00:00

557 lines
18 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint no-unused-vars: [2, {"vars": "local"}] */
/* import-globals-from ../../test/head.js */
"use strict";
// Import the inspector's head.js first (which itself imports shared-head.js).
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
this);
var {CssRuleView} = require("devtools/client/inspector/rules/rules");
var {getInplaceEditorForSpan: inplaceEditor} =
require("devtools/client/shared/inplace-editor");
const {getColor: getThemeColor} = require("devtools/client/shared/theme");
const TEST_URL_ROOT =
"http://example.com/browser/devtools/client/inspector/shared/test/";
const TEST_URL_ROOT_SSL =
"https://example.com/browser/devtools/client/inspector/shared/test/";
const ROOT_TEST_DIR = getRootDirectory(gTestPath);
const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
const STYLE_INSPECTOR_L10N =
new LocalizationHelper("devtools/shared/locales/styleinspector.properties");
// Clean-up all prefs that might have been changed during a test run
// (safer here because if the test fails, then the pref is never reverted)
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.defaultColorUnit");
});
/**
* The functions found below are here to ease test development and maintenance.
* Most of these functions are stateless and will require some form of context
* (the instance of the current toolbox, or inspector panel for instance).
*
* Most of these functions are async too and return promises.
*
* All tests should follow the following pattern:
*
* add_task(function*() {
* yield addTab(TEST_URI);
* let {toolbox, inspector} = yield openInspector();
* inspector.sidebar.select(viewId);
* let view = inspector[viewId].view;
* yield selectNode("#test", inspector);
* yield someAsyncTestFunction(view);
* });
*
* add_task is the way to define the testcase in the test file. It accepts
* a single generator-function argument.
* The generator function should yield any async call.
*
* There is no need to clean tabs up at the end of a test as this is done
* automatically.
*
* It is advised not to store any references on the global scope. There
* shouldn't be a need to anyway. Thanks to add_task, test steps, even
* though asynchronous, can be described in a nice flat way, and
* if/for/while/... control flow can be used as in sync code, making it
* possible to write the outline of the test case all in add_task, and delegate
* actual processing and assertions to other functions.
*/
/* *********************************************
* UTILS
* *********************************************
* General test utilities.
* Add new tabs, open the toolbox and switch to the various panels, select
* nodes, get node references, ...
*/
/**
* The rule-view tests rely on a frame-script to be injected in the content test
* page. So override the shared-head's addTab to load the frame script after the
* tab was added.
* FIXME: Refactor the rule-view tests to use the testActor instead of a frame
* script, so they can run on remote targets too.
*/
var _addTab = addTab;
addTab = function (url) {
return _addTab(url).then(tab => {
info("Loading the helper frame script " + FRAME_SCRIPT_URL);
let browser = tab.linkedBrowser;
browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false);
return tab;
});
};
/**
* Wait for a content -> chrome message on the message manager (the window
* messagemanager is used).
*
* @param {String} name
* The message name
* @return {Promise} A promise that resolves to the response data when the
* message has been received
*/
function waitForContentMessage(name) {
info("Expecting message " + name + " from content");
let mm = gBrowser.selectedBrowser.messageManager;
let def = defer();
mm.addMessageListener(name, function onMessage(msg) {
mm.removeMessageListener(name, onMessage);
def.resolve(msg.data);
});
return def.promise;
}
/**
* Send an async message to the frame script (chrome -> content) and wait for a
* response message with the same name (content -> chrome).
*
* @param {String} name
* The message name. Should be one of the messages defined
* in doc_frame_script.js
* @param {Object} data
* Optional data to send along
* @param {Object} objects
* Optional CPOW objects to send along
* @param {Boolean} expectResponse
* If set to false, don't wait for a response with the same name
* from the content script. Defaults to true.
* @return {Promise} Resolves to the response data if a response is expected,
* immediately resolves otherwise
*/
function executeInContent(name, data = {}, objects = {},
expectResponse = true) {
info("Sending message " + name + " to content");
let mm = gBrowser.selectedBrowser.messageManager;
mm.sendAsyncMessage(name, data, objects);
if (expectResponse) {
return waitForContentMessage(name);
}
return promise.resolve();
}
/**
* Send an async message to the frame script and get back the requested
* computed style property.
*
* @param {String} selector
* The selector used to obtain the element.
* @param {String} pseudo
* pseudo id to query, or null.
* @param {String} name
* name of the property.
*/
function* getComputedStyleProperty(selector, pseudo, propName) {
return yield executeInContent("Test:GetComputedStylePropertyValue",
{selector,
pseudo,
name: propName});
}
/**
* Send an async message to the frame script and wait until the requested
* computed style property has the expected value.
*
* @param {String} selector
* The selector used to obtain the element.
* @param {String} pseudo
* pseudo id to query, or null.
* @param {String} prop
* name of the property.
* @param {String} expected
* expected value of property
* @param {String} name
* the name used in test message
*/
function* waitForComputedStyleProperty(selector, pseudo, name, expected) {
return yield executeInContent("Test:WaitForComputedStylePropertyValue",
{selector,
pseudo,
expected,
name});
}
/**
* Given an inplace editable element, click to switch it to edit mode, wait for
* focus
*
* @return a promise that resolves to the inplace-editor element when ready
*/
var focusEditableField = Task.async(function* (ruleView, editable, xOffset = 1,
yOffset = 1, options = {}) {
let onFocus = once(editable.parentNode, "focus", true);
info("Clicking on editable field to turn to edit mode");
EventUtils.synthesizeMouse(editable, xOffset, yOffset, options,
editable.ownerDocument.defaultView);
yield onFocus;
info("Editable field gained focus, returning the input field now");
let onEdit = inplaceEditor(editable.ownerDocument.activeElement);
return onEdit;
});
/**
* Polls a given function waiting for it to return true.
*
* @param {Function} validatorFn
* A validator function that returns a boolean.
* This is called every few milliseconds to check if the result is true.
* When it is true, the promise resolves.
* @param {String} name
* Optional name of the test. This is used to generate
* the success and failure messages.
* @return a promise that resolves when the function returned true or rejects
* if the timeout is reached
*/
function waitForSuccess(validatorFn, name = "untitled") {
let def = defer();
function wait(validator) {
if (validator()) {
ok(true, "Validator function " + name + " returned true");
def.resolve();
} else {
setTimeout(() => wait(validator), 200);
}
}
wait(validatorFn);
return def.promise;
}
/**
* Get the dataURL for the font family tooltip.
*
* @param {String} font
* The font family value.
* @param {object} nodeFront
* The NodeActor that will used to retrieve the dataURL for the
* font family tooltip contents.
*/
var getFontFamilyDataURL = Task.async(function* (font, nodeFront) {
let fillStyle = getThemeColor("body-color");
let {data} = yield nodeFront.getFontFamilyDataURL(font, fillStyle);
let dataURL = yield data.string();
return dataURL;
});
/* *********************************************
* RULE-VIEW
* *********************************************
* Rule-view related test utility functions
* This object contains functions to get rules, get properties, ...
*/
/**
* Get the DOMNode for a css rule in the rule-view that corresponds to the given
* selector
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {String} selectorText
* The selector in the rule-view for which the rule
* object is wanted
* @return {DOMNode}
*/
function getRuleViewRule(view, selectorText) {
let rule;
for (let r of view.styleDocument.querySelectorAll(".ruleview-rule")) {
let selector = r.querySelector(".ruleview-selectorcontainer, " +
".ruleview-selector-matched");
if (selector && selector.textContent === selectorText) {
rule = r;
break;
}
}
return rule;
}
/**
* Get references to the name and value span nodes corresponding to a given
* selector and property name in the rule-view
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {String} selectorText
* The selector in the rule-view to look for the property in
* @param {String} propertyName
* The name of the property
* @return {Object} An object like {nameSpan: DOMNode, valueSpan: DOMNode}
*/
function getRuleViewProperty(view, selectorText, propertyName) {
let prop;
let rule = getRuleViewRule(view, selectorText);
if (rule) {
// Look for the propertyName in that rule element
for (let p of rule.querySelectorAll(".ruleview-property")) {
let nameSpan = p.querySelector(".ruleview-propertyname");
let valueSpan = p.querySelector(".ruleview-propertyvalue");
if (nameSpan.textContent === propertyName) {
prop = {nameSpan: nameSpan, valueSpan: valueSpan};
break;
}
}
}
return prop;
}
/**
* Get the text value of the property corresponding to a given selector and name
* in the rule-view
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {String} selectorText
* The selector in the rule-view to look for the property in
* @param {String} propertyName
* The name of the property
* @return {String} The property value
*/
function getRuleViewPropertyValue(view, selectorText, propertyName) {
return getRuleViewProperty(view, selectorText, propertyName)
.valueSpan.textContent;
}
/**
* Get a reference to the selector DOM element corresponding to a given selector
* in the rule-view
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {String} selectorText
* The selector in the rule-view to look for
* @return {DOMNode} The selector DOM element
*/
function getRuleViewSelector(view, selectorText) {
let rule = getRuleViewRule(view, selectorText);
return rule.querySelector(".ruleview-selector, .ruleview-selector-matched");
}
/**
* Get a reference to the selectorhighlighter icon DOM element corresponding to
* a given selector in the rule-view
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {String} selectorText
* The selector in the rule-view to look for
* @return {DOMNode} The selectorhighlighter icon DOM element
*/
function getRuleViewSelectorHighlighterIcon(view, selectorText) {
let rule = getRuleViewRule(view, selectorText);
return rule.querySelector(".ruleview-selectorhighlighter");
}
/**
* Simulate a color change in a given color picker tooltip, and optionally wait
* for a given element in the page to have its style changed as a result
*
* @param {RuleView} ruleView
* The related rule view instance
* @param {SwatchColorPickerTooltip} colorPicker
* @param {Array} newRgba
* The new color to be set [r, g, b, a]
* @param {Object} expectedChange
* Optional object that needs the following props:
* - {DOMNode} element The element in the page that will have its
* style changed.
* - {String} name The style name that will be changed
* - {String} value The expected style value
* The style will be checked like so: getComputedStyle(element)[name] === value
*/
var simulateColorPickerChange = Task.async(function* (ruleView, colorPicker,
newRgba, expectedChange) {
let onRuleViewChanged = ruleView.once("ruleview-changed");
info("Getting the spectrum colorpicker object");
let spectrum = yield colorPicker.spectrum;
info("Setting the new color");
spectrum.rgb = newRgba;
info("Applying the change");
spectrum.updateUI();
spectrum.onChange();
info("Waiting for rule-view to update");
yield onRuleViewChanged;
if (expectedChange) {
info("Waiting for the style to be applied on the page");
yield waitForSuccess(() => {
let {element, name, value} = expectedChange;
return content.getComputedStyle(element)[name] === value;
}, "Color picker change applied on the page");
}
});
/**
* Get a rule-link from the rule-view given its index
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {Number} index
* The index of the link to get
* @return {DOMNode} The link if any at this index
*/
function getRuleViewLinkByIndex(view, index) {
let links = view.styleDocument.querySelectorAll(".ruleview-rule-source");
return links[index];
}
/**
* Get rule-link text from the rule-view given its index
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {Number} index
* The index of the link to get
* @return {String} The string at this index
*/
function getRuleViewLinkTextByIndex(view, index) {
let link = getRuleViewLinkByIndex(view, index);
return link.querySelector(".ruleview-rule-source-label").textContent;
}
/**
* Click on a rule-view's close brace to focus a new property name editor
*
* @param {RuleEditor} ruleEditor
* An instance of RuleEditor that will receive the new property
* @return a promise that resolves to the newly created editor when ready and
* focused
*/
var focusNewRuleViewProperty = Task.async(function* (ruleEditor) {
info("Clicking on a close ruleEditor brace to start editing a new property");
ruleEditor.closeBrace.scrollIntoView();
let editor = yield focusEditableField(ruleEditor.ruleView,
ruleEditor.closeBrace);
is(inplaceEditor(ruleEditor.newPropSpan), editor,
"Focused editor is the new property editor.");
return editor;
});
/**
* Create a new property name in the rule-view, focusing a new property editor
* by clicking on the close brace, and then entering the given text.
* Keep in mind that the rule-view knows how to handle strings with multiple
* properties, so the input text may be like: "p1:v1;p2:v2;p3:v3".
*
* @param {RuleEditor} ruleEditor
* The instance of RuleEditor that will receive the new property(ies)
* @param {String} inputValue
* The text to be entered in the new property name field
* @return a promise that resolves when the new property name has been entered
* and once the value field is focused
*/
var createNewRuleViewProperty = Task.async(function* (ruleEditor, inputValue) {
info("Creating a new property editor");
let editor = yield focusNewRuleViewProperty(ruleEditor);
info("Entering the value " + inputValue);
editor.input.value = inputValue;
info("Submitting the new value and waiting for value field focus");
let onFocus = once(ruleEditor.element, "focus", true);
EventUtils.synthesizeKey("VK_RETURN", {},
ruleEditor.element.ownerDocument.defaultView);
yield onFocus;
});
/**
* Set the search value for the rule-view filter styles search box.
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {String} searchValue
* The filter search value
* @return a promise that resolves when the rule-view is filtered for the
* search term
*/
var setSearchFilter = Task.async(function* (view, searchValue) {
info("Setting filter text to \"" + searchValue + "\"");
let win = view.styleWindow;
let searchField = view.searchField;
searchField.focus();
synthesizeKeys(searchValue, win);
yield view.inspector.once("ruleview-filtered");
});
/* *********************************************
* COMPUTED-VIEW
* *********************************************
* Computed-view related utility functions.
* Allows to get properties, links, expand properties, ...
*/
/**
* Get references to the name and value span nodes corresponding to a given
* property name in the computed-view
*
* @param {CssComputedView} view
* The instance of the computed view panel
* @param {String} name
* The name of the property to retrieve
* @return an object {nameSpan, valueSpan}
*/
function getComputedViewProperty(view, name) {
let prop;
for (let property of view.styleDocument.querySelectorAll(".property-view")) {
let nameSpan = property.querySelector(".property-name");
let valueSpan = property.querySelector(".property-value");
if (nameSpan.textContent === name) {
prop = {nameSpan: nameSpan, valueSpan: valueSpan};
break;
}
}
return prop;
}
/**
* Get the text value of the property corresponding to a given name in the
* computed-view
*
* @param {CssComputedView} view
* The instance of the computed view panel
* @param {String} name
* The name of the property to retrieve
* @return {String} The property value
*/
function getComputedViewPropertyValue(view, name, propertyName) {
return getComputedViewProperty(view, name, propertyName)
.valueSpan.textContent;
}
/**
* Open the style editor context menu and return all of it's items in a flat array
* @param {CssRuleView} view
* The instance of the rule-view panel
* @return An array of MenuItems
*/
function openStyleContextMenuAndGetAllItems(view, target) {
let menu = view._contextmenu._openMenu({target: target});
// Flatten all menu items into a single array to make searching through it easier
let allItems = [].concat.apply([], menu.items.map(function addItem(item) {
if (item.submenu) {
return addItem(item.submenu.items);
}
return item;
}));
return allItems;
}