Bug 1408124 - Fix perf component tests draft
authorGreg Tatum <gtatum@mozilla.com>
Wed, 08 Nov 2017 10:36:43 -0600
changeset 695099 69079a001ddc255791a72199e726be89bb2a9e3e
parent 695086 043a247f303b4b557ee7aba22f5d0967ebea4f2d
child 739523 3d71022358e1f34781006d7ac59a35b1496e6d64
push id88342
push usergtatum@mozilla.com
push dateWed, 08 Nov 2017 19:07:06 +0000
bugs1408124
milestone58.0a1
Bug 1408124 - Fix perf component tests MozReview-Commit-ID: 4DfE7jPpqbx
devtools/client/performance/new/components/Perf.js
devtools/client/performance/new/frame-script.js
devtools/client/performance/new/moz.build
devtools/client/performance/new/test/browser.ini
devtools/client/performance/new/test/browser_perf-state-01.js
devtools/client/performance/new/test/browser_perf-state-02.js
devtools/client/performance/new/test/browser_perf-state-03.js
devtools/client/performance/new/test/chrome/chrome.ini
devtools/client/performance/new/test/chrome/head.js
devtools/client/performance/new/test/chrome/test_perf-state-01.html
devtools/client/performance/new/test/chrome/test_perf-state-02.html
devtools/client/performance/new/test/chrome/test_perf-state-03.html
devtools/client/performance/new/test/chrome/test_perf-state-04.html
devtools/client/performance/new/test/doc_mount_react.html
devtools/client/performance/new/test/head.js
devtools/shared/specs/perf.js
--- a/devtools/client/performance/new/components/Perf.js
+++ b/devtools/client/performance/new/components/Perf.js
@@ -78,25 +78,16 @@ class Perf extends Component {
           recordingState = isActive
             ? OTHER_IS_RECORDING
             : AVAILABLE_TO_RECORD;
         }
       }
       this.setState({ isSupportedPlatform, recordingState });
     });
 
-    // We don't yet know what state the profile is in, find out.
-    this.props.perfFront.isActive().then(isActive => {
-      if (this.state.recordingState === NOT_YET_KNOWN) {
-        this.setState({
-          recordingState: isActive ? OTHER_IS_RECORDING : AVAILABLE_TO_RECORD
-        });
-      }
-    });
-
     // Handle when the profiler changes state. It might be us, it might be someone else.
     this.props.perfFront.on("profiler-started", this.handleProfilerStarting);
     this.props.perfFront.on("profiler-stopped", this.handleProfilerStopping);
     this.props.perfFront.on("profile-locked-for-private-browsing",
       this.handlePrivateBrowsingStarting);
     this.props.perfFront.on("profile-unlocked-from-private-browsing",
       this.handlePrivateBrowsingEnding);
   }
--- a/devtools/client/performance/new/frame-script.js
+++ b/devtools/client/performance/new/frame-script.js
@@ -1,15 +1,19 @@
 /* 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";
-/* eslint-env browser */
 /* global addMessageListener, addEventListener, content */
 
+/**
+ * This frame script injects itself into perf-html.io and injects the profile
+ * into the page. It is mostly taken from the Gecko Profiler Addon implementation.
+ */
+
 const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
 
 let gProfile = null;
 
 addMessageListener(TRANSFER_EVENT, e => {
   gProfile = e.data;
   connectToPage();
   addEventListener("DOMContentLoaded", connectToPage);
@@ -20,54 +24,63 @@ function connectToPage() {
   if (unsafeWindow.connectToGeckoProfiler) {
     unsafeWindow.connectToGeckoProfiler(makeAccessibleToPage({
       getProfile: () => Promise.resolve(gProfile),
       getSymbolTable: (debugName, breakpadId) => getSymbolTable(debugName, breakpadId),
     }, unsafeWindow));
   }
 }
 
+/**
+ * For now, do not try to symbolicate. Reject any attempt.
+ */
 function getSymbolTable(debugName, breakpadId) {
   return new Promise((resolve, reject) => {
     reject();
   });
 }
 
-// Security stuff below, you usually don't need to read this.
+// The following functions handle the security of cloning the object into the page.
 
-// Create a promise that can be used in the page.
+/**
+ * Create a promise that can be used in the page.
+ */
 function createPromiseInPage(fun, contentGlobal) {
   function funThatClonesObjects(resolve, reject) {
     return fun(result => resolve(Components.utils.cloneInto(result, contentGlobal)),
                error => reject(Components.utils.cloneInto(error, contentGlobal)));
   }
   return new contentGlobal.Promise(Components.utils.exportFunction(funThatClonesObjects,
                                                                    contentGlobal));
 }
 
-// Returns a function that calls the original function and tries to make the
-// return value available to the page.
+/**
+ * Returns a function that calls the original function and tries to make the
+ * return value available to the page.
+ */
 function wrapFunction(fun, contentGlobal) {
   return function () {
     let result = fun.apply(this, arguments);
     if (typeof result === "object") {
       if (("then" in result) && (typeof result.then === "function")) {
         // fun returned a promise.
         return createPromiseInPage((resolve, reject) =>
           result.then(resolve, reject), contentGlobal);
       }
       return Components.utils.cloneInto(result, contentGlobal);
     }
     return result;
   };
 }
 
-// Pass a simple object containing values that are objects or functions.
-// The objects or functions are wrapped in such a way that they can be
-// consumed by the page.
+/**
+ * Pass a simple object containing values that are objects or functions.
+ * The objects or functions are wrapped in such a way that they can be
+ * consumed by the page.
+ */
 function makeAccessibleToPage(obj, contentGlobal) {
   let result = Components.utils.createObjectIn(contentGlobal);
   for (let field in obj) {
     switch (typeof obj[field]) {
       case "function":
         Components.utils.exportFunction(
           wrapFunction(obj[field], contentGlobal), result, { defineAs: field });
         break;
--- a/devtools/client/performance/new/moz.build
+++ b/devtools/client/performance/new/moz.build
@@ -7,12 +7,12 @@ DIRS += [
     'components',
 ]
 
 DevToolsModules(
     'initializer.js',
     'panel.js',
 )
 
-BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Developer Tools: Performance Tools (Profiler/Timeline)')
deleted file mode 100644
--- a/devtools/client/performance/new/test/browser.ini
+++ /dev/null
@@ -1,10 +0,0 @@
-[DEFAULT]
-tags = devtools
-subsuite = devtools
-support-files =
-  head.js
-  doc_mount_react.html
-
-[browser_perf-state-01.js]
-[browser_perf-state-02.js]
-[browser_perf-state-03.js]
deleted file mode 100644
--- a/devtools/client/performance/new/test/browser_perf-state-01.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/* 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";
-
-/**
- * Tests the normal workflow of starting and stopping the profiler through the Perf
- * component.
- */
-addPerfTest(async ({ document, browserRequire, mountElement, perfFront }) => {
-  const Perf = browserRequire("devtools/client/performance/new/components/Perf");
-  const React = browserRequire("devtools/client/shared/vendor/react");
-  const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
-
-  // Inject a function which will allow us to receive the profile.
-  let profile;
-  function receiveProfile(profileIn) {
-    profile = profileIn;
-  }
-
-  const element = React.createElement(Perf, { perfFront, receiveProfile });
-  const perfComponent = ReactDOM.render(element, mountElement);
-
-  is(perfComponent.state.recordingState, "not-yet-known",
-    "The component at first is in an unknown state.");
-
-  await perfFront.flushAsyncQueue();
-  is(perfComponent.state.recordingState, "available-to-record",
-    "After talking to the actor, we're ready to record.");
-
-  document.querySelector("button").click();
-  is(perfComponent.state.recordingState, "request-to-start-recording",
-    "Sent in a request to start recording.");
-
-  await perfFront.flushAsyncQueue();
-  is(perfComponent.state.recordingState, "recording",
-    "The actor has started its recording");
-
-  document.querySelector("button").click();
-  is(perfComponent.state.recordingState, "request-to-get-profile-and-stop-profiler",
-    "We have requested to stop the profiler.");
-
-  await perfFront.flushAsyncQueue();
-  is(perfComponent.state.recordingState, "available-to-record",
-    "The profiler is available to record again.");
-
-  await perfFront.flushAsyncQueue();
-  is(typeof profile, "object", "Got a profile");
-});
deleted file mode 100644
--- a/devtools/client/performance/new/test/browser_perf-state-02.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* 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";
-
-/**
- * Tests the perf component for when the profiler is already started.
- */
-addPerfTest(async ({ document, browserRequire, mountElement, perfFront }) => {
-  const Perf = browserRequire("devtools/client/performance/new/components/Perf");
-  const React = browserRequire("devtools/client/shared/vendor/react");
-  const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
-
-  const element = React.createElement(Perf, { perfFront });
-
-  ok(true, "Start the profiler before initiliazing the component, to simulate" +
-           "the profiler being controlled by another tool.");
-
-  perfFront.startProfiler();
-  await perfFront.flushAsyncQueue();
-
-  const perfComponent = ReactDOM.render(element, mountElement);
-  is(perfComponent.state.recordingState, "not-yet-known",
-    "The component at first is in an unknown state.");
-
-  await perfFront.flushAsyncQueue();
-  is(perfComponent.state.recordingState, "other-is-recording",
-    "The profiler is not available to record.");
-
-  document.querySelector("button").click();
-  is(perfComponent.state.recordingState, "request-to-stop-profiler",
-    "We can request to stop the profiler.");
-
-  await perfFront.flushAsyncQueue();
-  is(perfComponent.state.recordingState, "available-to-record",
-    "The profiler is now available to record.");
-});
deleted file mode 100644
--- a/devtools/client/performance/new/test/browser_perf-state-03.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/* 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";
-
-/**
- * Tests the workflow of what happens when a third party tool interrupts a recording.
- */
-addPerfTest(async ({ document, browserRequire, mountElement, perfFront }) => {
-  const Perf = browserRequire("devtools/client/performance/new/components/Perf");
-  const React = browserRequire("devtools/client/shared/vendor/react");
-  const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
-
-  const element = React.createElement(Perf, { perfFront });
-
-  const perfComponent = ReactDOM.render(element, mountElement);
-  is(perfComponent.state.recordingState, "not-yet-known",
-    "The component at first is in an unknown state.");
-
-  await perfFront.flushAsyncQueue();
-  is(perfComponent.state.recordingState, "available-to-record",
-    "After talking to the actor, we're ready to record.");
-
-  document.querySelector("button").click();
-  is(perfComponent.state.recordingState, "request-to-start-recording",
-    "Sent in a request to start recording.");
-
-  await perfFront.flushAsyncQueue();
-  is(perfComponent.state.recordingState, "recording",
-    "The actor has started its recording");
-
-  ok(true, "Simulate a third party stopping the profiler.");
-  perfFront.stopProfilerAndDiscardProfile();
-  await perfFront.flushAsyncQueue();
-
-  ok(perfComponent.state.recordingUnexpectedlyStopped,
-    "The profiler unexpectedly stopped.");
-  is(perfComponent.state.recordingState, "available-to-record",
-    "However, the profiler is available to record again.");
-});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/chrome/chrome.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files =
+  head.js
+
+[test_perf-state-01.html]
+[test_perf-state-02.html]
+[test_perf-state-03.html]
+[test_perf-state-04.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/chrome/head.js
@@ -0,0 +1,136 @@
+/* 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";
+
+/* exported addPerfTest, MockPerfFront */
+/* globals URL_ROOT */
+
+const { BrowserLoader } = Components.utils.import("resource://devtools/client/shared/browser-loader.js", {});
+var { require } = BrowserLoader({
+  baseURI: "resource://devtools/client/performance/new/",
+  window
+});
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const { perfDescription } = require("devtools/shared/specs/perf");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+
+flags.testing = true;
+let EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
+SimpleTest.registerCleanupFunction(function () {
+  if (DevToolsUtils.assertionFailureCount !== EXPECTED_DTU_ASSERT_FAILURE_COUNT) {
+    ok(false, "Should have had the expected number of DevToolsUtils.assert() failures." +
+      "Expected " + EXPECTED_DTU_ASSERT_FAILURE_COUNT +
+      ", got " + DevToolsUtils.assertionFailureCount);
+  }
+});
+
+/**
+ * Handle test setup and teardown while catching errors.
+ */
+function addPerfTest(asyncTest) {
+  window.onload = async () => {
+    try {
+      await asyncTest();
+    } catch (e) {
+      ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+    } finally {
+      SimpleTest.finish();
+    }
+  };
+}
+
+/**
+ * The Gecko Profiler is a rather heavy-handed component that uses a lot of resources.
+ * In order to get around that, and have quick component tests we provide a mock of
+ * the performance front. It also has a method called flushAsyncQueue() that will
+ * flush any queued async calls to deterministically run our tests.
+ */
+class MockPerfFront extends EventEmitter {
+  constructor() {
+    super();
+    this._isActive = false;
+    this._asyncQueue = [];
+
+    // Tests can update these two values directly as needed.
+    this.mockIsSupported = true;
+    this.mockIsLocked = false;
+
+    // Wrap all async methods in a flushable queue, so that tests can control
+    // when the responses come back.
+    this.isActive = this._wrapInAsyncQueue(this.isActive);
+    this.startProfiler = this._wrapInAsyncQueue(this.startProfiler);
+    this.stopProfilerAndDiscardProfile = this._wrapInAsyncQueue(
+      this.stopProfilerAndDiscardProfile);
+    this.getProfileAndStopProfiler = this._wrapInAsyncQueue(
+      this.getProfileAndStopProfiler);
+  }
+
+  /**
+   * Provide a flushable queue mechanism for all async work. The work piles up
+   * and then is evaluated at once when _flushPendingQueue is called.
+   */
+  _wrapInAsyncQueue(fn) {
+    if (typeof fn !== "function") {
+      throw new Error("_wrapInAsyncQueue requires a function");
+    }
+    return (...args) => {
+      return new Promise(resolve => {
+        this._asyncQueue.push(() => {
+          resolve(fn.apply(this, args));
+        });
+      });
+    };
+  }
+
+  flushAsyncQueue() {
+    const pending = this._asyncQueue;
+    this._asyncQueue = [];
+    pending.forEach(fn => fn());
+    // Wait for the next frame before continuing
+    return new Promise(requestAnimationFrame);
+  }
+
+  startProfiler() {
+    this._isActive = true;
+    // Defer this so it doesn't happen immediately.
+    this.emit("profiler-started");
+  }
+
+  getProfileAndStopProfiler() {
+    this._isActive = false;
+    // Defer this so it doesn't happen immediately.
+    this.emit("profiler-stopped");
+    // Return a fake profile.
+    return {};
+  }
+
+  stopProfilerAndDiscardProfile() {
+    this._isActive = false;
+    // Defer this so it doesn't happen immediately.
+    this.emit("profiler-stopped");
+  }
+
+  isActive() {
+    return this._isActive;
+  }
+
+  isSupportedPlatform() {
+    return this.mockIsSupported;
+  }
+
+  isLockedForPrivateBrowsing() {
+    return this.mockIsLocked;
+  }
+}
+
+// Do a quick validation to make sure that our Mock has the same methods as a spec.
+const mockKeys = Object.getOwnPropertyNames(MockPerfFront.prototype);
+Object.getOwnPropertyNames(perfDescription.methods).forEach(methodName => {
+  if (!mockKeys.includes(methodName)) {
+    throw new Error(`The MockPerfFront is missing the method "${methodName}" from the ` +
+                    "actor's spec. It should be added to the mock.");
+  }
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/chrome/test_perf-state-01.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+  <meta charset="utf-8">
+  <title>Perf component test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+  <div id="container"></div>
+
+  <pre id="test">
+    <script src="head.js" type="application/javascript"></script>
+    <script type="application/javascript">
+      "use strict";
+
+      /**
+       * Test the normal workflow of starting and stopping the profiler through the
+       * Perf component.
+       */
+      addPerfTest(async () => {
+        const Perf = require("devtools/client/performance/new/components/Perf");
+        const React = require("devtools/client/shared/vendor/react");
+        const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+        const perfFront = new MockPerfFront();
+        const container = document.querySelector("#container");
+
+        // Inject a function which will allow us to receive the profile.
+        let profile;
+        function receiveProfile(profileIn) {
+          profile = profileIn;
+        }
+
+        const element = React.createElement(Perf, { perfFront, receiveProfile });
+        const perfComponent = ReactDOM.render(element, container);
+        is(perfComponent.state.recordingState, "not-yet-known",
+          "The component at first is in an unknown state.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "available-to-record",
+          "After talking to the actor, we're ready to record.");
+
+        const button = container.querySelector("button");
+        ok(button, "Selected the button to click.");
+        button.click();
+        is(perfComponent.state.recordingState, "request-to-start-recording",
+          "Sent in a request to start recording.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "recording",
+          "The actor has started its recording");
+
+        button.click();
+        is(perfComponent.state.recordingState,
+          "request-to-get-profile-and-stop-profiler",
+          "We have requested to stop the profiler.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "available-to-record",
+          "The profiler is available to record again.");
+        await perfFront.flushAsyncQueue();
+        is(typeof profile, "object", "Got a profile");
+      });
+    </script>
+  </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/chrome/test_perf-state-02.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+  <meta charset="utf-8">
+  <title>Perf component test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+  <div id="container"></div>
+
+  <pre id="test">
+    <script src="head.js" type="application/javascript"></script>
+    <script type="application/javascript">
+      "use strict";
+
+      /**
+       * Test the perf component when the profiler is already started.
+       */
+      addPerfTest(async () => {
+        const Perf = require("devtools/client/performance/new/components/Perf");
+        const React = require("devtools/client/shared/vendor/react");
+        const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+        const perfFront = new MockPerfFront();
+        const container = document.querySelector("#container");
+
+        ok(true, "Start the profiler before initiliazing the component, to simulate" +
+                 "the profiler being controlled by another tool.");
+
+        perfFront.startProfiler();
+        await perfFront.flushAsyncQueue();
+
+        const receiveProfile = () => {};
+        const element = React.createElement(Perf, { perfFront, receiveProfile });
+        const perfComponent = ReactDOM.render(element, container);
+        is(perfComponent.state.recordingState, "not-yet-known",
+          "The component at first is in an unknown state.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "other-is-recording",
+          "The profiler is not available to record.");
+
+        const button = container.querySelector("button");
+        ok(button, "Selected a button on the component");
+        button.click();
+        is(perfComponent.state.recordingState, "request-to-stop-profiler",
+          "We can request to stop the profiler.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "available-to-record",
+          "The profiler is now available to record.");
+      });
+    </script>
+  </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/chrome/test_perf-state-03.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+  <meta charset="utf-8">
+  <title>Perf component test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+  <div id="container"></div>
+
+  <pre id="test">
+    <script src="head.js" type="application/javascript"></script>
+    <script type="application/javascript">
+      "use strict";
+
+      /**
+       * Test the perf component for when the profiler is already started.
+       */
+      addPerfTest(async () => {
+        const Perf = require("devtools/client/performance/new/components/Perf");
+        const React = require("devtools/client/shared/vendor/react");
+        const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+        const perfFront = new MockPerfFront();
+        const container = document.querySelector("#container");
+
+        const receiveProfile = () => {};
+        const element = React.createElement(Perf, { perfFront, receiveProfile });
+        const perfComponent = ReactDOM.render(element, container);
+
+        is(perfComponent.state.recordingState, "not-yet-known",
+          "The component at first is in an unknown state.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "available-to-record",
+          "After talking to the actor, we're ready to record.");
+
+        document.querySelector("button").click();
+        is(perfComponent.state.recordingState, "request-to-start-recording",
+          "Sent in a request to start recording.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "recording",
+          "The actor has started its recording");
+
+        ok(true, "Simulate a third party stopping the profiler.");
+        perfFront.stopProfilerAndDiscardProfile();
+        await perfFront.flushAsyncQueue();
+
+        ok(perfComponent.state.recordingUnexpectedlyStopped,
+          "The profiler unexpectedly stopped.");
+        is(perfComponent.state.recordingState, "available-to-record",
+          "However, the profiler is available to record again.");
+      });
+    </script>
+  </pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/performance/new/test/chrome/test_perf-state-04.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!-- 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/. -->
+<head>
+  <meta charset="utf-8">
+  <title>Perf component test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+  <div id="container"></div>
+
+  <pre id="test">
+    <script src="head.js" type="application/javascript"></script>
+    <script type="application/javascript">
+      "use strict";
+
+      /**
+       * Test that the profiler gets disabled during private browsing.
+       */
+      addPerfTest(async () => {
+        const Perf = require("devtools/client/performance/new/components/Perf");
+        const React = require("devtools/client/shared/vendor/react");
+        const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+        const perfFront = new MockPerfFront();
+        const container = document.querySelector("#container");
+
+        perfFront.mockIsLocked = true;
+
+        const receiveProfile = () => {};
+        const element = React.createElement(Perf, { perfFront, receiveProfile });
+        const perfComponent = ReactDOM.render(element, container);
+
+        is(perfComponent.state.recordingState, "not-yet-known",
+          "The component at first is in an unknown state.");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "locked-for-private-browsing",
+          "After talking to the actor, it's locked for private browsing.");
+
+        perfFront.mockIsLocked = false;
+        perfFront.emit("profile-unlocked-from-private-browsing");
+
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "available-to-record",
+          "After the profiler is unlocked, it's available to record.");
+
+        document.querySelector("button").click();
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "recording",
+          "The actor has started its recording");
+
+        perfFront.mockIsLocked = true;
+        perfFront.emit("profile-locked-for-private-browsing");
+        await perfFront.flushAsyncQueue();
+        is(perfComponent.state.recordingState, "locked-for-private-browsing",
+          "The recording stops when going into private browsing mode.");
+      });
+    </script>
+  </pre>
+</body>
+</html>
deleted file mode 100644
--- a/devtools/client/performance/new/test/doc_mount_react.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <title>Perf Test</title>
-  </head>
-  <body>
-    <div id="root"></div>
-  </body>
-</html>
deleted file mode 100644
--- a/devtools/client/performance/new/test/head.js
+++ /dev/null
@@ -1,140 +0,0 @@
-/* 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";
-
-/* exported addPerfTest */
-/* globals URL_ROOT */
-
-// Load the shared test helpers into this compartment.
-Services.scriptloader.loadSubScript(
-  "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
-  this);
-
-const EventEmitter = require("devtools/shared/event-emitter");
-const { Hosts } = require("devtools/client/framework/toolbox-hosts");
-const { DOMHelpers } = Cu.import("resource://devtools/client/shared/DOMHelpers.jsm", {});
-const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
-
-/**
- * The perf tests require some common setup. This does that setup, and injects a few
- * common requirements. It is a thin wrapper around add_task().
- */
-function addPerfTest(asyncTestFn) {
-  add_task(async () => {
-    waitForExplicitFinish();
-
-    const [host, window, document] = await createHost(URL_ROOT + "doc_mount_react.html");
-    const mountElement = document.querySelector("#root");
-    const perfFront = new MockPerfFront();
-    const { require: browserRequire } = BrowserLoader({
-      baseURI: "resource://devtools/client/performance/new/test/",
-      window
-    });
-
-    // Run the test.
-    try {
-      await asyncTestFn({
-        browserRequire,
-        document,
-        mountElement,
-        perfFront
-      });
-    } catch (error) {
-      console.error(error);
-      ok(false, "The test threw an error.");
-    }
-
-    // Clean up.
-    host.destroy();
-    finish();
-  });
-}
-
-/**
- * The Gecko Profiler is a rather heavy-handed component that uses a lot of resources.
- * In order to get around that, and have quick component tests we provide a mock of
- * the performance front. It also has a method called flushAsyncQueue() that will
- * flush any queued async calls to deterministically run our tests.
- */
-class MockPerfFront extends EventEmitter {
-  constructor() {
-    super();
-    this._isActive = false;
-    this._asyncQueue = [];
-
-    // Wrap all async methods in a flushable queue, so that tests can control
-    // when the responses come back.
-    this.isActive = this._wrapInAsyncQueue(this.isActive);
-    this.startProfiler = this._wrapInAsyncQueue(this.startProfiler);
-    this.stopProfilerAndDiscardProfile = this._wrapInAsyncQueue(
-      this.stopProfilerAndDiscardProfile);
-    this.getProfileAndStopProfiler = this._wrapInAsyncQueue(
-      this.getProfileAndStopProfiler);
-  }
-
-  /**
-   * Provide a flushable queue mechanism for all async work. The work piles up
-   * and then is evaluated at once when _flushPendingQueue is called.
-   */
-  _wrapInAsyncQueue(fn) {
-    if (typeof fn !== "function") {
-      throw new Error("_wrapInAsyncQueue requires a function");
-    }
-    return (...args) => {
-      return new Promise(resolve => {
-        this._asyncQueue.push(() => {
-          resolve(fn.apply(this, args));
-        });
-      });
-    };
-  }
-
-  flushAsyncQueue() {
-    const pending = this._asyncQueue;
-    this._asyncQueue = [];
-    pending.forEach(fn => fn());
-    return Promise.resolve();
-  }
-
-  isActive() {
-    return this._isActive;
-  }
-
-  startProfiler() {
-    this._isActive = true;
-    // Defer this so it doesn't happen immediately.
-    this.emit("profiler-started");
-  }
-
-  getProfileAndStopProfiler() {
-    this._isActive = false;
-    // Defer this so it doesn't happen immediately.
-    this.emit("profiler-stopped");
-    // Return a fake profile.
-    return {};
-  }
-
-  stopProfilerAndDiscardProfile() {
-    this._isActive = false;
-    // Defer this so it doesn't happen immediately.
-    this.emit("profiler-stopped");
-  }
-}
-
-/**
- * Create a DevTools host on the bottom of the window with the given URL. This quickly
- * gives us a document that the test can directly manipulate.
- */
-async function createHost(url) {
-  const host = new Hosts.bottom(gBrowser.selectedTab); // eslint-disable-line new-cap
-  const iframe = await host.create();
-
-  await new Promise(resolve => {
-    const domHelper = new DOMHelpers(iframe.contentWindow);
-    iframe.setAttribute("src", url);
-    domHelper.onceDOMReady(resolve);
-  });
-
-  return [host, iframe.contentWindow, iframe.contentDocument];
-}
--- a/devtools/shared/specs/perf.js
+++ b/devtools/shared/specs/perf.js
@@ -1,16 +1,16 @@
 /* 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";
 
 const { RetVal, generateActorSpec } = require("devtools/shared/protocol");
 
-const perfSpec = generateActorSpec({
+const perfDescription = {
   typeName: "perf",
 
   events: {
     "profiler-started": {
       type: "profiler-started"
     },
     "profiler-stopped": {
       type: "profiler-stopped"
@@ -52,11 +52,15 @@ const perfSpec = generateActorSpec({
       response: { value: RetVal("boolean") }
     },
 
     isLockedForPrivateBrowsing: {
       request: {},
       response: { value: RetVal("boolean") }
     }
   }
-});
+};
+
+exports.perfDescription = perfDescription;
+
+const perfSpec = generateActorSpec(perfDescription);
 
 exports.perfSpec = perfSpec;