Bug 1307881 - Part 2 - Provide a UI within the Web Console for managing persistence;r=nchevobbe
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 31 Aug 2017 21:43:42 -0700
changeset 378106 27d26ef5a86c7927df0ba3fde7bbe0258f36998f
parent 378105 a8bbf4e5c665e3ae8085e8ad9e5104b13bc4f0ec
child 378107 a918bec3a72ae393aef08d2b29c5a54bb55b468c
push id32421
push userarchaeopteryx@coole-files.de
push dateFri, 01 Sep 2017 08:31:26 +0000
treeherdermozilla-central@583e73fb8e3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnchevobbe
bugs1307881
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1307881 - Part 2 - Provide a UI within the Web Console for managing persistence;r=nchevobbe MozReview-Commit-ID: GVkOms1o74c
devtools/client/locales/en-US/webconsole.properties
devtools/client/themes/webconsole.css
devtools/client/webconsole/new-console-output/actions/ui.js
devtools/client/webconsole/new-console-output/components/filter-bar.js
devtools/client/webconsole/new-console-output/components/filter-checkbox.js
devtools/client/webconsole/new-console-output/components/moz.build
devtools/client/webconsole/new-console-output/constants.js
devtools/client/webconsole/new-console-output/reducers/ui.js
devtools/client/webconsole/new-console-output/store.js
devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js
devtools/client/webconsole/new-console-output/test/components/filter-checkbox.test.js
devtools/client/webconsole/new-console-output/test/fixtures/Services.js
devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_persist.js
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -310,8 +310,13 @@ webconsole.requestsFilterButton.label=Re
 # This is a semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # example: 345 items hidden by filters.
 webconsole.filteredMessages.label=#1 item hidden by filters;#1 items hidden by filters
 
 # Label used as the text of the "Reset filters" button in the "filtered messages" bar.
 # It resets the default filters of the console to their original values.
 webconsole.resetFiltersButton.label=Reset filters
+
+# LOCALIZATION NOTE (webconsole.enablePersistentLogs.label)
+webconsole.enablePersistentLogs.label=Persist Logs
+# LOCALIZATION NOTE (webconsole.enablePersistentLogs.tooltip)
+webconsole.enablePersistentLogs.tooltip=If you enable this option the output will not be cleared each time you navigate to a new page
--- a/devtools/client/themes/webconsole.css
+++ b/devtools/client/themes/webconsole.css
@@ -765,36 +765,43 @@ a.learn-more-link.webconsole-learn-more-
   /* Wrap so the "Hidden messages" bar can go to its own row if needed */
   flex-wrap: wrap;
 }
 
 .webconsole-filterbar-primary {
   display: flex;
   /* We want the toolbar (which contain the text search input) to be at least 200px
   so we don't allow to shrink it */
-  flex: 1 0 200px;
+  flex: 100 0 200px;
 }
 
 .devtools-toolbar.webconsole-filterbar-secondary {
   display: flex;
   width: 100%;
   align-items: center;
+  -moz-user-select: none;
 }
 
 .webconsole-filterbar-primary .devtools-plaininput {
   flex: 1 1 100%;
 }
 
+.webconsole-filterbar-primary .filter-checkbox {
+  flex-shrink: 0;
+  margin: 0 3px;
+}
+
 .webconsole-filterbar-secondary .devtools-separator {
   margin: 0 5px;
 }
 
 .webconsole-filterbar-filtered-messages {
   /* Needed so the bar takes the whole horizontal space when it is wrapped */
   flex-grow: 1;
+  justify-content: flex-end;
   color: var(--theme-comment);
   text-align: end;
 }
 
 .webconsole-filterbar-filtered-messages .filter-message-text {
   font-style: italic;
   -moz-user-select: none;
 }
--- a/devtools/client/webconsole/new-console-output/actions/ui.js
+++ b/devtools/client/webconsole/new-console-output/actions/ui.js
@@ -6,42 +6,54 @@
 
 "use strict";
 
 const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
 const Services = require("Services");
 
 const {
   FILTER_BAR_TOGGLE,
+  PERSIST_TOGGLE,
   PREFS,
   TIMESTAMPS_TOGGLE,
   SELECT_NETWORK_MESSAGE_TAB,
 } = require("devtools/client/webconsole/new-console-output/constants");
 
 function filterBarToggle(show) {
   return (dispatch, getState) => {
     dispatch({
       type: FILTER_BAR_TOGGLE,
     });
     const uiState = getAllUi(getState());
     Services.prefs.setBoolPref(PREFS.UI.FILTER_BAR, uiState.get("filterBarVisible"));
   };
 }
 
+function persistToggle(show) {
+  return (dispatch, getState) => {
+    dispatch({
+      type: PERSIST_TOGGLE,
+    });
+    const uiState = getAllUi(getState());
+    Services.prefs.setBoolPref(PREFS.UI.PERSIST, uiState.get("persistLogs"));
+  };
+}
+
 function timestampsToggle(visible) {
   return {
     type: TIMESTAMPS_TOGGLE,
     visible,
   };
 }
 
 function selectNetworkMessageTab(id) {
   return {
     type: SELECT_NETWORK_MESSAGE_TAB,
     id,
   };
 }
 
 module.exports = {
   filterBarToggle,
+  persistToggle,
   timestampsToggle,
   selectNetworkMessageTab,
 };
--- a/devtools/client/webconsole/new-console-output/components/filter-bar.js
+++ b/devtools/client/webconsole/new-console-output/components/filter-bar.js
@@ -7,87 +7,92 @@ const {
   createClass,
   DOM: dom,
   PropTypes
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { getAllFilters } = require("devtools/client/webconsole/new-console-output/selectors/filters");
 const { getFilteredMessagesCount } = require("devtools/client/webconsole/new-console-output/selectors/messages");
 const { getAllUi } = require("devtools/client/webconsole/new-console-output/selectors/ui");
-const {
-  filterBarToggle,
-  defaultFiltersReset,
-  filterTextSet,
-  messagesClear,
-} = require("devtools/client/webconsole/new-console-output/actions/index");
+const actions = require("devtools/client/webconsole/new-console-output/actions/index");
 const { l10n } = require("devtools/client/webconsole/new-console-output/utils/messages");
 const { PluralForm } = require("devtools/shared/plural-form");
 const {
   DEFAULT_FILTERS,
   FILTERS,
 } = require("../constants");
 
 const FilterButton = require("devtools/client/webconsole/new-console-output/components/filter-button");
+const FilterCheckbox = require("devtools/client/webconsole/new-console-output/components/filter-checkbox");
 
 const FilterBar = createClass({
 
   displayName: "FilterBar",
 
   propTypes: {
     dispatch: PropTypes.func.isRequired,
     filter: PropTypes.object.isRequired,
     serviceContainer: PropTypes.shape({
       attachRefToHud: PropTypes.func.isRequired,
     }).isRequired,
     filterBarVisible: PropTypes.bool.isRequired,
+    persistLogs: PropTypes.bool.isRequired,
     filteredMessagesCount: PropTypes.object.isRequired,
   },
 
   shouldComponentUpdate(nextProps, nextState) {
     if (nextProps.filter !== this.props.filter) {
       return true;
     }
 
     if (nextProps.filterBarVisible !== this.props.filterBarVisible) {
       return true;
     }
 
+    if (nextProps.persistLogs !== this.props.persistLogs) {
+      return true;
+    }
+
     if (
       JSON.stringify(nextProps.filteredMessagesCount)
       !== JSON.stringify(this.props.filteredMessagesCount)
     ) {
       return true;
     }
 
     return false;
   },
 
   componentDidMount() {
     this.props.serviceContainer.attachRefToHud("filterBox",
       this.wrapperNode.querySelector(".text-filter"));
   },
 
   onClickMessagesClear: function () {
-    this.props.dispatch(messagesClear());
+    this.props.dispatch(actions.messagesClear());
   },
 
   onClickFilterBarToggle: function () {
-    this.props.dispatch(filterBarToggle());
+    this.props.dispatch(actions.filterBarToggle());
   },
 
   onClickRemoveAllFilters: function () {
-    this.props.dispatch(defaultFiltersReset());
+    this.props.dispatch(actions.defaultFiltersReset());
   },
 
   onClickRemoveTextFilter: function () {
-    this.props.dispatch(filterTextSet(""));
+    this.props.dispatch(actions.filterTextSet(""));
   },
 
   onSearchInput: function (e) {
-    this.props.dispatch(filterTextSet(e.target.value));
+    this.props.dispatch(actions.filterTextSet(e.target.value));
+  },
+
+  onChangePersistToggle: function () {
+    this.props.dispatch(actions.persistToggle());
   },
 
   renderFiltersConfigBar() {
     const {
       dispatch,
       filter,
       filteredMessagesCount,
     } = this.props;
@@ -155,17 +160,17 @@ const FilterBar = createClass({
         filterKey: FILTERS.NETXHR,
         dispatch
       }),
       FilterButton({
         active: filter[FILTERS.NET],
         label: l10n.getStr("webconsole.requestsFilterButton.label"),
         filterKey: FILTERS.NET,
         dispatch
-      })
+      }),
     );
   },
 
   renderFilteredMessagesBar() {
     const {
       filteredMessagesCount
     } = this.props;
     const {
@@ -197,16 +202,17 @@ const FilterBar = createClass({
       }, l10n.getStr("webconsole.resetFiltersButton.label"))
     );
   },
 
   render() {
     const {
       filter,
       filterBarVisible,
+      persistLogs,
       filteredMessagesCount,
     } = this.props;
 
     let children = [
       dom.div({
         className: "devtools-toolbar webconsole-filterbar-primary",
         key: "primary-bar",
       },
@@ -222,16 +228,22 @@ const FilterBar = createClass({
           onClick: this.onClickFilterBarToggle
         }),
         dom.input({
           className: "devtools-plaininput text-filter",
           type: "search",
           value: filter.text,
           placeholder: l10n.getStr("webconsole.filterInput.placeholder"),
           onInput: this.onSearchInput
+        }),
+        FilterCheckbox({
+          label: l10n.getStr("webconsole.enablePersistentLogs.label"),
+          title: l10n.getStr("webconsole.enablePersistentLogs.tooltip"),
+          onChange: this.onChangePersistToggle,
+          checked: persistLogs,
         })
       )
     ];
 
     if (filteredMessagesCount.global > 0) {
       children.push(this.renderFilteredMessagesBar());
     }
 
@@ -247,16 +259,18 @@ const FilterBar = createClass({
         }
       }, ...children
       )
     );
   }
 });
 
 function mapStateToProps(state) {
+  let uiState = getAllUi(state);
   return {
     filter: getAllFilters(state),
-    filterBarVisible: getAllUi(state).filterBarVisible,
+    filterBarVisible: uiState.filterBarVisible,
+    persistLogs: uiState.persistLogs,
     filteredMessagesCount: getFilteredMessagesCount(state),
   };
 }
 
 module.exports = connect(mapStateToProps)(FilterBar);
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/components/filter-checkbox.js
@@ -0,0 +1,29 @@
+/* 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 {
+  DOM: dom,
+  PropTypes
+} = require("devtools/client/shared/vendor/react");
+
+FilterCheckbox.displayName = "FilterCheckbox";
+
+FilterCheckbox.propTypes = {
+  label: PropTypes.string.isRequired,
+  title: PropTypes.string,
+  checked: PropTypes.bool.isRequired,
+  onChange: PropTypes.func.isRequired,
+};
+
+function FilterCheckbox(props) {
+  const {checked, label, title, onChange} = props;
+  return dom.label({ title, className: "filter-checkbox" }, dom.input({
+    type: "checkbox",
+    checked,
+    onChange,
+  }), label);
+}
+
+module.exports = FilterCheckbox;
--- a/devtools/client/webconsole/new-console-output/components/moz.build
+++ b/devtools/client/webconsole/new-console-output/components/moz.build
@@ -8,15 +8,16 @@ DIRS += [
 ]
 
 DevToolsModules(
     'collapse-button.js',
     'console-output.js',
     'console-table.js',
     'filter-bar.js',
     'filter-button.js',
+    'filter-checkbox.js',
     'grip-message-body.js',
     'message-container.js',
     'message-icon.js',
     'message-indent.js',
     'message-repeat.js',
     'message.js'
 )
--- a/devtools/client/webconsole/new-console-output/constants.js
+++ b/devtools/client/webconsole/new-console-output/constants.js
@@ -19,32 +19,34 @@ const actionTypes = {
   REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR",
   TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
   FILTER_TOGGLE: "FILTER_TOGGLE",
   FILTER_TEXT_SET: "FILTER_TEXT_SET",
   FILTERS_CLEAR: "FILTERS_CLEAR",
   DEFAULT_FILTERS_RESET: "DEFAULT_FILTERS_RESET",
   FILTER_BAR_TOGGLE: "FILTER_BAR_TOGGLE",
   SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB",
+  PERSIST_TOGGLE: "PERSIST_TOGGLE",
 };
 
 const prefs = {
   PREFS: {
     FILTER: {
       ERROR: "devtools.webconsole.filter.error",
       WARN: "devtools.webconsole.filter.warn",
       INFO: "devtools.webconsole.filter.info",
       LOG: "devtools.webconsole.filter.log",
       DEBUG: "devtools.webconsole.filter.debug",
       CSS: "devtools.webconsole.filter.css",
       NET: "devtools.webconsole.filter.net",
       NETXHR: "devtools.webconsole.filter.netxhr",
     },
     UI: {
-      FILTER_BAR: "devtools.webconsole.ui.filterbar"
+      FILTER_BAR: "devtools.webconsole.ui.filterbar",
+      PERSIST: "devtools.webconsole.persistlog",
     }
   }
 };
 
 const FILTERS = {
   CSS: "css",
   DEBUG: "debug",
   ERROR: "error",
--- a/devtools/client/webconsole/new-console-output/reducers/ui.js
+++ b/devtools/client/webconsole/new-console-output/reducers/ui.js
@@ -2,36 +2,40 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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 {
   FILTER_BAR_TOGGLE,
+  PERSIST_TOGGLE,
   TIMESTAMPS_TOGGLE,
   SELECT_NETWORK_MESSAGE_TAB,
 } = require("devtools/client/webconsole/new-console-output/constants");
 const Immutable = require("devtools/client/shared/vendor/immutable");
 
 const {
   PANELS,
 } = require("devtools/client/netmonitor/src/constants");
 
 const UiState = Immutable.Record({
   filterBarVisible: false,
   filteredMessageVisible: false,
+  persistLogs: false,
   timestampsVisible: true,
   networkMessageActiveTabId: PANELS.HEADERS,
 });
 
 function ui(state = new UiState(), action) {
   switch (action.type) {
     case FILTER_BAR_TOGGLE:
       return state.set("filterBarVisible", !state.filterBarVisible);
+    case PERSIST_TOGGLE:
+      return state.set("persistLogs", !state.persistLogs);
     case TIMESTAMPS_TOGGLE:
       return state.set("timestampsVisible", action.visible);
     case SELECT_NETWORK_MESSAGE_TAB:
       return state.set("networkMessageActiveTabId", action.id);
   }
 
   return state;
 }
--- a/devtools/client/webconsole/new-console-output/store.js
+++ b/devtools/client/webconsole/new-console-output/store.js
@@ -49,16 +49,17 @@ function configureStore(hud, options = {
       log: Services.prefs.getBoolPref(PREFS.FILTER.LOG),
       css: Services.prefs.getBoolPref(PREFS.FILTER.CSS),
       net: Services.prefs.getBoolPref(PREFS.FILTER.NET),
       netxhr: Services.prefs.getBoolPref(PREFS.FILTER.NETXHR),
     }),
     ui: new UiState({
       filterBarVisible: Services.prefs.getBoolPref(PREFS.UI.FILTER_BAR),
       networkMessageActiveTabId: "headers",
+      persistLogs: Services.prefs.getBoolPref(PREFS.UI.PERSIST),
     })
   };
 
   return createStore(
     createRootReducer(),
     initialState,
     compose(
       applyMiddleware(thunk),
--- a/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js
+++ b/devtools/client/webconsole/new-console-output/test/components/filter-bar.test.js
@@ -205,17 +205,17 @@ describe("FilterBar component:", () => {
     );
 
     let buttons = [
       filterBtn({ label: "Errors", filterKey: FILTERS.ERROR }),
       filterBtn({ label: "Warnings", filterKey: FILTERS.WARN }),
       filterBtn({ label: "Logs", filterKey: FILTERS.LOG }),
       filterBtn({ label: "Info", filterKey: FILTERS.INFO }),
       filterBtn({ label: "Debug", filterKey: FILTERS.DEBUG }),
-      DOM.span({
+      DOM.div({
         className: "devtools-separator",
       }),
       filterBtn({ label: "CSS", filterKey: "css", active: false }),
       filterBtn({ label: "XHR", filterKey: "netxhr", active: false }),
       filterBtn({ label: "Requests", filterKey: "net", active: false }),
     ];
 
     secondaryBar.children().forEach((child, index) => {
@@ -237,9 +237,20 @@ describe("FilterBar component:", () => {
 
   it("sets filter text when text is typed", () => {
     const store = setupStore([]);
 
     const wrapper = mount(Provider({store}, FilterBar({ serviceContainer })));
     wrapper.find(".devtools-plaininput").simulate("input", { target: { value: "a" } });
     expect(store.getState().filters.text).toBe("a");
   });
+
+  it("toggles persist logs when checkbox is clicked", () => {
+    const store = setupStore([]);
+
+    expect(getAllUi(store.getState()).persistLogs).toBe(false);
+
+    const wrapper = mount(Provider({store}, FilterBar({ serviceContainer })));
+    wrapper.find(".filter-checkbox input").simulate("change");
+
+    expect(getAllUi(store.getState()).persistLogs).toBe(true);
+  });
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/components/filter-checkbox.test.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const expect = require("expect");
+const { render } = require("enzyme");
+
+const { createFactory } = require("devtools/client/shared/vendor/react");
+
+const FilterCheckbox = createFactory(require("devtools/client/webconsole/new-console-output/components/filter-checkbox"));
+
+describe("FilterCheckbox component:", () => {
+  const props = {
+    label: "test label",
+    title: "test title",
+    checked: true,
+    onChange: () => {},
+  };
+
+  it("displays as checked", () => {
+    const wrapper = render(FilterCheckbox(props));
+    expect(wrapper.html()).toBe(
+      '<label title="test title" class="filter-checkbox">' +
+      '<input type="checkbox" checked>test label</label>'
+    );
+  });
+
+  it("displays as unchecked", () => {
+    const uncheckedProps = Object.assign({}, props, { checked: false });
+    const wrapper = render(FilterCheckbox(uncheckedProps));
+    expect(wrapper.html()).toBe(
+      '<label title="test title" class="filter-checkbox">' +
+      '<input type="checkbox">test label</label>'
+    );
+  });
+});
--- a/devtools/client/webconsole/new-console-output/test/fixtures/Services.js
+++ b/devtools/client/webconsole/new-console-output/test/fixtures/Services.js
@@ -15,15 +15,16 @@ module.exports = {
       return null;
     },
     getBoolPref: pref => {
       const falsey = [
         PREFS.FILTER.CSS,
         PREFS.FILTER.NET,
         PREFS.FILTER.NETXHR,
         PREFS.UI.FILTER_BAR,
+        PREFS.UI.PERSIST,
       ];
       return !falsey.includes(pref);
     },
     setBoolPref: () => {},
     clearUserPref: () => {},
   }
 };
--- a/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser.ini
@@ -50,16 +50,17 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_webconsole_logErrorInPage.js]
 [browser_webconsole_network_messages_openinnet.js]
 [browser_webconsole_network_messages_expand.js]
 [browser_webconsole_nodes_highlight.js]
 [browser_webconsole_nodes_select.js]
 [browser_webconsole_object_inspector_entries.js]
 [browser_webconsole_object_inspector.js]
 [browser_webconsole_observer_notifications.js]
+[browser_webconsole_persist.js]
 [browser_webconsole_scroll.js]
 [browser_webconsole_shows_reqs_in_netmonitor.js]
 [browser_webconsole_sourcemap_error.js]
 [browser_webconsole_sourcemap_nosource.js]
 [browser_webconsole_stacktrace_location_debugger_link.js]
 [browser_webconsole_stacktrace_location_scratchpad_link.js]
 [browser_webconsole_string.js]
 [browser_webconsole_timestamps.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/new-console-output/test/mochitest/browser_webconsole_persist.js
@@ -0,0 +1,51 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check that message persistence works - bug 705921 / bug 1307881
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/new-console-output/test/mochitest/test-console.html";
+
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("devtools.webconsole.persistlog");
+});
+
+add_task(async function () {
+  info("Testing that messages disappear on a refresh if logs aren't persisted");
+  let hud = await openNewTabAndConsole(TEST_URI);
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, {}, () => {
+    content.wrappedJSObject.doLogs(5);
+  });
+  await waitFor(() => findMessages(hud, "").length === 5);
+  ok(true, "Messages showed up initially");
+
+  await refreshTab();
+  await waitFor(() => findMessages(hud, "").length === 0);
+  ok(true, "Messages disappeared");
+
+  await closeToolbox();
+});
+
+add_task(async function () {
+  info("Testing that messages persist on a refresh if logs are persisted");
+
+  let hud = await openNewTabAndConsole(TEST_URI);
+
+  hud.ui.outputNode.querySelector(".webconsole-filterbar-primary .filter-checkbox")
+    .click();
+
+  await ContentTask.spawn(gBrowser.selectedBrowser, {}, () => {
+    content.wrappedJSObject.doLogs(5);
+  });
+  await waitFor(() => findMessages(hud, "").length === 5);
+  ok(true, "Messages showed up initially");
+
+  await refreshTab();
+  await waitFor(() => findMessages(hud, "").length === 6);
+
+  ok(findMessage(hud, "Navigated"), "Navigated message appeared");
+});