/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"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
);
/**
* Dispatch the copy event on the given element
*/
function fireCopyEvent(element) {
const evt = element.ownerDocument.createEvent(
"Event");
evt.initEvent(
"copy",
true,
true);
element.dispatchEvent(evt);
}
/**
* Return all the computed items in the computed view
*
* @param {CssComputedView} view
* The instance of the computed view panel
* @returns {Array<Element>}
*/
function getComputedViewProperties(view) {
return Array.from(
view.styleDocument.querySelectorAll(
"#computed-container .computed-property-view"
)
);
}
/**
* 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 (
const property of getComputedViewProperties(view)) {
const nameSpan = property.querySelector(
".computed-property-name");
const valueSpan = property.querySelector(
".computed-property-value");
if (nameSpan.firstChild.textContent === name) {
prop = { nameSpan, valueSpan };
break;
}
}
return prop;
}
/**
* Get an instance of PropertyView from the computed-view.
*
* @param {CssComputedView} view
* The instance of the computed view panel
* @param {string} name
* The name of the property to retrieve
* @return {PropertyView}
*/
function getComputedViewPropertyView(view, name) {
return view.propertyViews.find(propertyView => propertyView.name === name);
}
/**
* Get a reference to the matched rules element for a given property name in
* the computed-view.
* A matched rule element is inside the property element (<li>) itself
* and is only shown when the twisty icon is expanded on the property.
* It contains matched rules, with selectors, properties, values and stylesheet links.
*
* @param {CssComputedView} view
* The instance of the computed view panel
* @param {string} name
* The name of the property to retrieve
* @return {Promise} A promise that resolves to the property matched rules
* container
*/
var getComputedViewMatchedRules = async
function (view, name) {
let expander;
let matchedRulesEl;
for (
const property of view.styleDocument.querySelectorAll(
"#computed-container .computed-property-view"
)) {
const nameSpan = property.querySelector(
".computed-property-name");
if (nameSpan.firstChild.textContent === name) {
expander = property.querySelector(
".computed-expandable");
matchedRulesEl = property.querySelector(
".matchedselectors");
break;
}
}
if (!expander.hasAttribute(
"open")) {
// Need to expand the property
const onExpand = view.inspector.once(
"computed-view-property-expanded");
expander.click();
await onExpand;
await waitFor(() => expander.hasAttribute(
"open"));
}
return matchedRulesEl;
};
/**
* 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) {
return getComputedViewProperty(view, name).valueSpan.textContent;
}
/**
* Expand a given property, given its index in the current property list of
* the computed view
*
* @param {CssComputedView} view
* The instance of the computed view panel
* @param {number} index
* The index of the property to be expanded
* @return a promise that resolves when the property has been expanded, or
* rejects if the property was not found
*/
function expandComputedViewPropertyByIndex(view, index) {
info(
"Expanding property " + index +
" in the computed view");
const expandos = view.styleDocument.querySelectorAll(
".computed-expandable");
if (!expandos.length || !expandos[index]) {
return Promise.reject();
}
const onExpand = view.inspector.once(
"computed-view-property-expanded");
expandos[index].click();
return onExpand;
}
/**
* Get a rule-link from the computed-view given its index
*
* @param {CssComputedView} view
* The instance of the computed view panel
* @param {number} index
* The index of the matched selector element
* @return {DOMNode} The link at the given index, if one exists, null otherwise
*/
function getComputedViewLinkByIndex(view, index) {
const matchedSelectors = view.styleDocument.querySelectorAll(
".matchedselectors > p"
);
const matchedSelector = matchedSelectors[index];
if (!matchedSelector) {
return null;
}
return matchedSelector.querySelector(`.rule-link .computed-link`);
}
/**
* Trigger the select all action in the computed view.
*
* @param {CssComputedView} view
* The instance of the computed view panel
*/
function selectAllText(view) {
info(
"Selecting all the text");
view.contextMenu._onSelectAll();
}
/**
* Select all the text, copy it, and check the content in the clipboard.
*
* @param {CssComputedView} view
* The instance of the computed view panel
* @param {string} expectedPattern
* A regular expression used to check the content of the clipboard
*/
async
function copyAllAndCheckClipboard(view, expectedPattern) {
selectAllText(view);
const contentDoc = view.styleDocument;
const prop = contentDoc.querySelector(
"#computed-container .computed-property-view"
);
try {
info(
"Trigger a copy event and wait for the clipboard content");
await waitForClipboardPromise(
() => fireCopyEvent(prop),
() => checkClipboard(expectedPattern)
);
}
catch (e) {
failClipboardCheck(expectedPattern);
}
}
/**
* Select some text, copy it, and check the content in the clipboard.
*
* @param {CssComputedView} view
* The instance of the computed view panel
* @param {object} positions
* The start and end positions of the text to be selected. This must be an object
* like this:
* { start: {prop: 1, offset: 0}, end: {prop: 3, offset: 5} }
* @param {string} expectedPattern
* A regular expression used to check the content of the clipboard
*/
async
function copySomeTextAndCheckClipboard(view, positions, expectedPattern) {
info(
"Testing selection copy");
const contentDocument = view.styleDocument;
const props = contentDocument.querySelectorAll(
"#computed-container .computed-property-view"
);
info(
"Create the text selection range");
const range = contentDocument.createRange();
range.setStart(props[positions.start.prop], positions.start.offset);
range.setEnd(props[positions.end.prop], positions.end.offset);
contentDocument.defaultView.getSelection().addRange(range);
try {
info(
"Trigger a copy event and wait for the clipboard content");
await waitForClipboardPromise(
() => fireCopyEvent(props[
0]),
() => checkClipboard(expectedPattern)
);
}
catch (e) {
failClipboardCheck(expectedPattern);
}
}
function checkClipboard(expectedPattern) {
const actual = SpecialPowers.getClipboardData(
"text/plain");
const expectedRegExp =
new RegExp(expectedPattern,
"g");
return expectedRegExp.test(actual);
}
function failClipboardCheck(expectedPattern) {
// Format expected text for comparison
const terminator = Services.appinfo.OS ==
"WINNT" ?
"\r\n" :
"\n";
expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator);
expectedPattern = expectedPattern.replace(/\\\(/g,
"(");
expectedPattern = expectedPattern.replace(/\\\)/g,
")");
let actual = SpecialPowers.getClipboardData(
"text/plain");
// Trim the right hand side of our strings. This is because expectedPattern
// accounts for windows sometimes adding a newline to our copied data.
expectedPattern = expectedPattern.trimRight();
actual = actual.trimRight();
dump(
"TEST-UNEXPECTED-FAIL | Clipboard text does not match expected ... " +
"results (escaped for accurate comparison):\n"
);
info(
"Actual: " + escape(actual));
info(
"Expected: " + escape(expectedPattern));
}
/**
* Check that the given property has the expected value and the expected matched selectors
*
* @param {CssComputedView} view
* The instance of the computed view panel
* @param {object} options
* @param {string} options.property
* The property name to check
* @param {string} options.expectedComputedValue
* The expected value displayed for the property
* @param {object[]} options.expectedMatchedSelectors
* An array of objects describing the expected matched selectors
* @param {string} options.expectedMatchedSelectors[].selector
* The selector that should be displayed at this index
* @param {string} options.expectedMatchedSelectors[].value
* The value that should be displayed at this index
* @param {boolean} options.expectedMatchedSelectors[].match
* Whether the selector should match the currently selected element. Defaults to true.
* @returns {Element[]} The list of selectors element (.rule-text)
*/
async
function checkMatchedSelectorForProperty(
view,
{ property, expectedComputedValue, expectedMatchedSelectors }
) {
const propertyView = getComputedViewPropertyView(view, property);
ok(propertyView, `found PropertyView
for "${property}"`);
const { valueNode } = propertyView;
is(
valueNode.textContent,
expectedComputedValue,
`Expected displayed computed value
for "${property}"`
);
is(propertyView.hasMatchedSelectors,
true,
"hasMatchedSelectors is true");
info(
"Expanding the matched selectors");
propertyView.matchedExpanded =
true;
await propertyView.refreshMatchedSelectors();
const selectorsEl =
propertyView.matchedSelectorsContainer.querySelectorAll(
".rule-text");
is(
selectorsEl.length,
expectedMatchedSelectors.length,
"Expected number of selectors are displayed"
);
selectorsEl.forEach((selectorEl, index) => {
is(
selectorEl.querySelector(
".fix-get-selection").innerText,
expectedMatchedSelectors[index].selector,
`Selector #${index} is the expected one`
);
is(
selectorEl.querySelector(
".computed-other-property-value").innerText,
expectedMatchedSelectors[index].value,
`Selector #${index} (
"${expectedMatchedSelectors[index].selector}") has the expected
"${property}"`
);
const classToMatch = index ===
0 ?
"bestmatch" :
"matched";
const expectedMatch = expectedMatchedSelectors[index].match ??
true;
is(
selectorEl.classList.contains(classToMatch),
expectedMatch,
`Selector #${index} (
"${expectedMatchedSelectors[index].selector}") element does ${ex
pectedMatch ? "" : "not "}have a matching class`
);
});
return selectorsEl;
}