fune/devtools/client/inspector/rules/test/head.js

535 lines
19 KiB
JavaScript

/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.defaultColorUnit");
});
var {getInplaceEditorForSpan: inplaceEditor} =
require("devtools/client/shared/inplace-editor");
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");
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.defaultColorUnit");
});
/**
* 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;
});
};
/**
* Get an element's inline style property value.
* @param {TestActor} testActor
* @param {String} selector
* The selector used to obtain the element.
* @param {String} name
* name of the property.
*/
function getStyle(testActor, selector, propName) {
return testActor.eval(`
content.document.querySelector("${selector}")
.style.getPropertyValue("${propName}");
`);
}
/**
* When a tooltip is closed, this ends up "commiting" the value changed within
* the tooltip (e.g. the color in case of a colorpicker) which, in turn, ends up
* setting the value of the corresponding css property in the rule-view.
* Use this function to close the tooltip and make sure the test waits for the
* ruleview-changed event.
* @param {SwatchBasedEditorTooltip} editorTooltip
* @param {CSSRuleView} view
*/
function* hideTooltipAndWaitForRuleViewChanged(editorTooltip, view) {
let onModified = view.once("ruleview-changed");
let onHidden = editorTooltip.tooltip.once("hidden");
editorTooltip.hide();
yield onModified;
yield onHidden;
}
/**
* Polls a given generator function waiting for it to return true.
*
* @param {Function} validatorFn
* A validator generator 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
*/
var waitForSuccess = Task.async(function* (validatorFn, desc = "untitled") {
let i = 0;
while (true) {
info("Checking: " + desc);
if (yield validatorFn()) {
ok(true, "Success: " + desc);
break;
}
i++;
if (i > 10) {
ok(false, "Failure: " + desc);
break;
}
yield new Promise(r => setTimeout(r, 200));
}
});
/**
* 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.
* Note that this function assumes that the colorpicker popup is already open
* and it won't close it after having selected the new color.
*
* @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:
* - {String} selector The selector to 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 onComputedStyleChanged;
if (expectedChange) {
let {selector, name, value} = expectedChange;
onComputedStyleChanged = waitForComputedStyleProperty(selector, null, name, value);
}
let onRuleViewChanged = ruleView.once("ruleview-changed");
info("Getting the spectrum colorpicker object");
let spectrum = 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 onComputedStyleChanged;
}
});
/**
* Open the color picker popup for a given property in a given rule and
* simulate a color change. Optionally wait for a given element in the page to
* have its style changed as a result.
*
* @param {RuleView} view
* The related rule view instance
* @param {Number} ruleIndex
* Which rule to target in the rule view
* @param {Number} propIndex
* Which property to target in the rule
* @param {Array} newRgba
* The new color to be set [r, g, b, a]
* @param {Object} expectedChange
* Optional object that needs the following props:
* - {String} selector The selector to 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 openColorPickerAndSelectColor = Task.async(function* (view, ruleIndex,
propIndex, newRgba, expectedChange) {
let ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
let propEditor = ruleEditor.rule.textProps[propIndex].editor;
let swatch = propEditor.valueSpan.querySelector(".ruleview-colorswatch");
let cPicker = view.tooltips.getTooltip("colorPicker");
info("Opening the colorpicker by clicking the color swatch");
let onColorPickerReady = cPicker.once("ready");
swatch.click();
yield onColorPickerReady;
yield simulateColorPickerChange(view, cPicker, newRgba, expectedChange);
return {propEditor, swatch, cPicker};
});
/**
* Open the cubicbezier popup for a given property in a given rule and
* simulate a curve change. Optionally wait for a given element in the page to
* have its style changed as a result.
*
* @param {RuleView} view
* The related rule view instance
* @param {Number} ruleIndex
* Which rule to target in the rule view
* @param {Number} propIndex
* Which property to target in the rule
* @param {Array} coords
* The new coordinates to be used, e.g. [0.1, 2, 0.9, -1]
* @param {Object} expectedChange
* Optional object that needs the following props:
* - {String} selector The selector to 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 openCubicBezierAndChangeCoords = Task.async(function* (view, ruleIndex,
propIndex, coords, expectedChange) {
let ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
let propEditor = ruleEditor.rule.textProps[propIndex].editor;
let swatch = propEditor.valueSpan.querySelector(".ruleview-bezierswatch");
let bezierTooltip = view.tooltips.getTooltip("cubicBezier");
info("Opening the cubicBezier by clicking the swatch");
let onBezierWidgetReady = bezierTooltip.once("ready");
swatch.click();
yield onBezierWidgetReady;
let widget = yield bezierTooltip.widget;
info("Simulating a change of curve in the widget");
let onRuleViewChanged = view.once("ruleview-changed");
widget.coordinates = coords;
yield onRuleViewChanged;
if (expectedChange) {
info("Waiting for the style to be applied on the page");
let {selector, name, value} = expectedChange;
yield waitForComputedStyleProperty(selector, null, name, value);
}
return {propEditor, swatch, bezierTooltip};
});
/**
* Simulate adding a new property in an existing rule in the rule-view.
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {Number} ruleIndex
* The index of the rule to use. Note that if ruleIndex is 0, you might
* want to also listen to markupmutation events in your test since
* that's going to change the style attribute of the selected node.
* @param {String} name
* The name for the new property
* @param {String} value
* The value for the new property
* @param {String} commitValueWith
* Which key should be used to commit the new value. VK_RETURN is used by
* default, but tests might want to use another key to test cancelling
* for exemple.
* @param {Boolean} blurNewProperty
* After the new value has been added, a new property would have been
* focused. This parameter is true by default, and that causes the new
* property to be blurred. Set to false if you don't want this.
* @return {TextProperty} The instance of the TextProperty that was added
*/
var addProperty = Task.async(function* (view, ruleIndex, name, value,
commitValueWith = "VK_RETURN",
blurNewProperty = true) {
info("Adding new property " + name + ":" + value + " to rule " + ruleIndex);
let ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
let editor = yield focusNewRuleViewProperty(ruleEditor);
let numOfProps = ruleEditor.rule.textProps.length;
info("Adding name " + name);
editor.input.value = name;
let onNameAdded = view.once("ruleview-changed");
EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
yield onNameAdded;
// Focus has moved to the value inplace-editor automatically.
editor = inplaceEditor(view.styleDocument.activeElement);
let textProps = ruleEditor.rule.textProps;
let textProp = textProps[textProps.length - 1];
is(ruleEditor.rule.textProps.length, numOfProps + 1,
"A new test property was added");
is(editor, inplaceEditor(textProp.editor.valueSpan),
"The inplace editor appeared for the value");
info("Adding value " + value);
// Setting the input value schedules a preview to be shown in 10ms which
// triggers a ruleview-changed event (see bug 1209295).
let onPreview = view.once("ruleview-changed");
editor.input.value = value;
view.throttle.flush();
yield onPreview;
let onValueAdded = view.once("ruleview-changed");
EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow);
yield onValueAdded;
if (blurNewProperty) {
view.styleDocument.activeElement.blur();
}
return textProp;
});
/**
* Simulate changing the value of a property in a rule in the rule-view.
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {TextProperty} textProp
* The instance of the TextProperty to be changed
* @param {String} value
* The new value to be used. If null is passed, then the value will be
* deleted
* @param {Boolean} blurNewProperty
* After the value has been changed, a new property would have been
* focused. This parameter is true by default, and that causes the new
* property to be blurred. Set to false if you don't want this.
*/
var setProperty = Task.async(function* (view, textProp, value,
blurNewProperty = true) {
yield focusEditableField(view, textProp.editor.valueSpan);
let onPreview = view.once("ruleview-changed");
if (value === null) {
EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
} else {
EventUtils.sendString(value, view.styleWindow);
}
view.throttle.flush();
yield onPreview;
let onValueDone = view.once("ruleview-changed");
EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
yield onValueDone;
if (blurNewProperty) {
view.styleDocument.activeElement.blur();
}
});
/**
* Simulate removing a property from an existing rule in the rule-view.
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {TextProperty} textProp
* The instance of the TextProperty to be removed
* @param {Boolean} blurNewProperty
* After the property has been removed, a new property would have been
* focused. This parameter is true by default, and that causes the new
* property to be blurred. Set to false if you don't want this.
*/
var removeProperty = Task.async(function* (view, textProp,
blurNewProperty = true) {
yield focusEditableField(view, textProp.editor.nameSpan);
let onModifications = view.once("ruleview-changed");
info("Deleting the property name now");
EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
yield onModifications;
if (blurNewProperty) {
view.styleDocument.activeElement.blur();
}
});
/**
* Simulate clicking the enable/disable checkbox next to a property in a rule.
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {TextProperty} textProp
* The instance of the TextProperty to be enabled/disabled
*/
var togglePropStatus = Task.async(function* (view, textProp) {
let onRuleViewRefreshed = view.once("ruleview-changed");
textProp.editor.enable.click();
yield onRuleViewRefreshed;
});
/**
* Reload the current page and wait for the inspector to be initialized after
* the navigation
*
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @param {TestActor} testActor
* The current instance of the TestActor
*/
function* reloadPage(inspector, testActor) {
let onNewRoot = inspector.once("new-root");
yield testActor.reload();
yield onNewRoot;
yield inspector.markup._waitForChildren();
}
/**
* Create a new rule by clicking on the "add rule" button.
* This will leave the selector inplace-editor active.
*
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @param {CssRuleView} view
* The instance of the rule-view panel
* @return a promise that resolves after the rule has been added
*/
function* addNewRule(inspector, view) {
info("Adding the new rule using the button");
view.addRuleButton.click();
info("Waiting for rule view to change");
yield view.once("ruleview-changed");
}
/**
* Create a new rule by clicking on the "add rule" button, dismiss the editor field and
* verify that the selector is correct.
*
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {String} expectedSelector
* The value we expect the selector to have
* @param {Number} expectedIndex
* The index we expect the rule to have in the rule-view
* @return a promise that resolves after the rule has been added
*/
function* addNewRuleAndDismissEditor(inspector, view, expectedSelector, expectedIndex) {
yield addNewRule(inspector, view);
info("Getting the new rule at index " + expectedIndex);
let ruleEditor = getRuleViewRuleEditor(view, expectedIndex);
let editor = ruleEditor.selectorText.ownerDocument.activeElement;
is(editor.value, expectedSelector,
"The editor for the new selector has the correct value: " + expectedSelector);
info("Pressing escape to leave the editor");
EventUtils.synthesizeKey("VK_ESCAPE", {});
is(ruleEditor.selectorText.textContent, expectedSelector,
"The new selector has the correct text: " + expectedSelector);
}
/**
* Simulate a sequence of non-character keys (return, escape, tab) and wait for
* a given element to receive the focus.
*
* @param {CssRuleView} view
* The instance of the rule-view panel
* @param {DOMNode} element
* The element that should be focused
* @param {Array} keys
* Array of non-character keys, the part that comes after "DOM_VK_" eg.
* "RETURN", "ESCAPE"
* @return a promise that resolves after the element received the focus
*/
function* sendKeysAndWaitForFocus(view, element, keys) {
let onFocus = once(element, "focus", true);
for (let key of keys) {
EventUtils.sendKey(key, view.styleWindow);
}
yield onFocus;
}
/**
* Wait for a markupmutation event on the inspector that is for a style modification.
* @param {InspectorPanel} inspector
* @return {Promise}
*/
function waitForStyleModification(inspector) {
return new Promise(function (resolve) {
function checkForStyleModification(name, mutations) {
for (let mutation of mutations) {
if (mutation.type === "attributes" && mutation.attributeName === "style") {
inspector.off("markupmutation", checkForStyleModification);
resolve();
return;
}
}
}
inspector.on("markupmutation", checkForStyleModification);
});
}
/**
* Click on the selector icon
* @param {DOMNode} icon
* @param {CSSRuleView} view
*/
function* clickSelectorIcon(icon, view) {
let onToggled = view.once("ruleview-selectorhighlighter-toggled");
EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
yield onToggled;
}
/**
* Toggle one of the checkboxes inside the class-panel. Resolved after the DOM mutation
* has been recorded.
* @param {CssRuleView} view The rule-view instance.
* @param {String} name The class name to find the checkbox.
*/
function* toggleClassPanelCheckBox(view, name) {
info(`Clicking on checkbox for class ${name}`);
const checkBox = [...view.classPanel.querySelectorAll("[type=checkbox]")].find(box => {
return box.dataset.name === name;
});
const onMutation = view.inspector.once("markupmutation");
checkBox.click();
info("Waiting for a markupmutation as a result of toggling this class");
yield onMutation;
}
/**
* Verify the content of the class-panel.
* @param {CssRuleView} view The rule-view isntance
* @param {Array} classes The list of expected classes. Each item in this array is an
* object with the following properties: {name: {String}, state: {Boolean}}
*/
function checkClassPanelContent(view, classes) {
const checkBoxNodeList = view.classPanel.querySelectorAll("[type=checkbox]");
is(checkBoxNodeList.length, classes.length,
"The panel contains the expected number of checkboxes");
for (let i = 0; i < classes.length; i++) {
is(checkBoxNodeList[i].dataset.name, classes[i].name,
`Checkbox ${i} has the right class name`);
is(checkBoxNodeList[i].checked, classes[i].state,
`Checkbox ${i} has the right state`);
}
}