Source code
Revision control
Copy as Markdown
Other Tools
/* Any copyright is dedicated to the Public Domain.
/* eslint no-unused-vars: [2, {"vars": "local"}] */
"use strict";
// Import the inspector's head.js first (which itself imports shared-head.js).
Services.scriptloader.loadSubScript(
this
);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.defaultColorUnit");
});
var {
getInplaceEditorForSpan: inplaceEditor,
} = require("resource://devtools/client/shared/inplace-editor.js");
const {
COMPATIBILITY_TOOLTIP_MESSAGE,
} = require("resource://devtools/client/inspector/rules/constants.js");
const ROOT_TEST_DIR = getRootDirectory(gTestPath);
const STYLE_INSPECTOR_L10N = new LocalizationHelper(
"devtools/shared/locales/styleinspector.properties"
);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.defaultColorUnit");
});
/**
* 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
*/
async function hideTooltipAndWaitForRuleViewChanged(editorTooltip, view) {
const onModified = view.once("ruleview-changed");
const onHidden = editorTooltip.tooltip.once("hidden");
editorTooltip.hide();
await onModified;
await 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 = async function (validatorFn, desc = "untitled") {
let i = 0;
while (true) {
info("Checking: " + desc);
if (await validatorFn()) {
ok(true, "Success: " + desc);
break;
}
i++;
if (i > 10) {
ok(false, "Failure: " + desc);
break;
}
await 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 = async function (
ruleView,
colorPicker,
newRgba,
expectedChange
) {
let onComputedStyleChanged;
if (expectedChange) {
const { selector, name, value } = expectedChange;
onComputedStyleChanged = waitForComputedStyleProperty(
selector,
null,
name,
value
);
}
const onRuleViewChanged = ruleView.once("ruleview-changed");
info("Getting the spectrum colorpicker object");
const 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");
await onRuleViewChanged;
if (expectedChange) {
info("Waiting for the style to be applied on the page");
await 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 = async function (
view,
ruleIndex,
propIndex,
newRgba,
expectedChange
) {
const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
const propEditor = ruleEditor.rule.textProps[propIndex].editor;
const swatch = propEditor.valueSpan.querySelector(".inspector-colorswatch");
const cPicker = view.tooltips.getTooltip("colorPicker");
info("Opening the colorpicker by clicking the color swatch");
const onColorPickerReady = cPicker.once("ready");
swatch.click();
await onColorPickerReady;
await 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 = async function (
view,
ruleIndex,
propIndex,
coords,
expectedChange
) {
const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
const propEditor = ruleEditor.rule.textProps[propIndex].editor;
const swatch = propEditor.valueSpan.querySelector(".inspector-bezierswatch");
const bezierTooltip = view.tooltips.getTooltip("cubicBezier");
info("Opening the cubicBezier by clicking the swatch");
const onBezierWidgetReady = bezierTooltip.once("ready");
swatch.click();
await onBezierWidgetReady;
const widget = await bezierTooltip.widget;
info("Simulating a change of curve in the widget");
const onRuleViewChanged = view.once("ruleview-changed");
widget.coordinates = coords;
await onRuleViewChanged;
if (expectedChange) {
info("Waiting for the style to be applied on the page");
const { selector, name, value } = expectedChange;
await 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.
* @param {String} name
* The name for the new property
* @param {String} value
* The value for the new property
* @param {Object=} options
* @param {String=} options.commitValueWith
* Which key should be used to commit the new value. VK_TAB is used by
* default, but tests might want to use another key to test cancelling
* for exemple.
* If set to null, no keys will be hit, so the input will still be focused
* at the end of this function
* @param {Boolean=} options.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 = async function (
view,
ruleIndex,
name,
value,
{ commitValueWith = "VK_TAB", blurNewProperty = true } = {}
) {
info("Adding new property " + name + ":" + value + " to rule " + ruleIndex);
const ruleEditor = getRuleViewRuleEditor(view, ruleIndex);
let editor = await focusNewRuleViewProperty(ruleEditor);
const numOfProps = ruleEditor.rule.textProps.length;
const onMutations = new Promise(r => {
// If the rule index is 0, then we are updating the rule for the "element"
// selector in the rule view.
// This rule is actually updating the style attribute of the element, and
// therefore we can expect mutations.
// For any other rule index, no mutation should be created, we can resolve
// immediately.
if (ruleIndex !== 0) {
r();
}
// Use CSS.escape for the name in order to match the logic at
// devtools/client/fronts/inspector/rule-rewriter.js
// This leads to odd values in the style attribute and might change in the
const expectedAttributeValue = `${CSS.escape(name)}: ${value}`;
view.inspector.walker.on(
"mutations",
function onWalkerMutations(mutations) {
// Wait until we receive a mutation which updates the style attribute
// with the expected value.
const receivedLastMutation = mutations.some(
mut =>
mut.attributeName === "style" &&
mut.newValue.includes(expectedAttributeValue)
);
if (receivedLastMutation) {
view.inspector.walker.off("mutations", onWalkerMutations);
r();
}
}
);
});
info("Adding name " + name);
editor.input.value = name;
is(
editor.input.getAttribute("aria-label"),
"New property name",
"New property name input has expected aria-label"
);
const onNameAdded = view.once("ruleview-changed");
EventUtils.synthesizeKey("VK_TAB", {}, view.styleWindow);
await onNameAdded;
// Focus has moved to the value inplace-editor automatically.
editor = inplaceEditor(view.styleDocument.activeElement);
const textProps = ruleEditor.rule.textProps;
const 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
const onPreview = view.once("ruleview-changed");
editor.input.value = value;
ok(
!!editor.input.getAttribute("aria-labelledby"),
"The value input has an aria-labelledby attribute…"
);
is(
editor.input.getAttribute("aria-labelledby"),
textProp.editor.nameSpan.id,
"…which references the property name input"
);
view.debounce.flush();
await onPreview;
if (commitValueWith === null) {
return textProp;
}
const onRuleViewChanged = view.once("ruleview-changed");
EventUtils.synthesizeKey(commitValueWith, {}, view.styleWindow);
await onRuleViewChanged;
info(
"Waiting for DOM mutations in case the property was added to the element style"
);
await onMutations;
if (blurNewProperty) {
view.styleDocument.activeElement.blur();
}
return textProp;
};
/**
* Change the name 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} name
* The new property name.
*/
var renameProperty = async function (view, textProp, name) {
await focusEditableField(view, textProp.editor.nameSpan);
const onNameDone = view.once("ruleview-changed");
info(`Rename the property to ${name}`);
EventUtils.sendString(name, view.styleWindow);
EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
info("Wait for property name.");
await onNameDone;
if (
!Services.prefs.getBoolPref("devtools.inspector.rule-view.focusNextOnEnter")
) {
return;
}
// Renaming the property auto-advances the focus to the value input. Exiting without
// committing will still fire a change event. @see TextPropertyEditor._onValueDone().
// Wait for that event too before proceeding.
const onValueDone = view.once("ruleview-changed");
EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
info("Wait for property value.");
await onValueDone;
};
/**
* 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 = async function (view, textProp, blurNewProperty = true) {
await focusEditableField(view, textProp.editor.nameSpan);
const onModifications = view.once("ruleview-changed");
info("Deleting the property name now");
EventUtils.synthesizeKey("VK_DELETE", {}, view.styleWindow);
EventUtils.synthesizeKey("VK_TAB", {}, view.styleWindow);
await 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 = async function (view, textProp) {
const onRuleViewRefreshed = view.once("ruleview-changed");
textProp.editor.enable.click();
await onRuleViewRefreshed;
};
/**
* 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
* @returns {Rule} a promise that resolves the new model Rule after the rule has
* been added
*/
async function addNewRule(inspector, view) {
const onNewRuleAdded = view.once("new-rule-added");
info("Adding the new rule using the button");
view.addRuleButton.click();
info("Waiting for new-rule-added event…");
const rule = await onNewRuleAdded;
info("…received new-rule-added");
return rule;
}
/**
* 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
* @returns {Rule} a promise that resolves the new model Rule after the rule has
* been added
*/
async function addNewRuleAndDismissEditor(
inspector,
view,
expectedSelector,
expectedIndex
) {
const rule = await addNewRule(inspector, view);
info("Getting the new rule at index " + expectedIndex);
const ruleEditor = getRuleViewRuleEditor(view, expectedIndex);
const 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("KEY_Escape");
is(
ruleEditor.selectorText.textContent,
expectedSelector,
"The new selector has the correct text: " + expectedSelector
);
return rule;
}
/**
* 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
*/
async function sendKeysAndWaitForFocus(view, element, keys) {
const onFocus = once(element, "focus", true);
for (const key of keys) {
EventUtils.sendKey(key, view.styleWindow);
}
await 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(mutations) {
for (const mutation of mutations) {
if (
mutation.type === "attributes" &&
mutation.attributeName === "style"
) {
inspector.off("markupmutation", checkForStyleModification);
resolve();
return;
}
}
}
inspector.on("markupmutation", checkForStyleModification);
});
}
/**
* Click on the icon next to the selector of a CSS rule in the Rules view
* to toggle the selector highlighter. If a selector highlighter is not already visible
* for the given selector, wait for it to be shown. Otherwise, wait for it to be hidden.
*
* @param {CssRuleView} view
* The instance of the Rules view
* @param {String} selectorText
* The selector of the CSS rule to look for
* @param {Number} index
* If there are more CSS rules with the same selector, use this index
* to determine which one should be retrieved. Defaults to 0 (first)
*/
async function clickSelectorIcon(view, selectorText, index = 0) {
const { inspector } = view;
const rule = getRuleViewRule(view, selectorText, index);
info(`Waiting for icon to be available for selector: ${selectorText}`);
const icon = await waitFor(() => {
return rule.querySelector(".js-toggle-selector-highlighter");
});
// Grab the actual selector associated with the matched icon.
// For inline styles, the CSS rule with the "element" selector actually points to
// a generated unique selector, for example: "div:nth-child(1)".
// The selector highlighter is invoked with this unique selector.
// Continuing to use selectorText ("element") would fail some of the checks below.
const selector = icon.dataset.computedSelector;
const { waitForHighlighterTypeShown, waitForHighlighterTypeHidden } =
getHighlighterTestHelpers(inspector);
// If there is an active selector highlighter, get its configuration options.
// Will be undefined if there isn't an active selector highlighter.
const options = inspector.highlighters.getOptionsForActiveHighlighter(
inspector.highlighters.TYPES.SELECTOR
);
// If there is already a highlighter visible for this selector,
// wait for hidden event. Otherwise, wait for shown event.
const waitForEvent =
options?.selector === selector
? waitForHighlighterTypeHidden(inspector.highlighters.TYPES.SELECTOR)
: waitForHighlighterTypeShown(inspector.highlighters.TYPES.SELECTOR);
// Boolean flag whether we waited for a highlighter shown event
const waitedForShown = options?.selector !== selector;
info(`Click the icon for selector: ${selectorText}`);
icon.scrollIntoView();
EventUtils.synthesizeMouseAtCenter(icon, {}, view.styleWindow);
// Promise resolves with event data from either highlighter shown or hidden event.
const data = await waitForEvent;
return { ...data, isShown: waitedForShown };
}
/**
* 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.
*/
async 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");
await onMutation;
}
/**
* Verify the content of the class-panel.
* @param {CssRuleView} view The rule-view instance
* @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`
);
}
}
/**
* Opens the eyedropper from the colorpicker tooltip
* by selecting the colorpicker and then selecting the eyedropper icon
* @param {view} ruleView
* @param {swatch} color swatch of a particular property
*/
async function openEyedropper(view, swatch) {
const tooltip = view.tooltips.getTooltip("colorPicker").tooltip;
info("Click on the swatch");
const onColorPickerReady = view.tooltips
.getTooltip("colorPicker")
.once("ready");
EventUtils.synthesizeMouseAtCenter(swatch, {}, swatch.ownerGlobal);
await onColorPickerReady;
const dropperButton = tooltip.container.querySelector("#eyedropper-button");
info("Click on the eyedropper icon");
const onOpened = tooltip.once("eyedropper-opened");
dropperButton.click();
await onOpened;
}
/**
* Gets a set of declarations for a rule index.
*
* @param {ruleView} view
* The rule-view instance.
* @param {Number} ruleIndex
* The index we expect the rule to have in the rule-view. If an array, the first
* item is the children index in the rule view, and the second item is the child
* node index in the retrieved rule view element. This is helpful to select rules
* inside the pseudo element section.
* @param {boolean} addCompatibilityData
* Optional argument to add compatibility dat with the property data
*
* @returns A Promise that resolves with a Map containing stringified property declarations e.g.
* [
* {
* "color:red":
* {
* propertyName: "color",
* propertyValue: "red",
* warning: "This won't work",
* used: true,
* compatibilityData: {
* isCompatible: true,
* },
* }
* },
* ...
* ]
*/
async function getPropertiesForRuleIndex(
view,
ruleIndex,
addCompatibilityData = false
) {
const declaration = new Map();
let nodeIndex;
if (Array.isArray(ruleIndex)) {
[ruleIndex, nodeIndex] = ruleIndex;
}
const ruleEditor = getRuleViewRuleEditor(view, ruleIndex, nodeIndex);
for (const currProp of ruleEditor?.rule?.textProps || []) {
const icon = currProp.editor.unusedState;
const unused = currProp.editor.element.classList.contains("unused");
let compatibilityData;
let compatibilityIcon;
if (addCompatibilityData) {
compatibilityData = await currProp.isCompatible();
compatibilityIcon = currProp.editor.compatibilityState;
}
declaration.set(`${currProp.name}:${currProp.value}`, {
propertyName: currProp.name,
propertyValue: currProp.value,
icon,
data: currProp.isUsed(),
warning: unused,
used: !unused,
...(addCompatibilityData
? {
compatibilityData,
compatibilityIcon,
}
: {}),
});
}
return declaration;
}
/**
* Toggle a declaration disabled or enabled.
*
* @param {ruleView} view
* The rule-view instance
* @param {Number} ruleIndex
* The index of the CSS rule where we can find the declaration to be
* toggled.
* @param {Object} declaration
* An object representing the declaration e.g. { color: "red" }.
*/
async function toggleDeclaration(view, ruleIndex, declaration) {
const textProp = getTextProperty(view, ruleIndex, declaration);
const [[name, value]] = Object.entries(declaration);
const dec = `${name}:${value}`;
ok(textProp, `Declaration "${dec}" found`);
const newStatus = textProp.enabled ? "disabled" : "enabled";
info(`Toggling declaration "${dec}" of rule ${ruleIndex} to ${newStatus}`);
await togglePropStatus(view, textProp);
info("Toggled successfully.");
}
/**
* Update a declaration from a CSS rule in the Rules view
* by changing its property name, property value or both.
*
* @param {RuleView} view
* Instance of RuleView.
* @param {Number} ruleIndex
* The index of the CSS rule where to find the declaration.
* @param {Object} declaration
* An object representing the target declaration e.g. { color: red }.
* @param {Object} newDeclaration
* An object representing the desired updated declaration e.g. { display: none }.
*/
async function updateDeclaration(
view,
ruleIndex,
declaration,
newDeclaration = {}
) {
const textProp = getTextProperty(view, ruleIndex, declaration);
const [[name, value]] = Object.entries(declaration);
const [[newName, newValue]] = Object.entries(newDeclaration);
if (newName && name !== newName) {
info(
`Updating declaration ${name}:${value};
Changing ${name} to ${newName}`
);
await renameProperty(view, textProp, newName);
}
if (newValue && value !== newValue) {
info(
`Updating declaration ${name}:${value};
Changing ${value} to ${newValue}`
);
await setProperty(view, textProp, newValue);
}
}
/**
* Check whether the given CSS declaration is compatible or not
*
* @param {ruleView} view
* The rule-view instance.
* @param {Number} ruleIndex
* The index we expect the rule to have in the rule-view.
* @param {Object} declaration
* An object representing the declaration e.g. { color: "red" }.
* @param {Object} options
* @param {string | undefined} options.expected
* Expected message ID for the given incompatible property.
* If the expected message is not specified (undefined), the given declaration
* is inferred as cross-browser compatible and is tested for same.
* @param {string | null | undefined} options.expectedLearnMoreUrl
* Expected learn more link. Pass `null` to check that no "Learn more" link is displayed.
*/
async function checkDeclarationCompatibility(
view,
ruleIndex,
declaration,
{ expected, expectedLearnMoreUrl }
) {
const declarations = await getPropertiesForRuleIndex(view, ruleIndex, true);
const [[name, value]] = Object.entries(declaration);
const dec = `${name}:${value}`;
const { compatibilityData } = declarations.get(dec);
is(
!expected,
compatibilityData.isCompatible,
`"${dec}" has the correct compatibility status in the payload`
);
is(compatibilityData.msgId, expected, `"${dec}" has expected message ID`);
if (expected) {
await checkInteractiveTooltip(
view,
"compatibility-tooltip",
ruleIndex,
declaration
);
}
if (expectedLearnMoreUrl !== undefined) {
// Show the tooltip
const tooltip = view.tooltips.getTooltip("interactiveTooltip");
const onTooltipReady = tooltip.once("shown");
const { compatibilityIcon } = declarations.get(dec);
await view.tooltips.onInteractiveTooltipTargetHover(compatibilityIcon);
tooltip.show(compatibilityIcon);
await onTooltipReady;
const learnMoreEl = tooltip.panel.querySelector(".link");
if (expectedLearnMoreUrl === null) {
ok(!learnMoreEl, `"${dec}" has no "Learn more" link`);
} else {
ok(learnMoreEl, `"${dec}" has a "Learn more" link`);
const { link } = await simulateLinkClick(learnMoreEl);
is(
link,
expectedLearnMoreUrl,
`Click on ${dec} "Learn more" link navigates user to expected url`
);
}
// Hide the tooltip.
const onTooltipHidden = tooltip.once("hidden");
tooltip.hide();
await onTooltipHidden;
}
}
/**
* Check that a declaration is marked inactive and that it has the expected
* warning.
*
* @param {ruleView} view
* The rule-view instance.
* @param {Number} ruleIndex
* The index we expect the rule to have in the rule-view.
* @param {Object} declaration
* An object representing the declaration e.g. { color: "red" }.
*/
async function checkDeclarationIsInactive(view, ruleIndex, declaration) {
const declarations = await getPropertiesForRuleIndex(view, ruleIndex);
const [[name, value]] = Object.entries(declaration);
const dec = `${name}:${value}`;
const { used, warning } = declarations.get(dec);
ok(!used, `"${dec}" is inactive`);
ok(warning, `"${dec}" has a warning`);
await checkInteractiveTooltip(
view,
"inactive-css-tooltip",
ruleIndex,
declaration
);
}
/**
* Check that a declaration is marked active.
*
* @param {ruleView} view
* The rule-view instance.
* @param {Number|Array} ruleIndex
* The index we expect the rule to have in the rule-view. If an array, the first
* item is the children index in the rule view, and the second item is the child
* node index in the retrieved rule view element. This is helpful to select rules
* inside the pseudo element section.
* @param {Object} declaration
* An object representing the declaration e.g. { color: "red" }.
*/
async function checkDeclarationIsActive(view, ruleIndex, declaration) {
const declarations = await getPropertiesForRuleIndex(view, ruleIndex);
const [[name, value]] = Object.entries(declaration);
const dec = `${name}:${value}`;
const { used, warning } = declarations.get(dec);
ok(used, `${dec} is active`);
ok(!warning, `${dec} has no warning`);
}
/**
* Check that a tooltip contains the correct value.
*
* @param {ruleView} view
* The rule-view instance.
* @param {string} type
* The interactive tooltip type being tested.
* @param {Number} ruleIndex
* The index we expect the rule to have in the rule-view.
* @param {Object} declaration
* An object representing the declaration e.g. { color: "red" }.
*/
async function checkInteractiveTooltip(view, type, ruleIndex, declaration) {
// Get the declaration
const declarations = await getPropertiesForRuleIndex(
view,
ruleIndex,
type === "compatibility-tooltip"
);
const [[name, value]] = Object.entries(declaration);
const dec = `${name}:${value}`;
// Get the relevant icon and tooltip payload data
let icon;
let data;
if (type === "inactive-css-tooltip") {
({ icon, data } = declarations.get(dec));
} else {
const { compatibilityIcon, compatibilityData } = declarations.get(dec);
icon = compatibilityIcon;
data = compatibilityData;
}
// Get the tooltip.
const tooltip = view.tooltips.getTooltip("interactiveTooltip");
// Get the necessary tooltip helper to fetch the Fluent template.
let tooltipHelper;
if (type === "inactive-css-tooltip") {
tooltipHelper = view.tooltips.inactiveCssTooltipHelper;
} else {
tooltipHelper = view.tooltips.compatibilityTooltipHelper;
}
// Get the HTML template.
const template = tooltipHelper.getTemplate(data, tooltip);
// Translate the template using Fluent.
const { doc } = tooltip;
await doc.l10n.translateFragment(template);
// Get the expected HTML content of the now translated template.
const expected = template.firstElementChild.outerHTML;
// Show the tooltip for the correct icon.
const onTooltipReady = tooltip.once("shown");
await view.tooltips.onInteractiveTooltipTargetHover(icon);
tooltip.show(icon);
await onTooltipReady;
// Get the tooltip's actual HTML content.
const actual = tooltip.panel.firstElementChild.outerHTML;
// Hide the tooltip.
const onTooltipHidden = tooltip.once("hidden");
tooltip.hide();
await onTooltipHidden;
// Finally, check the values.
is(actual, expected, "Tooltip contains the correct value.");
}
/**
* CSS compatibility test runner.
*
* @param {ruleView} view
* The rule-view instance.
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox.
* @param {Array} tests
* An array of test object for this method to consume e.g.
* [
* {
* selector: "#flex-item",
* rules: [
* // Rule Index: 0
* {
* // If the object doesn't include the "expected"
* // key, we consider the declaration as
* // cross-browser compatible and test for same
* color: { value: "green" },
* },
* // Rule Index: 1
* {
* cursor:
* {
* value: "grab",
* expected: INCOMPATIBILITY_TOOLTIP_MESSAGE.default,
* },
* },
* ],
* },
* ...
* ]
*/
async function runCSSCompatibilityTests(view, inspector, tests) {
for (const test of tests) {
if (test.selector) {
await selectNode(test.selector, inspector);
}
for (const [ruleIndex, rules] of test.rules.entries()) {
for (const rule in rules) {
await checkDeclarationCompatibility(
view,
ruleIndex,
{
[rule]: rules[rule].value,
},
{
expected: rules[rule].expected,
expectedLearnMoreUrl: rules[rule].expectedLearnMoreUrl,
}
);
}
}
}
}
/**
* Inactive CSS test runner.
*
* @param {ruleView} view
* The rule-view instance.
* @param {InspectorPanel} inspector
* The instance of InspectorPanel currently loaded in the toolbox.
* @param {Array} tests
* An array of test object for this method to consume e.g.
* [
* {
* selector: "#flex-item",
* // or
* selectNode: (inspector) => { // custom select logic }
* activeDeclarations: [
* {
* declarations: {
* "order": "2",
* },
* ruleIndex: 0,
* },
* {
* declarations: {
* "flex-basis": "auto",
* "flex-grow": "1",
* "flex-shrink": "1",
* },
* ruleIndex: 1,
* },
* ],
* inactiveDeclarations: [
* {
* declaration: {
* "flex-direction": "row",
* },
* ruleIndex: [1, 0],
* },
* ],
* },
* ...
* ]
*/
async function runInactiveCSSTests(view, inspector, tests) {
for (const test of tests) {
if (test.selector) {
await selectNode(test.selector, inspector);
} else if (typeof test.selectNode === "function") {
await test.selectNode(inspector);
}
if (test.activeDeclarations) {
info("Checking whether declarations are marked as used.");
for (const activeDeclarations of test.activeDeclarations) {
for (const [name, value] of Object.entries(
activeDeclarations.declarations
)) {
await checkDeclarationIsActive(view, activeDeclarations.ruleIndex, {
[name]: value,
});
}
}
}
if (test.inactiveDeclarations) {
info("Checking that declarations are unused and have a warning.");
for (const inactiveDeclaration of test.inactiveDeclarations) {
await checkDeclarationIsInactive(
view,
inactiveDeclaration.ruleIndex,
inactiveDeclaration.declaration
);
}
}
}
}
/**
* Return the checkbox element from the Rules view corresponding
* to the given pseudo-class.
*
* @param {Object} view
* Instance of RuleView.
* @param {String} pseudo
* Pseudo-class, like :hover, :active, :focus, etc.
* @return {HTMLElement}
*/
function getPseudoClassCheckbox(view, pseudo) {
return view.pseudoClassCheckboxes.filter(
checkbox => checkbox.value === pseudo
)[0];
}
/**
* Check that the CSS variable output has the expected class name and data attribute.
*
* @param {RulesView} view
* The RulesView instance.
* @param {String} selector
* Selector name for a rule. (e.g. "div", "div::before" and ".sample" etc);
* @param {String} propertyName
* Property name (e.g. "color" and "padding-top" etc);
* @param {String} expectedClassName
* The class name the variable should have.
* @param {String} expectedDatasetValue
* The variable data attribute value.
*/
function checkCSSVariableOutput(
view,
selector,
propertyName,
expectedClassName,
expectedDatasetValue
) {
const target = getRuleViewProperty(
view,
selector,
propertyName
).valueSpan.querySelector(`.${expectedClassName}`);
ok(target, "The target element should exist");
is(target.dataset.variable, expectedDatasetValue);
}
/**
* Return specific rule ancestor data element (i.e. the one containing @layer / @media
* information) from the Rules view
*
* @param {RulesView} view
* The RulesView instance.
* @param {Number} ruleIndex
* @returns {HTMLElement}
*/
function getRuleViewAncestorRulesDataElementByIndex(view, ruleIndex) {
return view.styleDocument
.querySelectorAll(`.ruleview-rule`)
[ruleIndex]?.querySelector(`.ruleview-rule-ancestor-data`);
}
/**
* Return specific rule ancestor data text from the Rules view.
* Will return something like "@layer topLayer\n@media screen\n@layer".
*
* @param {RulesView} view
* The RulesView instance.
* @param {Number} ruleIndex
* @returns {String}
*/
function getRuleViewAncestorRulesDataTextByIndex(view, ruleIndex) {
return getRuleViewAncestorRulesDataElementByIndex(view, ruleIndex)?.innerText;
}
/**
* Runs a sequence of tests against the provided property editor.
*
* @param {TextPropertyEditor} propertyEditor
* The TextPropertyEditor instance to test.
* @param {RuleView} view
* The RuleView which owns the propertyEditor.
* @param {Array<object>} test
* The array of tests to run.
*/
async function runIncrementTest(propertyEditor, view, tests) {
propertyEditor.valueSpan.scrollIntoView();
const editor = await focusEditableField(view, propertyEditor.valueSpan);
for (const testIndex in tests) {
await testIncrement(editor, view, tests[testIndex], testIndex);
}
// Blur the field to put back the UI in its initial state (and avoid pending
// requests when the test ends).
const onRuleViewChanged = view.once("ruleview-changed");
EventUtils.synthesizeKey("VK_ESCAPE", {}, view.styleWindow);
view.debounce.flush();
await onRuleViewChanged;
}
/**
* Individual test runner for increment tests used via runIncrementTest in
* browser_rules_edit-property-increments.js and similar tests.
*
* Will attempt to increment the value of the provided inplace editor based on
* the test options provided.
*
* @param {InplaceEditor} editor
* The InplaceEditor instance to test.
* @param {RuleView} view
* The RuleView which owns the editor.
* @param {object} test
* @param {boolean=} test.alt
* Whether alt should be depressed.
* @param {boolean=} test.ctrl
* Whether ctrl should be depressed.
* @param {number=} test.deltaX
* Only relevant if test.wheel=true, value of the wheel delta on the horizontal axis.
* @param {number=} test.deltaY
* Only relevant if test.wheel=true, value of the wheel delta on the vertical axis.
* @param {boolean=} test.down
* For key increment tests, whether this should simulate pressing the down
* arrow, or the up arrow. down, pagedown and pageup are mutually exclusive.
* @param {string} test.end
* The expected value at the end of the test.
* @param {boolean=} test.pagedown
* For key increment tests, whether this should simulate pressing the
* pagedown key. down, pagedown and pageup are mutually exclusive.
* @param {boolean=} test.pageup
* For key increment tests, whether this should simulate pressing the
* pageup key. down, pagedown and pageup are mutually exclusive.
* @param {boolean=} test.selectAll
* Whether all the input text should be selected. You can also specify a
* range with test.selection.
* @param {Array<number>=} test.selection
* An array of two numbers which corresponds to the initial selection range.
* @param {boolean=} test.shift
* Whether shift should be depressed.
* @param {string} test.start
* The input value at the beginning of the test.
* @param {boolean=} test.wheel
* True if the test should use wheel events to increment the value.
* @param {number} testIndex
* The test index, used for logging.
*/
async function testIncrement(editor, view, test, testIndex) {
editor.input.value = test.start;
const input = editor.input;
if (test.selectAll) {
input.select();
} else if (test.selection) {
input.setSelectionRange(test.selection[0], test.selection[1]);
}
is(input.value, test.start, "Value initialized at " + test.start);
const onRuleViewChanged = view.once("ruleview-changed");
let smallIncrementKey = { ctrlKey: test.ctrl };
if (AppConstants.platform === "macosx") {
smallIncrementKey = { altKey: test.alt };
}
const options = {
shiftKey: test.shift,
...smallIncrementKey,
};
if (test.wheel) {
// If test.wheel is true, we should increment the value using the wheel.
const onWheel = once(input, "wheel");
input.dispatchEvent(
new view.styleWindow.WheelEvent("wheel", {
deltaX: test.deltaX,
deltaY: test.deltaY,
deltaMode: 0,
...options,
})
);
await onWheel;
} else {
let key;
key = test.down ? "VK_DOWN" : "VK_UP";
if (test.pageDown) {
key = "VK_PAGE_DOWN";
} else if (test.pageUp) {
key = "VK_PAGE_UP";
}
const onKeyUp = once(input, "keyup");
EventUtils.synthesizeKey(key, options, view.styleWindow);
await onKeyUp;
}
// Only expect a change if the value actually changed!
if (test.start !== test.end) {
view.debounce.flush();
await onRuleViewChanged;
}
is(input.value, test.end, `[Test ${testIndex}] Value changed to ${test.end}`);
}
function getSmallIncrementKey() {
if (AppConstants.platform === "macosx") {
return { alt: true };
}
return { ctrl: true };
}