Bug 1307881 - Part 2 - Provide a UI within the Web Console for managing persistence;r=nchevobbe draft
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 31 Aug 2017 21:43:42 -0700
changeset 657166 19b8decae600ef1a93338003200f97c96fd558de
parent 657165 2f3d35329126866b0c85d3620fb9f3d046ff3a85
child 657167 df78c410d2961d0604e811cbcfaa08fe7139d430
push id77454
push userbgrinstead@mozilla.com
push dateFri, 01 Sep 2017 04:45:10 +0000
reviewersnchevobbe
bugs1307881
milestone57.0a1
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");
+});