/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */
"use strict";
// shared-head.js handles imports, constants, and utility functions
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/framework/test/head.js",
this
);
const JSON_VIEW_PREF =
"devtools.jsonview.enabled";
// Enable JSON View for the test
Services.prefs.setBoolPref(JSON_VIEW_PREF,
true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref(JSON_VIEW_PREF);
});
// XXX move some API into devtools/shared/test/shared-head.js
/**
* Add a new test tab in the browser and load the given url.
* @param {String} url
* The url to be loaded in the new tab.
*
* @param {Object} [optional]
* An object with the following optional properties:
* - appReadyState: The readyState of the JSON Viewer app that you want to
* wait for. Its value can be one of:
* - "uninitialized": The converter has started the request.
* If JavaScript is disabled, there will be no more readyState changes.
* - "loading": RequireJS started loading the scripts for the JSON Viewer.
* If the load timeouts, there will be no more readyState changes.
* - "interactive": The JSON Viewer app loaded, but possibly not all the JSON
* data has been received.
* - "complete" (default): The app is fully loaded with all the JSON.
* - docReadyState: The standard readyState of the document that you want to
* wait for. Its value can be one of:
* - "loading": The JSON data has not been completely loaded (but the app might).
* - "interactive": All the JSON data has been received.
* - "complete" (default): Since there aren't sub-resources like images,
* behaves as "interactive". Note the app might not be loaded yet.
*/
async
function addJsonViewTab(
url,
{ appReadyState =
"complete", docReadyState =
"complete" } = {}
) {
info(
"Adding a new JSON tab with URL: '" + url +
"'");
const tabAdded = BrowserTestUtils.waitForNewTab(gBrowser, url);
const tabLoaded = addTab(url);
// The `tabAdded` promise resolves when the JSON Viewer starts loading.
// This is usually what we want, however, it never resolves for unrecognized
// content types that trigger a download.
// On the other hand, `tabLoaded` always resolves, but not until the document
// is fully loaded, which is too late if `docReadyState !== "complete"`.
// Therefore, we race both promises.
const tab = await Promise.race([tabAdded, tabLoaded]);
const browser = tab.linkedBrowser;
const rootDir = getRootDirectory(gTestPath);
// Catch RequireJS errors (usually timeouts)
const error = tabLoaded.then(() =>
SpecialPowers.spawn(browser, [],
function () {
return new Promise((resolve, reject) => {
const { requirejs } = content.wrappedJSObject;
if (requirejs) {
requirejs.onError = err => {
info(err);
ok(
false,
"RequireJS error");
reject(err);
};
}
});
})
);
const data = { rootDir, appReadyState, docReadyState };
await Promise.race([
error,
// eslint-disable-next-line no-shadow
ContentTask.spawn(browser, data, async
function (data) {
// Check if there is a JSONView object.
const { JSONView } = content.wrappedJSObject;
if (!JSONView) {
throw new Error(
"The JSON Viewer did not load.");
}
const docReadyStates = [
"loading",
"interactive",
"complete"];
const docReadyIndex = docReadyStates.indexOf(data.docReadyState);
const appReadyStates = [
"uninitialized", ...docReadyStates];
const appReadyIndex = appReadyStates.indexOf(data.appReadyState);
if (docReadyIndex < 0 || appReadyIndex < 0) {
throw new Error(
"Invalid app or doc readyState parameter.");
}
// Wait until the document readyState suffices.
const { document } = content;
while (docReadyStates.indexOf(document.readyState) < docReadyIndex) {
info(
`DocReadyState is
"${document.readyState}". Await
"${data.docReadyState}"`
);
await
new Promise(resolve => {
document.addEventListener(
"readystatechange", resolve, {
once:
true,
});
});
}
// Wait until the app readyState suffices.
while (appReadyStates.indexOf(JSONView.readyState) < appReadyIndex) {
info(
`AppReadyState is
"${JSONView.readyState}". Await
"${data.appReadyState}"`
);
await
new Promise(resolve => {
content.addEventListener(
"AppReadyStateChange", resolve, {
once:
true,
});
});
}
}),
]);
return tab;
}
/**
* Expanding a node in the JSON tree
*/
function clickJsonNode(selector) {
info(
"Expanding node: '" + selector +
"'");
// eslint-disable-next-line no-shadow
return ContentTask.spawn(gBrowser.selectedBrowser, selector, selector => {
content.document.querySelector(selector).click();
});
}
/**
* Select JSON View tab (in the content).
*/
function selectJsonViewContentTab(name) {
info(
"Selecting tab: '" + name +
"'");
// eslint-disable-next-line no-shadow
return ContentTask.spawn(gBrowser.selectedBrowser, name, async name => {
const tabsSelector =
".tabs-menu .tabs-menu-item";
const targetTabSelector = `${tabsSelector}.${CSS.escape(name)}`;
const targetTab = content.document.querySelector(targetTabSelector);
const targetTabIndex = Array.prototype.indexOf.call(
content.document.querySelectorAll(tabsSelector),
targetTab
);
const targetTabButton = targetTab.querySelector(
"a");
await
new Promise(resolve => {
content.addEventListener(
"TabChanged",
({ detail: { index } }) => {
is(index, targetTabIndex,
"Hm?");
if (index === targetTabIndex) {
resolve();
}
},
{ once:
true }
);
targetTabButton.click();
});
is(
targetTabButton.getAttribute(
"aria-selected"),
"true",
"Tab is now selected"
);
});
}
function getElementCount(selector) {
info(
"Get element count: '" + selector +
"'");
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[selector],
selectorChild => {
return content.document.querySelectorAll(selectorChild).length;
}
);
}
function getElementText(selector) {
info(
"Get element text: '" + selector +
"'");
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[selector],
selectorChild => {
const element = content.document.querySelector(selectorChild);
return element ? element.textContent :
null;
}
);
}
function getElementAttr(selector, attr) {
info(
"Get attribute '" + attr +
"' for element '" + selector +
"'");
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[selector, attr],
(selectorChild, attrChild) => {
const element = content.document.querySelector(selectorChild);
return element ? element.getAttribute(attrChild) :
null;
}
);
}
function focusElement(selector) {
info(
"Focus element: '" + selector +
"'");
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[selector],
selectorChild => {
const element = content.document.querySelector(selectorChild);
if (element) {
element.focus();
}
}
);
}
/**
* Send the string aStr to the focused element.
*
* For now this method only works for ASCII characters and emulates the shift
* key state on US keyboard layout.
*/
function sendString(str, selector) {
info(
"Send string: '" + str +
"'");
return SpecialPowers.spawn(
gBrowser.selectedBrowser,
[selector, str],
(selectorChild, strChild) => {
if (selectorChild) {
const element = content.document.querySelector(selectorChild);
if (element) {
element.focus();
}
}
EventUtils.sendString(strChild, content);
}
);
}
function waitForTime(delay) {
return new Promise(resolve => setTimeout(resolve, delay));
}
function waitForFilter() {
return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
return new Promise(resolve => {
const firstRow = content.document.querySelector(
".jsonPanelBox .treeTable .treeRow"
);
// Check if the filter is already set.
if (firstRow.classList.contains(
"hidden")) {
resolve();
return;
}
// Wait till the first row has 'hidden' class set.
const observer =
new content.MutationObserver(
function (mutations) {
for (let i = 0; i < mutations.length; i++) {
const mutation = mutations[i];
if (mutation.attributeName ==
"class") {
if (firstRow.classList.contains(
"hidden")) {
observer.disconnect();
resolve();
break;
}
}
}
});
observer.observe(firstRow, { attributes:
true });
});
});
}
function normalizeNewLines(value) {
return value.replace(
"(\r\n|\n)",
"\n");
}