Skip to content

Commit

Permalink
Bug 1611817 - Add mochitests for the performance-new panel in DevTool…
Browse files Browse the repository at this point in the history
…s; r=julienw

Note that this patch also updates the setReactFriendlyInputValue function in
head.js, as it was not working for selects.

Differential Revision: https://phabricator.services.mozilla.com/D65151

--HG--
extra : moz-landing-system : lando
  • Loading branch information
gregtatum committed Mar 9, 2020
1 parent 68c5d4c commit 45cedbd
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 21 deletions.
4 changes: 3 additions & 1 deletion devtools/client/performance-new/test/browser/browser.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ support-files =
[browser_aboutprofiling-threads-behavior.js]
[browser_aboutprofiling-presets.js]
[browser_aboutprofiling-presets-custom.js]
[browser_devtools-presets.js]
[browser_devtools-record-capture.js]
[browser_devtools-record-discard.js]
[browser_webchannel-enable-menu-button.js]

[browser_popup-record-capture.js]
[browser_popup-record-discard.js]
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

add_task(async function test() {
info("Test that about:profiling presets configure the profiler");

if (!Services.profiler.GetFeatures().includes("stackwalk")) {
ok(true, "This platform does not support stackwalking, skip this test.");
return;
}

await withDevToolsPanel(async document => {
{
const presets = await getNearestInputFromText(document, "Settings");

is(presets.value, "web-developer", "The presets default to webdev mode.");
ok(
!(await devToolsActiveConfigurationHasFeature(document, "stackwalk")),
"Stack walking is not used in Web Developer mode."
);
}

{
const presets = await getNearestInputFromText(document, "Settings");
setReactFriendlyInputValue(presets, "firefox-platform");
is(
presets.value,
"firefox-platform",
"The preset was changed to Firefox Platform"
);
ok(
await devToolsActiveConfigurationHasFeature(document, "stackwalk"),
"Stack walking is used in Firefox Platform mode."
);
}
});

const { revertRecordingPreferences } = ChromeUtils.import(
"resource://devtools/client/performance-new/popup/background.jsm.js"
);

revertRecordingPreferences();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

add_task(async function test() {
info("Test that DevTools can capture profiles.");

await setProfilerFrontendUrl(
"http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html"
);

await withDevToolsPanel(async document => {
{
const button = await getActiveButtonFromText(document, "Start recording");
info("Click the button to start recording");
button.click();
}

{
const button = await getActiveButtonFromText(
document,
"Capture recording"
);
info("Click the button to capture the recording.");
button.click();
}

info(
"If the DevTools successfully injects a profile into the page, then the " +
"fake frontend will rename the title of the page."
);

await checkTabLoadedProfile({
initialTitle: "Waiting on the profile",
successTitle: "Profile received",
errorTitle: "Error",
});
});

const { revertRecordingPreferences } = ChromeUtils.import(
"resource://devtools/client/performance-new/popup/background.jsm.js"
);

revertRecordingPreferences();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

add_task(async function test() {
info("Test that DevTools can capture profiles.");

await setProfilerFrontendUrl(
"http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html"
);

await withDevToolsPanel(async document => {
{
const button = await getActiveButtonFromText(document, "Start recording");
info("Click the button to start recording");
button.click();
}

{
const button = await getActiveButtonFromText(
document,
"Cancel recording"
);
info("Click the button to discard to profile.");
button.click();
}

{
const button = await getActiveButtonFromText(document, "Start recording");
ok(Boolean(button), "The start recording button is available again.");
}
});

const { revertRecordingPreferences } = ChromeUtils.import(
"resource://devtools/client/performance-new/popup/background.jsm.js"
);

revertRecordingPreferences();
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,14 @@
if (
profile &&
typeof profile === 'object' &&
profile instanceof ArrayBuffer
(
// The popup injects the compressed profile as an ArrayBuffer.
(profile instanceof ArrayBuffer) ||
// DevTools injects the profile as just the plain object, although
// maybe in the future it could also do it as a compressed profile
// to make this faster.
Object.keys(profile).includes("threads")
)
) {
// The profile looks good!
document.title = successTitle;
Expand Down
141 changes: 122 additions & 19 deletions devtools/client/performance-new/test/browser/head.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,11 @@ async function toggleOpenProfilerPopup() {
* @returns {Promise}
*/
function setProfilerFrontendUrl(url) {
info("Setting the profiler URL to the fake frontend.");
info(
"Setting the profiler URL to the fake frontend. Note that this doesn't currently " +
"support the WebChannels, so expect a few error messages about the WebChannel " +
"URLs not being correct."
);
return SpecialPowers.pushPrefEnv({
set: [
// Make sure observer and testing function run in the same process
Expand Down Expand Up @@ -331,6 +335,41 @@ function withAboutProfiling(callback) {
);
}

/**
* Open DevTools and view the performance-new tab. After running the callback, clean
* up the test.
*
* @template T
* @param {(Document) => T} callback
* @returns {Promise<T>}
*/
async function withDevToolsPanel(callback) {
SpecialPowers.pushPrefEnv({
set: [["devtools.performance.new-panel-enabled", "true"]],
});

const { gDevTools } = require("devtools/client/framework/devtools");
const { TargetFactory } = require("devtools/client/framework/target");

info("Create a new about:blank tab.");
const tab = BrowserTestUtils.addTab(gBrowser, "about:blank");

info("Begin to open the DevTools and the performance-new panel.");
const target = await TargetFactory.forTab(tab);
const toolbox = await gDevTools.showToolbox(target, "performance");

const { document } = toolbox.getCurrentPanel().panelWin;

info("The performance-new panel is now open and ready to use.");
await callback(document);

info("About to remove the about:blank tab");
await toolbox.destroy();
BrowserTestUtils.removeTab(tab);
info("The about:blank tab is now removed.");
await new Promise(resolve => setTimeout(resolve, 500));
}

/**
* Start and stop the profiler to get the current active configuration. This is
* done programmtically through the nsIProfiler interface, rather than through click
Expand Down Expand Up @@ -389,8 +428,43 @@ function activeConfigurationHasThread(thread) {
}

/**
* Grabs the associated input from the element, or it walks up the DOM from a text
* element and tries to query select an input.
* Use user driven events to start the profiler, and then get the active configuration
* of the profiler. This is similar to functions in the head.js file, but is specific
* for the DevTools situation. The UI complains if the profiler stops unexpectedly.
*
* @param {Document} document
* @param {string} feature
* @returns {boolean}
*/
async function devToolsActiveConfigurationHasFeature(document, feature) {
info("Get the active configuration of the profiler via user driven events.");
const start = await getActiveButtonFromText(document, "Start recording");
info("Click the button to start recording.");
start.click();

// Get the cancel button first, so that way we know the profile has actually
// been recorded.
const cancel = await getActiveButtonFromText(document, "Cancel recording");

const { activeConfiguration } = Services.profiler;
if (!activeConfiguration) {
throw new Error(
"Expected to find an active configuration for the profile."
);
}

info("Click the cancel button to discard the profile..");
cancel.click();

// Wait until the start button is back.
await getActiveButtonFromText(document, "Start recording");

return activeConfiguration.features.includes(feature);
}

/**
* Selects an element with some given text, then it walks up the DOM until it finds
* an input or select element via a call to querySelector.
*
* @param {Document} document
* @param {string} text
Expand All @@ -405,12 +479,40 @@ async function getNearestInputFromText(document, text) {
// A non-label node
let next = textElement;
while ((next = next.parentElement)) {
const input = next.querySelector("input");
const input = next.querySelector("input, select");
if (input) {
return input;
}
}
throw new Error("Could not find an input near text element.");
throw new Error("Could not find an input or select near the text element.");
}

/**
* Grabs the closest button element from a given snippet of text, and make sure
* the button is not disabled.
*
* @param {Document} document
* @param {string} text
* @param {HTMLButtonElement}
*/
async function getActiveButtonFromText(document, text) {
// This could select a span inside the button, or the button itself.
let button = await getElementFromDocumentByText(document, text);

while (button.tagName !== "button") {
// Walk up until a button element is found.
button = button.parentElement;
if (!button) {
throw new Error(`Unable to find a button from the text "${text}"`);
}
}

await waitUntil(
() => !button.disabled,
"Waiting until the button is not disabled."
);

return button;
}

/**
Expand Down Expand Up @@ -473,20 +575,21 @@ function withWebChannelTestDocument(callback) {
/**
* Set a React-friendly input value. Doing this the normal way doesn't work.
*
* See: https://github.com/facebook/react/issues/10135#issuecomment-314441175
* See https://github.com/facebook/react/issues/10135#issuecomment-500929024
*
* @param {HTMLInputElement} input
* @param {string} value
*/
function setReactFriendlyInputValue(element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, "value").set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(
prototype,
"value"
).set;

if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value);
} else {
valueSetter.call(element, value);
function setReactFriendlyInputValue(input, value) {
const previousValue = input.value;

input.value = value;

const tracker = input._valueTracker;
if (tracker) {
tracker.setValue(previousValue);
}
element.dispatchEvent(new Event("input", { bubbles: true }));

// 'change' instead of 'input', see https://github.com/facebook/react/issues/11488#issuecomment-381590324
input.dispatchEvent(new Event("change", { bubbles: true }));
}

0 comments on commit 45cedbd

Please sign in to comment.