--- 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");
+});