Bug 1753177 - Use a virtualized list for webconsole output r=nchevobbe

This should be ready to review. However, I will include a big caveat that I
tried to do things the Reacty way, and it felt really awkward. Notably, I used
a React functional component, because they seem to be recommended as the
Right Way to do things, but because of all the state we need to keep track of,
as well as just the props that we don't want to lexically access because then
they can be stale, it felt really wrong. Guidance is welcome.

However, the core logic should be ready for review, so focus on that, and let
me know if I need to move anything around to make it more palatable.

Depends on D138030

Differential Revision: https://phabricator.services.mozilla.com/D138031
This commit is contained in:
Doug Thayer 2022-04-11 02:50:46 +00:00
Родитель e85628cd17
Коммит 81292d59a9
44 изменённых файлов: 1100 добавлений и 246 удалений

Просмотреть файл

@ -2214,10 +2214,20 @@ async function hasConsoleMessage({ toolbox }, msg) {
}
function evaluateExpressionInConsole(hud, expression) {
const seenMessages = new Set(
JSON.parse(
hud.ui.outputNode
.querySelector("[data-visible-messages]")
.getAttribute("data-visible-messages")
)
);
const onResult = new Promise(res => {
const onNewMessage = messages => {
for (const message of messages) {
if (message.node.classList.contains("result")) {
if (
message.node.classList.contains("result") &&
!seenMessages.has(message.node.getAttribute("data-message-id"))
) {
hud.ui.off("new-messages", onNewMessage);
res(message.node);
}

Просмотреть файл

@ -52,6 +52,7 @@ support-files =
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js
!/devtools/client/shared/test/telemetry-test-helpers.js
!/devtools/client/webconsole/test/browser/shared-head.js
# This is far from ideal. https://bugzilla.mozilla.org/show_bug.cgi?id=1565279
# covers removing this pref flip.
prefs =

Просмотреть файл

@ -5,6 +5,12 @@
// Test for error icon and the error count displayed at right of the
// toolbox toolbar
/* import-globals-from ../../webconsole/test/browser/shared-head.js */
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js",
this
);
const TEST_URI = `https://example.com/document-builder.sjs?html=<meta charset=utf8></meta>
<script>
console.error("Cache Error1");
@ -86,21 +92,18 @@ add_task(async function() {
"When the console is selected, the icon does not have a title"
);
const webconsoleDoc = toolbox.getCurrentPanel().hud.ui.window.document;
const hud = toolbox.getCurrentPanel().hud;
const webconsoleDoc = hud.ui.window.document;
// wait until all error messages are displayed in the console
await waitFor(
() =>
webconsoleDoc.querySelectorAll(".message.error").length ===
expectedErrorCount
async () => (await findAllErrors(hud)).length === expectedErrorCount
);
info("Clear the console output and check that the error icon is hidden");
webconsoleDoc.querySelector(".devtools-clear-icon").click();
await waitFor(() => !getErrorIcon(toolbox));
ok(true, "Clearing the console does hide the icon");
await waitFor(
() => webconsoleDoc.querySelectorAll(".message.error").length === 0
);
await waitFor(async () => (await findAllErrors(hud)).length === 0);
info("Check that the error count is capped at 99");
expectedErrorCount = 100;
@ -112,9 +115,7 @@ add_task(async function() {
// Wait until all the messages are displayed in the console
await waitFor(
() =>
webconsoleDoc.querySelectorAll(".message.error").length ===
expectedErrorCount
async () => (await findAllErrors(hud)).length === expectedErrorCount
);
await waitFor(() => getErrorIconCount(toolbox) === "99+");
@ -132,9 +133,7 @@ add_task(async function() {
// wait until all error messages are displayed in the console
await waitFor(
() =>
webconsoleDoc.querySelectorAll(".message.error").length ===
expectedErrorCount
async () => (await findAllErrors(hud)).length === expectedErrorCount
);
info("Disable the error icon from the options panel");
@ -160,9 +159,7 @@ add_task(async function() {
// to render the error icon again.
await toolbox.selectTool("webconsole");
await waitFor(
() =>
webconsoleDoc.querySelectorAll(".message.error").length ===
expectedErrorCount
async () => (await findAllErrors(hud)).length === expectedErrorCount
);
is(
getErrorIcon(toolbox),
@ -181,3 +178,7 @@ add_task(async function() {
toolbox.destroy();
});
function findAllErrors(hud) {
return findMessagesVirtualized({ hud, selector: ".message.error" });
}

Просмотреть файл

@ -128,8 +128,11 @@
}
/* We already paint a top border on jsterm-input-container (and we need to keep
* it when scrolling console content), so remove the last item's border. */
.webconsole-app:not(.jsterm-editor) .message:last-child {
* it when scrolling console content), so remove the last item's border. (NOTE:
* the last element is actually the second-to-last element when the output is
* scrolled all the way down, because we include an empty buffer div which
* grows to simulate the height of unrendered content.) */
.webconsole-app:not(.jsterm-editor) .message:nth-last-child(2) {
border-bottom-width: 0;
/* Adjust the min-height since we now only have a single border. */
min-height: calc(var(--console-row-height) + 1px);
@ -947,3 +950,13 @@ a.learn-more-link.webconsole-learn-more-link {
width: 0;
height: 0;
}
.lazy-message-list-top,
.lazy-message-list-bottom {
overflow-anchor: none;
pointer-events: none;
user-select: none;
padding: 0;
margin: 0;
border: none;
}

Просмотреть файл

@ -6,12 +6,14 @@
const {
Component,
createElement,
createRef,
} = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const {
connect,
} = require("devtools/client/shared/redux/visibility-handler-connect");
const { initialize } = require("devtools/client/webconsole/actions/ui");
const LazyMessageList = require("devtools/client/webconsole/components/Output/LazyMessageList");
const {
getMutableMessagesById,
@ -35,11 +37,9 @@ loader.lazyRequireGetter(
"devtools/client/webconsole/components/Output/MessageContainer",
true
);
loader.lazyRequireGetter(this, "flags", "devtools/shared/flags");
const { MESSAGE_TYPE } = require("devtools/client/webconsole/constants");
const {
getInitialMessageCountForViewport,
} = require("devtools/client/webconsole/utils/messages.js");
class ConsoleOutput extends Component {
static get propTypes() {
@ -63,6 +63,7 @@ class ConsoleOutput extends Component {
networkMessageActiveTabId: PropTypes.string.isRequired,
onFirstMeaningfulPaint: PropTypes.func.isRequired,
editorMode: PropTypes.bool.isRequired,
cacheGeneration: PropTypes.number.isRequired,
};
}
@ -70,6 +71,9 @@ class ConsoleOutput extends Component {
super(props);
this.onContextMenu = this.onContextMenu.bind(this);
this.maybeScrollToBottom = this.maybeScrollToBottom.bind(this);
this.messageIdsToKeepAlive = new Set();
this.ref = createRef();
this.lazyMessageListRef = createRef();
this.resizeObserver = new ResizeObserver(entries => {
// If we don't have the outputNode reference, or if the outputNode isn't connected
@ -91,7 +95,7 @@ class ConsoleOutput extends Component {
this.scrollToBottom();
}
this.lastMessageIntersectionObserver = new IntersectionObserver(
this.scrollDetectionIntersectionObserver = new IntersectionObserver(
entries => {
for (const entry of entries) {
// Consider that we're not pinned to the bottom anymore if the bottom of the
@ -105,7 +109,10 @@ class ConsoleOutput extends Component {
this.resizeObserver.observe(this.getElementToObserve());
const { serviceContainer, onFirstMeaningfulPaint, dispatch } = this.props;
serviceContainer.attachRefToWebConsoleUI("outputScroller", this.outputNode);
serviceContainer.attachRefToWebConsoleUI(
"outputScroller",
this.ref.current
);
// Waiting for the next paint.
new Promise(res => requestAnimationFrame(res)).then(() => {
@ -121,6 +128,11 @@ class ConsoleOutput extends Component {
}
componentWillUpdate(nextProps, nextState) {
this.isUpdating = true;
if (nextProps.cacheGeneration !== this.props.cacheGeneration) {
this.messageIdsToKeepAlive = new Set();
}
if (nextProps.editorMode !== this.props.editorMode) {
this.resizeObserver.disconnect();
}
@ -131,11 +143,12 @@ class ConsoleOutput extends Component {
// This makes the console stay pinned to the bottom if a batch of messages
// are added after a page refresh (Bug 1402237).
this.shouldScrollBottom = true;
this.scrolledToBottom = true;
return;
}
const { lastChild } = outputNode;
this.lastMessageIntersectionObserver.unobserve(lastChild);
const bottomBuffer = this.lazyMessageListRef.current.bottomBuffer;
this.scrollDetectionIntersectionObserver.unobserve(bottomBuffer);
// We need to scroll to the bottom if:
// - we are reacting to "initialize" action, and we are already scrolled to the bottom
@ -175,9 +188,11 @@ class ConsoleOutput extends Component {
}
componentDidUpdate(prevProps) {
this.isUpdating = false;
this.maybeScrollToBottom();
if (this?.outputNode?.lastChild) {
this.lastMessageIntersectionObserver.observe(this.outputNode.lastChild);
const bottomBuffer = this.lazyMessageListRef.current.bottomBuffer;
this.scrollDetectionIntersectionObserver.observe(bottomBuffer);
}
if (prevProps.editorMode !== this.props.editorMode) {
@ -185,13 +200,35 @@ class ConsoleOutput extends Component {
}
}
get outputNode() {
return this.ref.current;
}
maybeScrollToBottom() {
if (this.outputNode && this.shouldScrollBottom) {
this.scrollToBottom();
}
}
// The maybeScrollToBottom callback we provide to messages needs to be a little bit more
// strict than the one we normally use, because they can potentially interrupt a user
// scroll (between when the intersection observer registers the scroll break and when
// a componentDidUpdate comes through to reconcile it.)
maybeScrollToBottomMessageCallback(index) {
if (
this.outputNode &&
this.shouldScrollBottom &&
this.scrolledToBottom &&
this.lazyMessageListRef.current?.isItemNearBottom(index)
) {
this.scrollToBottom();
}
}
scrollToBottom() {
if (flags.testing && this.outputNode.hasAttribute("disable-autoscroll")) {
return;
}
if (this.outputNode.scrollHeight > this.outputNode.clientHeight) {
this.outputNode.scrollTop = this.outputNode.scrollHeight;
}
@ -215,7 +252,8 @@ class ConsoleOutput extends Component {
}
render() {
let {
const {
cacheGeneration,
dispatch,
visibleMessages,
mutableMessages,
@ -227,22 +265,10 @@ class ConsoleOutput extends Component {
networkMessageActiveTabId,
serviceContainer,
timestampsVisible,
initialized,
} = this.props;
if (!initialized) {
const numberMessagesFitViewport = getInitialMessageCountForViewport(
window
);
if (numberMessagesFitViewport < visibleMessages.length) {
visibleMessages = visibleMessages.slice(
visibleMessages.length - numberMessagesFitViewport
);
}
}
const messageNodes = visibleMessages.map(messageId =>
createElement(MessageContainer, {
const renderMessage = (messageId, index) => {
return createElement(MessageContainer, {
dispatch,
key: messageId,
messageId,
@ -264,20 +290,45 @@ class ConsoleOutput extends Component {
networkMessageUpdate: networkMessagesUpdate[messageId],
networkMessageActiveTabId,
getMessage: () => mutableMessages.get(messageId),
maybeScrollToBottom: this.maybeScrollToBottom,
})
);
maybeScrollToBottom: () =>
this.maybeScrollToBottomMessageCallback(index),
// Whenever a node is expanded, we want to make sure we keep the
// message node alive so as to not lose the expanded state.
setExpanded: () => this.messageIdsToKeepAlive.add(messageId),
});
};
// scrollOverdrawCount tells the list to draw extra elements above and
// below the scrollport so that we can avoid flashes of blank space
// when scrolling. Regarding the pref, this is really only intended to
// be used by tests, and specifically tests that *need* to change this
// value because the existing findMessageVirtualized and friends will not
// work for their use case.
const scrollOverdrawCount = 20;
const attrs = {
className: "webconsole-output",
role: "main",
onContextMenu: this.onContextMenu,
ref: this.ref,
};
if (flags.testing) {
attrs["data-visible-messages"] = JSON.stringify(visibleMessages);
}
return dom.div(
{
className: "webconsole-output",
role: "main",
onContextMenu: this.onContextMenu,
ref: node => {
this.outputNode = node;
},
},
messageNodes
attrs,
createElement(LazyMessageList, {
viewportRef: this.ref,
items: visibleMessages,
itemDefaultHeight: 21,
editorMode: this.props.editorMode,
scrollOverdrawCount,
ref: this.lazyMessageListRef,
renderItem: renderMessage,
itemsToKeepAlive: this.messageIdsToKeepAlive,
serviceContainer,
cacheGeneration,
shouldScrollBottom: () => this.shouldScrollBottom && this.isUpdating,
})
);
}
}
@ -286,6 +337,7 @@ function mapStateToProps(state, props) {
const mutableMessages = getMutableMessagesById(state);
return {
initialized: state.ui.initialized,
cacheGeneration: state.ui.cacheGeneration,
// We need to compute this so lifecycle methods can compare the global message count
// on state change (since we can't do it with mutableMessagesById).
messageCount: mutableMessages.size,

Просмотреть файл

@ -38,6 +38,7 @@ class ConsoleTable extends Component {
parameters: PropTypes.array.isRequired,
serviceContainer: PropTypes.object.isRequired,
id: PropTypes.string.isRequired,
setExpanded: PropTypes.func,
};
}
@ -66,7 +67,7 @@ class ConsoleTable extends Component {
}
getRows(columns, items) {
const { dispatch, serviceContainer } = this.props;
const { dispatch, serviceContainer, setExpanded } = this.props;
return items.map((item, index) => {
const cells = [];
@ -83,6 +84,7 @@ class ConsoleTable extends Component {
useQuotes: false,
serviceContainer,
dispatch,
setExpanded,
});
cells.push(

Просмотреть файл

@ -42,6 +42,7 @@ GripMessageBody.propTypes = {
type: PropTypes.string,
helperType: PropTypes.string,
maybeScrollToBottom: PropTypes.func,
setExpanded: PropTypes.func,
};
GripMessageBody.defaultProps = {
@ -58,6 +59,7 @@ function GripMessageBody(props) {
mode = MODE.LONG,
dispatch,
maybeScrollToBottom,
setExpanded,
customFormat = false,
} = props;
@ -73,6 +75,7 @@ function GripMessageBody(props) {
autoExpandDepth: shouldAutoExpandObjectInspector(props) ? 1 : 0,
mode,
maybeScrollToBottom,
setExpanded,
customFormat,
onCmdCtrlClick: (node, { depth, event, focused, expanded }) => {
const front = objectInspector.utils.node.getFront(node);

Просмотреть файл

@ -0,0 +1,395 @@
/* 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/.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* MIT License
*
* Copyright (c) 2019 Oleg Grishechkin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
"use strict";
const {
Fragment,
Component,
createElement,
createRef,
} = require("devtools/client/shared/vendor/react");
loader.lazyRequireGetter(
this,
"PropTypes",
"devtools/client/shared/vendor/react-prop-types"
);
// This element is a webconsole optimization for handling large numbers of
// console messages. The purpose is to only create DOM elements for messages
// which are actually visible within the scrollport. This code was based on
// Oleg Grishechkin's react-viewport-list element - however, it has been quite
// heavily modified, to the point that it is mostly unrecognizable. The most
// notable behavioral modification is that the list implements the behavior of
// pinning the scrollport to the bottom of the scroll container.
class LazyMessageList extends Component {
static get propTypes() {
return {
viewportRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) })
.isRequired,
items: PropTypes.array.isRequired,
itemsToKeepAlive: PropTypes.shape({
has: PropTypes.func,
keys: PropTypes.func,
size: PropTypes.number,
}).isRequired,
editorMode: PropTypes.bool.isRequired,
itemDefaultHeight: PropTypes.number.isRequired,
scrollOverdrawCount: PropTypes.number.isRequired,
renderItem: PropTypes.func.isRequired,
shouldScrollBottom: PropTypes.func.isRequired,
cacheGeneration: PropTypes.number.isRequired,
serviceContainer: PropTypes.shape({
emitForTests: PropTypes.func.isRequired,
}),
};
}
constructor(props) {
super(props);
this.#initialized = false;
this.#topBufferRef = createRef();
this.#bottomBufferRef = createRef();
this.#viewportHeight = window.innerHeight;
this.#startIndex = 0;
this.#resizeObserver = null;
this.#cachedHeights = [];
this.#scrollHandlerBinding = this.#scrollHandler.bind(this);
}
componentWillUpdate(nextProps, nextState) {
if (nextProps.cacheGeneration !== this.props.cacheGeneration) {
this.#cachedHeights = [];
this.#startIndex = 0;
} else if (
(this.props.shouldScrollBottom() &&
nextProps.items.length > this.props.items.length) ||
this.#startIndex > nextProps.items.length - this.#numItemsToDraw
) {
this.#startIndex = Math.max(
0,
nextProps.items.length - this.#numItemsToDraw
);
}
}
componentDidUpdate(prevProps) {
const { viewportRef, serviceContainer } = this.props;
if (!viewportRef.current || !this.#topBufferRef.current) {
return;
}
if (!this.#initialized) {
// We set these up from a one-time call in componentDidUpdate, rather than in
// componentDidMount, because we need the parent to be mounted first, to add
// listeners to it, and React orders things such that children mount before
// parents.
this.#addListeners();
}
if (!this.#initialized || prevProps.editorMode !== this.props.editorMode) {
this.#resizeObserver.observe(viewportRef.current);
}
this.#initialized = true;
// Since we updated, we're now going to compute the heights of all visible
// elements and store them in a cache. This allows us to get more accurate
// buffer regions to make scrolling correct when these elements no longer
// exist.
let index = this.#startIndex;
let element = this.#topBufferRef.current.nextSibling;
let elementRect = element?.getBoundingClientRect();
while (
element instanceof Element &&
index < this.#clampedEndIndex &&
element !== this.#bottomBufferRef.current
) {
const next = element.nextSibling;
const nextRect = next.getBoundingClientRect();
this.#cachedHeights[index] = nextRect.top - elementRect.top;
element = next;
elementRect = nextRect;
index++;
}
serviceContainer.emitForTests("lazy-message-list-updated-or-noop");
}
componentWillUnmount() {
this.#removeListeners();
}
#initialized;
#topBufferRef;
#bottomBufferRef;
#viewportHeight;
#startIndex;
#resizeObserver;
#cachedHeights;
#scrollHandlerBinding;
get #maxIndex() {
return this.props.items.length - 1;
}
get #overdrawHeight() {
return this.props.scrollOverdrawCount * this.props.itemDefaultHeight;
}
get #numItemsToDraw() {
const scrollingWindowCount = Math.ceil(
this.#viewportHeight / this.props.itemDefaultHeight
);
return scrollingWindowCount + 2 * this.props.scrollOverdrawCount;
}
get #unclampedEndIndex() {
return this.#startIndex + this.#numItemsToDraw;
}
// Since the "end index" is computed based off a fixed offset from the start
// index, it can exceed the length of our items array. This is just a helper
// to ensure we don't exceed that.
get #clampedEndIndex() {
return Math.min(this.#unclampedEndIndex, this.props.items.length);
}
/**
* Increases our start index until we've passed enough elements to cover
* the difference in px between where we are and where we want to be.
*
* @param Number startIndex
* The current value of our start index.
* @param Number deltaPx
* The difference in pixels between where we want to be and
* where we are.
* @return {Number} The new computed start index.
*/
#increaseStartIndex(startIndex, deltaPx) {
for (let i = startIndex + 1; i < this.props.items.length; i++) {
deltaPx -= this.#cachedHeights[i];
startIndex = i;
if (deltaPx <= 0) {
break;
}
}
return startIndex;
}
/**
* Decreases our start index until we've passed enough elements to cover
* the difference in px between where we are and where we want to be.
*
* @param Number startIndex
* The current value of our start index.
* @param Number deltaPx
* The difference in pixels between where we want to be and
* where we are.
* @return {Number} The new computed start index.
*/
#decreaseStartIndex(startIndex, diff) {
for (let i = startIndex - 1; i >= 0; i--) {
diff -= this.#cachedHeights[i];
startIndex = i;
if (diff <= 0) {
break;
}
}
return startIndex;
}
#scrollHandler() {
if (!this.props.viewportRef.current || !this.#topBufferRef.current) {
return;
}
const scrollportMin =
this.props.viewportRef.current.getBoundingClientRect().top -
this.#overdrawHeight;
const uppermostItemRect = this.#topBufferRef.current.nextSibling.getBoundingClientRect();
const uppermostItemMin = uppermostItemRect.top;
const uppermostItemMax = uppermostItemRect.bottom;
let nextStartIndex = this.#startIndex;
const downwardPx = scrollportMin - uppermostItemMax;
const upwardPx = uppermostItemMin - scrollportMin;
if (downwardPx > 0) {
nextStartIndex = this.#increaseStartIndex(nextStartIndex, downwardPx);
} else if (upwardPx > 0) {
nextStartIndex = this.#decreaseStartIndex(nextStartIndex, upwardPx);
}
nextStartIndex = Math.max(
0,
Math.min(nextStartIndex, this.props.items.length - this.#numItemsToDraw)
);
if (nextStartIndex !== this.#startIndex) {
this.#startIndex = nextStartIndex;
this.forceUpdate();
} else {
const { serviceContainer } = this.props;
serviceContainer.emitForTests("lazy-message-list-updated-or-noop");
}
}
#addListeners() {
const { viewportRef } = this.props;
viewportRef.current.addEventListener("scroll", this.#scrollHandlerBinding);
this.#resizeObserver = new ResizeObserver(entries => {
this.#viewportHeight =
viewportRef.current.parentNode.parentNode.clientHeight;
this.forceUpdate();
});
}
#removeListeners() {
const { viewportRef } = this.props;
this.#resizeObserver?.disconnect();
viewportRef.current?.removeEventListener(
"scroll",
this.#scrollHandlerBinding
);
}
get bottomBuffer() {
return this.#bottomBufferRef.current;
}
isItemNearBottom(index) {
return index >= this.props.items.length - this.#numItemsToDraw;
}
render() {
const {
items,
itemDefaultHeight,
renderItem,
itemsToKeepAlive,
} = this.props;
if (!items.length) {
return createElement(Fragment, {
key: "LazyMessageList",
});
}
// Resize our cached heights to fit if necessary.
const countUncached = items.length - this.#cachedHeights.length;
if (countUncached > 0) {
// It would be lovely if javascript allowed us to resize an array in one
// go. I think this is the closest we can get to that. This in theory
// allows us to realloc, and doesn't require copying the whole original
// array like concat does.
this.#cachedHeights.push(...Array(countUncached).fill(itemDefaultHeight));
}
let topBufferHeight = 0;
let bottomBufferHeight = 0;
// We can't compute the bottom buffer height until the end, so we just
// store the index of where it needs to go.
let bottomBufferIndex = 0;
let currentChild = 0;
const startIndex = this.#startIndex;
const endIndex = this.#clampedEndIndex;
// We preallocate this array to avoid allocations in the loop. The minimum,
// and typical length for it is the size of the body plus 2 for the top and
// bottom buffers. It can be bigger due to itemsToKeepAlive, but we can't just
// add the size, since itemsToKeepAlive could in theory hold items which are
// not even in the list.
const children = new Array(endIndex - startIndex + 2);
const pushChild = c => {
if (currentChild >= children.length) {
children.push(c);
} else {
children[currentChild] = c;
}
return currentChild++;
};
for (let i = 0; i < items.length; i++) {
const itemId = items[i];
if (i < startIndex) {
if (i == 0 || itemsToKeepAlive.has(itemId)) {
// If this is our first item, and we wouldn't otherwise be rendering
// it, we want to ensure that it's at the beginning of our children
// array to ensure keyboard navigation functions properly.
pushChild(renderItem(itemId, i));
} else {
topBufferHeight += this.#cachedHeights[i];
}
} else if (i < endIndex) {
if (i == startIndex) {
pushChild(
createElement("div", {
key: "LazyMessageListTop",
className: "lazy-message-list-top",
ref: this.#topBufferRef,
style: { height: topBufferHeight },
})
);
}
pushChild(renderItem(itemId, i));
if (i == endIndex - 1) {
// We're just reserving the bottom buffer's spot in the children
// array here. We will create the actual element and assign it at
// this index after the loop.
bottomBufferIndex = pushChild(null);
}
} else if (i == items.length - 1 || itemsToKeepAlive.has(itemId)) {
// Similarly to the logic for our first item, we also want to ensure
// that our last item is always rendered as the last item in our
// children array.
pushChild(renderItem(itemId, i));
} else {
bottomBufferHeight += this.#cachedHeights[i];
}
}
children[bottomBufferIndex] = createElement("div", {
key: "LazyMessageListBottom",
className: "lazy-message-list-bottom",
ref: this.#bottomBufferRef,
style: { height: bottomBufferHeight },
});
return createElement(
Fragment,
{
key: "LazyMessageList",
},
children
);
}
}
module.exports = LazyMessageList;

Просмотреть файл

@ -45,6 +45,7 @@ class CSSWarning extends Component {
repeat: PropTypes.any,
serviceContainer: PropTypes.object,
timestampsVisible: PropTypes.bool.isRequired,
setExpanded: PropTypes.func,
};
}
@ -92,6 +93,7 @@ class CSSWarning extends Component {
serviceContainer,
timestampsVisible,
inWarningGroup,
setExpanded,
} = this.props;
const {
@ -136,6 +138,7 @@ class CSSWarning extends Component {
escapeWhitespace: false,
grip: cssMatchingElements,
serviceContainer,
setExpanded,
})
);

Просмотреть файл

@ -45,6 +45,7 @@ function ConsoleApiCall(props) {
timestampsVisible,
repeat,
maybeScrollToBottom,
setExpanded,
} = props;
const {
id: messageId,
@ -70,6 +71,7 @@ function ConsoleApiCall(props) {
serviceContainer,
type,
maybeScrollToBottom,
setExpanded,
// When the object is a parameter of a console.dir call, we always want to show its
// properties, like regular object (i.e. not showing the DOM tree for an Element, or
// only showing the message + stacktrace for Error object).
@ -114,6 +116,7 @@ function ConsoleApiCall(props) {
serviceContainer,
useQuotes: false,
transformEmptyString: true,
setExpanded,
type,
});
}
@ -173,6 +176,7 @@ function formatReps(options = {}) {
userProvidedStyles,
type,
maybeScrollToBottom,
setExpanded,
customFormat,
} = options;
@ -192,6 +196,7 @@ function formatReps(options = {}) {
loadedObjectEntries,
type,
maybeScrollToBottom,
setExpanded,
customFormat,
})
);

Просмотреть файл

@ -35,6 +35,7 @@ function EvaluationResult(props) {
timestampsVisible,
maybeScrollToBottom,
open,
setExpanded,
} = props;
const {
@ -86,6 +87,7 @@ function EvaluationResult(props) {
type,
helperType,
maybeScrollToBottom,
setExpanded,
customFormat: true,
})
);

Просмотреть файл

@ -26,6 +26,7 @@ PageError.propTypes = {
timestampsVisible: PropTypes.bool.isRequired,
serviceContainer: PropTypes.object,
maybeScrollToBottom: PropTypes.func,
setExpanded: PropTypes.func,
inWarningGroup: PropTypes.bool.isRequired,
};
@ -42,6 +43,7 @@ function PageError(props) {
serviceContainer,
timestampsVisible,
maybeScrollToBottom,
setExpanded,
inWarningGroup,
} = props;
const {
@ -81,6 +83,7 @@ function PageError(props) {
type,
customFormat: true,
maybeScrollToBottom,
setExpanded,
...repsProps,
})
);

Просмотреть файл

@ -12,6 +12,7 @@ DevToolsModules(
"ConsoleOutput.js",
"ConsoleTable.js",
"GripMessageBody.js",
"LazyMessageList.js",
"Message.js",
"MessageContainer.js",
"MessageIcon.js",

Просмотреть файл

@ -43,6 +43,7 @@ const UiState = overrides =>
editorPrettifiedAt: null,
showEditorOnboarding: false,
filterBarDisplayMode: FILTERBAR_DISPLAY_MODES.WIDE,
cacheGeneration: 0,
},
overrides
)
@ -67,7 +68,12 @@ function ui(state = UiState(), action) {
case INITIALIZE:
return { ...state, initialized: true };
case MESSAGES_CLEAR:
return { ...state, sidebarVisible: false, frontInSidebar: null };
return {
...state,
sidebarVisible: false,
frontInSidebar: null,
cacheGeneration: state.cacheGeneration + 1,
};
case SHOW_OBJECT_IN_SIDEBAR:
if (action.front === state.frontInSidebar) {
return state;

Просмотреть файл

@ -9,6 +9,7 @@ support-files =
code_bundle_nosource.js.map
cookieSetter.html
head.js
shared-head.js
sjs_cors-test-server.sjs
sjs_slow-response-test-server.sjs
source-mapped.css
@ -365,6 +366,7 @@ skip-if =
[browser_webconsole_sandbox_update_after_navigation.js]
[browser_webconsole_script_errordoc_urls.js]
[browser_webconsole_scroll.js]
skip-if = true # bug 1763459
[browser_webconsole_select_all.js]
[browser_webconsole_show_subresource_security_errors.js]
skip-if = verify

Просмотреть файл

@ -66,7 +66,11 @@ async function testMessages() {
// Wait a bit to let room for the message to be displayed
await wait(1000);
is(
findMessage(hud, "The Web Console logging API", ".warn"),
await findMessageVirtualized({
hud,
text: "The Web Console logging API",
selector: ".warn",
}),
undefined,
"The message about disabled console API is not displayed"
);

Просмотреть файл

@ -166,7 +166,8 @@ add_task(async function() {
info("Check that we have the expected number of commands");
const expectedInputsNumber = 16;
is(
hud.ui.outputNode.querySelectorAll(".message.command").length,
(await findMessagesVirtualized({ hud, selector: ".message.command" }))
.length,
expectedInputsNumber,
"There is the expected number of commands messages"
);
@ -174,14 +175,15 @@ add_task(async function() {
info("Check that we have as many errors as commands");
const expectedErrorsNumber = expectedInputsNumber;
is(
hud.ui.outputNode.querySelectorAll(".message.error").length,
(await findMessagesVirtualized({ hud, selector: ".message.error" })).length,
expectedErrorsNumber,
"There is the expected number of error messages"
);
info("Check that there's no result message");
is(
hud.ui.outputNode.querySelectorAll(".message.result").length,
(await findMessagesVirtualized({ hud, selector: ".message.result" }))
.length,
0,
"There is no result messages"
);

Просмотреть файл

@ -33,13 +33,19 @@ add_task(async function() {
const ToolboxTask = await initBrowserToolboxTask({
enableContentMessages: true,
});
await ToolboxTask.importFunctions({ findMessages, findMessage, waitUntil });
await ToolboxTask.importFunctions({
findMessagesVirtualized,
findMessageVirtualized,
waitUntil,
});
// Make sure the data: URL message appears in the OBT.
await ToolboxTask.spawn(null, async () => {
await gToolbox.selectTool("webconsole");
const hud = gToolbox.getCurrentPanel().hud;
await waitUntil(() => findMessage(hud, "Data Message"));
await waitUntil(() =>
findMessageVirtualized({ hud, text: "Data Message" })
);
});
ok(true, "First message appeared in toolbox");
@ -49,7 +55,7 @@ add_task(async function() {
// Make sure the example.com message appears in the OBT.
await ToolboxTask.spawn(null, async () => {
const hud = gToolbox.getCurrentPanel().hud;
await waitUntil(() => findMessage(hud, "stringLog"));
await waitUntil(() => findMessageVirtualized({ hud, text: "stringLog" }));
});
ok(true, "New message appeared in toolbox");

Просмотреть файл

@ -23,9 +23,15 @@ async function testSimpleBatchLogging(hud, messageNumber) {
) {
content.wrappedJSObject.batchLog(numMessages);
});
const allMessages = await waitFor(async () => {
const msgs = await findAllMessagesVirtualized(hud);
if (msgs.length == messageNumber) {
return msgs;
}
return null;
});
for (let i = 0; i < messageNumber; i++) {
const node = await waitFor(() => findMessageAtIndex(hud, i, i));
const node = allMessages[i].querySelector(".message-body");
is(
node.textContent,
i.toString(),
@ -48,8 +54,3 @@ async function testBatchLoggingAndClear(hud, messageNumber) {
const messages = findMessages(hud, "");
is(messages.length, 1, "console was cleared as expected");
}
function findMessageAtIndex(hud, text, index) {
const selector = `.message:nth-of-type(${index + 1}) .message-body`;
return findMessage(hud, text, selector);
}

Просмотреть файл

@ -30,7 +30,7 @@ add_task(async function() {
);
is(
findMessages(hud, "startup message").length,
(await findAllMessagesVirtualized(hud)).length,
50,
"We have the expected number of messages"
);

Просмотреть файл

@ -415,24 +415,20 @@ add_task(async function() {
});
}
);
const nodes = [];
for (const testCase of testCases) {
const node = await waitFor(() =>
findConsoleTable(hud.ui.outputNode, testCases.indexOf(testCase))
);
nodes.push(node);
}
const consoleTableNodes = hud.ui.outputNode.querySelectorAll(
".message .consoletable"
);
is(
consoleTableNodes.length,
testCases.length,
"console has the expected number of consoleTable items"
);
const messages = await waitFor(async () => {
const msgs = await findAllMessagesVirtualized(hud);
if (msgs.length === testCases.length) {
return msgs;
}
return null;
});
for (const [index, testCase] of testCases.entries()) {
await testItem(testCase, nodes[index]);
// Refresh the reference to the message, as it may have been scrolled out of existence.
const node = await findMessageVirtualized({
hud,
messageId: messages[index].getAttribute("data-message-id"),
});
await testItem(testCase, node.querySelector(".consoletable"));
}
});
@ -493,7 +489,14 @@ async function testItem(testCase, node) {
});
if (testCase.expected.overflow) {
ok(node.scrollHeight > node.clientHeight, "table overflows");
ok(
node.isConnected,
"Node must be connected to test overflow. It is likely scrolled out of view."
);
ok(
node.scrollHeight > node.clientHeight,
testCase.info + " table overflows"
);
ok(getComputedStyle(node).overflowY !== "hidden", "table can be scrolled");
}
@ -501,10 +504,3 @@ async function testItem(testCase, node) {
await testCase.additionalTest(node);
}
}
function findConsoleTable(node, index) {
const condition = node.querySelector(
`.message:nth-of-type(${index + 1}) .consoletable`
);
return condition;
}

Просмотреть файл

@ -83,6 +83,11 @@ add_task(async function() {
});
async function storeAsVariable(hud, msg, type, varIdx, equalTo) {
// Refresh the reference to the message, as it may have been scrolled out of existence.
msg = await findMessageVirtualized({
hud,
messageId: msg.getAttribute("data-message-id"),
});
const element = msg.querySelector(".objectBox-" + type);
const menuPopup = await openContextMenu(hud, element);
const storeMenuItem = menuPopup.querySelector("#console-menu-store");

Просмотреть файл

@ -37,7 +37,7 @@ add_task(async function() {
onCorsMessage = waitForMessage(hud, "Reason: CORS disabled");
makeFaultyCorsCall("CORSDisabled");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSDisabled");
await checkCorsMessage(hud, message, "CORSDisabled");
await pushPref("content.cors.disable", false);
info("Test CORSPreflightDidNotSucceed");
@ -47,7 +47,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSPreflightDidNotSucceed");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSPreflightDidNotSucceed");
await checkCorsMessage(hud, message, "CORSPreflightDidNotSucceed");
info("Test CORS did not succeed");
onCorsMessage = waitForMessage(
@ -56,7 +56,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSDidNotSucceed");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSDidNotSucceed");
await checkCorsMessage(hud, message, "CORSDidNotSucceed");
info("Test CORSExternalRedirectNotAllowed");
onCorsMessage = waitForMessage(
@ -65,7 +65,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSExternalRedirectNotAllowed");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSExternalRedirectNotAllowed");
await checkCorsMessage(hud, message, "CORSExternalRedirectNotAllowed");
info("Test CORSMissingAllowOrigin");
onCorsMessage = waitForMessage(
@ -76,7 +76,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSMissingAllowOrigin");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSMissingAllowOrigin");
await checkCorsMessage(hud, message, "CORSMissingAllowOrigin");
info("Test CORSMultipleAllowOriginNotAllowed");
onCorsMessage = waitForMessage(
@ -87,7 +87,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSMultipleAllowOriginNotAllowed");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSMultipleAllowOriginNotAllowed");
await checkCorsMessage(hud, message, "CORSMultipleAllowOriginNotAllowed");
info("Test CORSAllowOriginNotMatchingOrigin");
onCorsMessage = waitForMessage(
@ -99,7 +99,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSAllowOriginNotMatchingOrigin");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSAllowOriginNotMatchingOrigin");
await checkCorsMessage(hud, message, "CORSAllowOriginNotMatchingOrigin");
info("Test CORSNotSupportingCredentials");
onCorsMessage = waitForMessage(
@ -109,7 +109,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSNotSupportingCredentials");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSNotSupportingCredentials");
await checkCorsMessage(hud, message, "CORSNotSupportingCredentials");
info("Test CORSMethodNotFound");
onCorsMessage = waitForMessage(
@ -119,7 +119,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSMethodNotFound");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSMethodNotFound");
await checkCorsMessage(hud, message, "CORSMethodNotFound");
info("Test CORSMissingAllowCredentials");
onCorsMessage = waitForMessage(
@ -129,7 +129,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSMissingAllowCredentials");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSMissingAllowCredentials");
await checkCorsMessage(hud, message, "CORSMissingAllowCredentials");
info("Test CORSInvalidAllowMethod");
onCorsMessage = waitForMessage(
@ -139,7 +139,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSInvalidAllowMethod");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSInvalidAllowMethod");
await checkCorsMessage(hud, message, "CORSInvalidAllowMethod");
info("Test CORSInvalidAllowHeader");
onCorsMessage = waitForMessage(
@ -149,7 +149,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSInvalidAllowHeader");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSInvalidAllowHeader");
await checkCorsMessage(hud, message, "CORSInvalidAllowHeader");
info("Test CORSMissingAllowHeaderFromPreflight");
onCorsMessage = waitForMessage(
@ -161,7 +161,7 @@ add_task(async function() {
);
makeFaultyCorsCall("CORSMissingAllowHeaderFromPreflight");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSMissingAllowHeaderFromPreflight");
await checkCorsMessage(hud, message, "CORSMissingAllowHeaderFromPreflight");
// See Bug 1480671.
// XXX: how to make Origin to not be included in the request ?
@ -169,7 +169,7 @@ add_task(async function() {
// `Reason: CORS header ${quote("Origin")} cannot be added`);
// makeFaultyCorsCall("CORSOriginHeaderNotAdded");
// message = await onCorsMessage;
// await checkCorsMessage(message, "CORSOriginHeaderNotAdded");
// await checkCorsMessage(hud, message, "CORSOriginHeaderNotAdded");
// See Bug 1480672.
// XXX: Failing with another error: Console message: Security Error: Content at
@ -181,11 +181,16 @@ add_task(async function() {
// dir.append("sjs_cors-test-server.sjs");
// makeFaultyCorsCall("CORSRequestNotHttp", Services.io.newFileURI(dir).spec);
// message = await onCorsMessage;
// await checkCorsMessage(message, "CORSRequestNotHttp");
// await checkCorsMessage(hud, message, "CORSRequestNotHttp");
});
async function checkCorsMessage(message, category) {
const node = message.node;
async function checkCorsMessage(hud, message, category) {
// Get a new reference to the node, as it may have been scrolled out of existence.
const node = await findMessageVirtualized({
hud,
messageId: message.node.getAttribute("data-message-id"),
});
node.scrollIntoView();
ok(
node.classList.contains("error"),
"The cors message has the expected classname"

Просмотреть файл

@ -154,8 +154,8 @@ add_task(async function() {
text: "",
});
const groupA = findMessage(hud, "[a]");
const groupJ = findMessage(hud, "[j]");
const groupA = await findMessageVirtualized({ hud, text: "[a]" });
const groupJ = await findMessageVirtualized({ hud, text: "[j]" });
toggleGroup(groupA);
toggleGroup(groupJ);

Просмотреть файл

@ -28,7 +28,7 @@ add_task(async function() {
"Filter out some messages and check that the scroll position is not impacted"
);
let onMessagesFiltered = waitFor(
() => !findMessage(hud, "init-1"),
() => !findMessage(hud, "init-0"),
null,
200
);
@ -43,7 +43,7 @@ add_task(async function() {
"Clear the text filter and check that the scroll position is not impacted"
);
let onMessagesUnFiltered = waitFor(
() => findMessage(hud, "init-1"),
() => findMessage(hud, "init-0"),
null,
200
);
@ -63,7 +63,11 @@ add_task(async function() {
);
await setFilterState(hud, { text: "init-9" });
onMessagesFiltered = waitFor(() => !findMessage(hud, "init-1"), null, 200);
onMessagesFiltered = waitFor(
async () => !findMessage(hud, "init-0"),
null,
200
);
await onMessagesFiltered;
is(
outputContainer.scrollTop,
@ -74,7 +78,7 @@ add_task(async function() {
info(
"Clear the text filter and check that the scroll position is not impacted"
);
onMessagesUnFiltered = waitFor(() => findMessage(hud, "init-1"), null, 200);
onMessagesUnFiltered = waitFor(() => findMessage(hud, "init-0"), null, 200);
await setFilterState(hud, { text: "" });
await onMessagesUnFiltered;
is(

Просмотреть файл

@ -27,7 +27,8 @@ add_task(async function() {
const outputContainer = ui.outputNode.querySelector(".webconsole-output");
is(
outputContainer.querySelectorAll(".message.console-api").length,
(await findMessagesVirtualized({ hud, selector: ".message.console-api" }))
.length,
20,
"Correct number of messages appear"
);

Просмотреть файл

@ -18,7 +18,7 @@ add_task(async function() {
info("Web Console opened");
const outputScroller = hud.ui.outputScroller;
await waitFor(
() => findMessages(hud, "").length == 100,
() => findMessage(hud, "console message 100"),
"waiting for all the messages to be displayed",
100,
1000

Просмотреть файл

@ -22,11 +22,20 @@ add_task(async function() {
});
await onMessage;
ok(!findMessage(hud, "test message [0]"), "Message 0 has been pruned");
ok(!findMessage(hud, "test message [9]"), "Message 9 has been pruned");
ok(findMessage(hud, "test message [10]"), "Message 10 is still displayed");
ok(
!(await findMessageVirtualized({ hud, text: "test message [0]" })),
"Message 0 has been pruned"
);
ok(
!(await findMessageVirtualized({ hud, text: "test message [9]" })),
"Message 9 has been pruned"
);
ok(
await findMessageVirtualized({ hud, text: "test message [10]" }),
"Message 10 is still displayed"
);
is(
findMessages(hud, "").length,
(await findAllMessagesVirtualized(hud)).length,
140,
"Number of displayed messages is correct"
);
@ -37,10 +46,16 @@ add_task(async function() {
});
await onMessage;
ok(!findMessage(hud, "test message [10]"), "Message 10 has been pruned");
ok(findMessage(hud, "test message [11]"), "Message 11 is still displayed");
ok(
!(await findMessageVirtualized({ hud, text: "test message [10]" })),
"Message 10 has been pruned"
);
ok(
await findMessageVirtualized({ hud, text: "test message [11]" }),
"Message 11 is still displayed"
);
is(
findMessages(hud, "").length,
(await findAllMessagesVirtualized(hud)).length,
140,
"Number of displayed messages is still correct"
);

Просмотреть файл

@ -7,13 +7,13 @@ const TEST_URI = `data:text/html;charset=utf-8,<!DOCTYPE html><p>Web Console tes
<script>
var a = () => b();
var b = () => c();
var c = () => console.trace("trace in C");
var c = (i) => console.trace("trace in C " + i);
for (let i = 0; i < 100; i++) {
if (i % 10 === 0) {
c();
}
for (let i = 0; i <= 100; i++) {
console.log("init-" + i);
if (i % 10 === 0) {
c(i);
}
}
</script>
`;
@ -23,7 +23,7 @@ add_task(async function() {
const outputContainer = ui.outputNode.querySelector(".webconsole-output");
info("Console should be scrolled to bottom on initial load from page logs");
await waitFor(() => findMessage(hud, "init-99"));
await waitFor(() => findMessage(hud, "init-100"));
ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow");
ok(
isScrolledToBottom(outputContainer),
@ -31,8 +31,8 @@ add_task(async function() {
);
info("Wait until all stacktraces are rendered");
await waitFor(
() => outputContainer.querySelectorAll(".frames").length === 10
await waitFor(() =>
findMessages(hud, "")?.every(m => m.textContent.length > 0)
);
ok(
isScrolledToBottom(outputContainer),
@ -42,7 +42,7 @@ add_task(async function() {
await reloadBrowser();
info("Console should be scrolled to bottom after refresh from page logs");
await waitFor(() => findMessage(hud, "init-99"));
await waitFor(() => findMessage(hud, "init-100"));
ok(hasVerticalOverflow(outputContainer), "There is a vertical overflow");
ok(
isScrolledToBottom(outputContainer),
@ -50,8 +50,17 @@ add_task(async function() {
);
info("Wait until all stacktraces are rendered");
await waitFor(
() => outputContainer.querySelectorAll(".frames").length === 10
await waitFor(() =>
findMessages(hud, "")?.every(m => m.textContent.length > 0)
);
// There's an annoying race here where the SmartTrace from above goes into
// the DOM, our waitFor passes, but the SmartTrace still hasn't called its
// onReady callback. If this happens, it will call ConsoleOutput's
// maybeScrollToBottomMessageCallback *after* we set scrollTop below,
// causing it to undo our work. Waiting a little bit here should resolve it.
await new Promise(r =>
window.requestAnimationFrame(() => TestUtils.executeSoon(r))
);
ok(
isScrolledToBottom(outputContainer),

Просмотреть файл

@ -29,9 +29,9 @@ function testBrowserMenuSelectAll(hud) {
const outputContainer = ui.outputNode.querySelector(".webconsole-output");
is(
outputContainer.childNodes.length,
outputContainer.querySelectorAll(".message").length,
6,
"the output node contains the expected number of children"
"the output node contains the expected number of messages"
);
// The focus is on the JsTerm, so we need to blur it for the copy comand to
@ -51,7 +51,13 @@ function checkMessagesSelected(outputContainer) {
const messages = outputContainer.querySelectorAll(".message");
for (const message of messages) {
const selected = selection.containsNode(message);
// Oddly, something about the top and bottom buffer having user-select be
// 'none' means that the messages themselves don't register as selected.
// However, all of their children will count as selected, which should be
// good enough for our purposes.
const selected = [...message.children].every(c =>
selection.containsNode(c)
);
ok(selected, `Node containing text "${message.textContent}" was selected`);
}
}

Просмотреть файл

@ -121,14 +121,11 @@ add_task(async function() {
);
info("Wait for all messages to be visible in the split console");
const waitForMessagePromises = [];
for (let j = 1; j <= MESSAGES_COUNT; j++) {
waitForMessagePromises.push(
waitFor(() => findMessage(hud, "in-inspector log " + j))
);
}
await Promise.all(waitForMessagePromises);
await waitFor(
async () =>
(await findMessagesVirtualized({ hud, text: "in-inspector log " }))
.length === MESSAGES_COUNT
);
ok(true, "All the messages logged when we are using the split console");
await toolbox.closeSplitConsole();

Просмотреть файл

@ -84,7 +84,7 @@ add_task(async function testContentBlockingMessage() {
"The badge has the expected text"
);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL} 2`,
]);
@ -92,7 +92,7 @@ add_task(async function testContentBlockingMessage() {
node.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, "https://tracking.example.com/?1"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL} 2`,
`| The resource at \u201chttps://tracking.example.com/?1&${now}\u201d was blocked`,
`| The resource at \u201chttps://tracking.example.com/?2&${now}\u201d was blocked`,
@ -206,13 +206,13 @@ async function testStorageAccessBlockedGrouping(groupLabel) {
"The badge has the expected text"
);
checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${groupLabel} 2`]);
await checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${groupLabel} 2`]);
info("Open the group");
node.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, TRACKER_IMG));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${groupLabel} 2`,
`| ${getWarningMessage(TRACKER_IMG + "?1&" + now)}`,
`| ${getWarningMessage(TRACKER_IMG + "?2&" + now)}`,

Просмотреть файл

@ -87,13 +87,13 @@ add_task(async function testSameSiteCookieMessage() {
"The badge has the expected text"
);
checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${test.groupLabel} 2`]);
await checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${test.groupLabel} 2`]);
info("Open the group");
node.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, "SameSite"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${test.groupLabel} 2`,
`| ${test.message1}`,
`| ${test.message2}`,
@ -132,13 +132,13 @@ add_task(async function testInvalidSameSiteMessage() {
"The badge has the expected text"
);
checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${groupLabel} 2`]);
await checkConsoleOutputForWarningGroup(hud, [`▶︎⚠ ${groupLabel} 2`]);
info("Open the group");
node.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, "SameSite"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${groupLabel} 2`,
`| ${message1}`,
`| ${message2}`,

Просмотреть файл

@ -87,7 +87,7 @@ add_task(async function testContentBlockingMessage() {
"The badge has the expected text"
);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`,
`simple message 1`,
]);
@ -114,7 +114,7 @@ add_task(async function testContentBlockingMessage() {
info("Log a second simple message");
await logString(hud, "simple message 2");
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`,
`simple message 1`,
`${STORAGE_BLOCKED_URL}`,
@ -144,7 +144,7 @@ add_task(async function testContentBlockingMessage() {
storageBlockedWarningGroupNode.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, STORAGE_BLOCKED_URL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`,
`simple message 1`,
`▼︎⚠ ${STORAGE_BLOCKED_GROUP_LABEL}`,
@ -164,7 +164,7 @@ add_task(async function testContentBlockingMessage() {
emitStorageAccessBlockedMessage(hud);
await onStorageBlockedMessage;
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`,
`simple message 1`,
`▼︎⚠ ${STORAGE_BLOCKED_GROUP_LABEL}`,
@ -184,7 +184,7 @@ add_task(async function testContentBlockingMessage() {
contentBlockedWarningGroupNode.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, CONTENT_BLOCKED_URL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`,
`| ${CONTENT_BLOCKED_URL}?1`,
`| ${CONTENT_BLOCKED_URL}?2`,
@ -212,7 +212,7 @@ add_task(async function testContentBlockingMessage() {
emitContentBlockingMessage(hud);
await onContentBlockedMessage;
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`,
`| ${CONTENT_BLOCKED_URL}?1`,
`| ${CONTENT_BLOCKED_URL}?2`,
@ -247,7 +247,7 @@ add_task(async function testContentBlockingMessage() {
emitContentBlockingMessage();
await onContentBlockedWarningGroupMessage;
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKED_GROUP_LABEL}`,
`| ${CONTENT_BLOCKED_URL}?1`,
`| ${CONTENT_BLOCKED_URL}?2`,

Просмотреть файл

@ -72,7 +72,7 @@ add_task(async function testStorageIsolationMessage() {
"The badge has the expected text"
);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${STORAGE_ISOLATION_GROUP_LABEL} 2`,
]);
@ -80,7 +80,7 @@ add_task(async function testStorageIsolationMessage() {
node.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, url1));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${STORAGE_ISOLATION_GROUP_LABEL} 2`,
`| ${getWarningMessage(url1)}`,
`| ${getWarningMessage(url2)}`,

Просмотреть файл

@ -76,7 +76,7 @@ add_task(async function testContentBlockingMessage() {
"The badge has the expected text"
);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`simple message 1`,
]);
@ -84,7 +84,7 @@ add_task(async function testContentBlockingMessage() {
info("Log another simple message");
await logString(hud, "simple message 2");
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`simple message 1`,
`simple message 2`,
@ -98,7 +98,7 @@ add_task(async function testContentBlockingMessage() {
() => node.querySelector(".warning-group-badge").textContent == "3"
);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`simple message 1`,
`simple message 2`,
@ -108,7 +108,7 @@ add_task(async function testContentBlockingMessage() {
node.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, BLOCKED_URL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -125,7 +125,7 @@ add_task(async function testContentBlockingMessage() {
await onContentBlockingWarningMessage;
ok(true, "The new tracking protection message is displayed");
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -148,7 +148,7 @@ add_task(async function testContentBlockingMessage() {
await logString(hud, "simple message 3");
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -177,7 +177,7 @@ add_task(async function testContentBlockingMessage() {
"The badge has the expected text"
);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -194,7 +194,7 @@ add_task(async function testContentBlockingMessage() {
node.querySelector(".arrow").click();
await waitFor(() => findMessages(hud, BLOCKED_URL).length === 6);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -213,7 +213,7 @@ add_task(async function testContentBlockingMessage() {
node.querySelector(".arrow").click();
await waitFor(() => findMessages(hud, BLOCKED_URL).length === 4);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -234,7 +234,7 @@ add_task(async function testContentBlockingMessage() {
() => node.querySelector(".warning-group-badge").textContent == "3"
);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,

Просмотреть файл

@ -79,7 +79,7 @@ add_task(async function testContentBlockingMessage() {
emitContentBlockedMessage(hud);
await waitForBadgeNumber(warningGroupMessage2, "3");
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`simple message A #1`,
`simple message A #2`,
@ -95,7 +95,7 @@ add_task(async function testContentBlockingMessage() {
await setFilterState(hud, { warn: false });
await waitFor(() => !findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`simple message A #1`,
`simple message A #2`,
`simple message B #1`,
@ -109,7 +109,7 @@ add_task(async function testContentBlockingMessage() {
await setFilterState(hud, { warn: true });
await waitFor(() => findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`simple message A #1`,
`simple message A #2`,
@ -127,7 +127,7 @@ add_task(async function testContentBlockingMessage() {
.click();
await waitFor(() => findMessage(hud, BLOCKED_URL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -147,7 +147,7 @@ add_task(async function testContentBlockingMessage() {
await setFilterState(hud, { warn: false });
await waitFor(() => !findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`simple message A #1`,
`simple message A #2`,
`simple message B #1`,
@ -160,7 +160,7 @@ add_task(async function testContentBlockingMessage() {
info("Display warning messages again");
await setFilterState(hud, { warn: true });
await waitFor(() => findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -179,7 +179,7 @@ add_task(async function testContentBlockingMessage() {
info("Filter on warning group text");
await setFilterState(hud, { text: CONTENT_BLOCKING_GROUP_LABEL });
await waitFor(() => !findMessage(hud, "simple message"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -195,7 +195,7 @@ add_task(async function testContentBlockingMessage() {
.click();
await waitFor(() => findMessage(hud, "?6"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -211,7 +211,7 @@ add_task(async function testContentBlockingMessage() {
info("Filter on warning message text from a single warning group");
await setFilterState(hud, { text: "/\\?(2|4)/" });
await waitFor(() => !findMessage(hud, "?1"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?2`,
`| ${BLOCKED_URL}?4`,
@ -221,7 +221,7 @@ add_task(async function testContentBlockingMessage() {
info("Filter on warning message text from two warning groups");
await setFilterState(hud, { text: "/\\?(3|6|7)/" });
await waitFor(() => findMessage(hud, "?7"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?3`,
`Navigated to`,
@ -232,7 +232,7 @@ add_task(async function testContentBlockingMessage() {
info("Clearing text filter");
await setFilterState(hud, { text: "" });
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -254,7 +254,7 @@ add_task(async function testContentBlockingMessage() {
info("Filter warnings with two opened warning groups");
await setFilterState(hud, { warn: false });
await waitFor(() => !findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`simple message A #1`,
`simple message A #2`,
`simple message B #1`,
@ -267,7 +267,7 @@ add_task(async function testContentBlockingMessage() {
info("Display warning messages again with two opened warning groups");
await setFilterState(hud, { warn: true });
await waitFor(() => findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,

Просмотреть файл

@ -45,7 +45,7 @@ add_task(async function testContentBlockingMessage() {
const { node: consoleGroupMessageNode } = await onGroupMessage;
await onInGroupMessage;
checkConsoleOutputForWarningGroup(hud, [`▼ myGroup`, `| log in group`]);
await checkConsoleOutputForWarningGroup(hud, [`▼ myGroup`, `| log in group`]);
info(
"Log a tracking protection message to check a single message isn't grouped"
@ -59,7 +59,7 @@ add_task(async function testContentBlockingMessage() {
emitContentBlockedMessage(now);
await onContentBlockingWarningMessage;
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼ myGroup`,
`| log in group`,
`| ${BLOCKED_URL}?${now}-1`,
@ -69,13 +69,13 @@ add_task(async function testContentBlockingMessage() {
consoleGroupMessageNode.querySelector(".arrow").click();
await waitFor(() => !findMessage(hud, "log in group"));
checkConsoleOutputForWarningGroup(hud, [`▶︎ myGroup`]);
await checkConsoleOutputForWarningGroup(hud, [`▶︎ myGroup`]);
info("Expand the console.group");
consoleGroupMessageNode.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, "log in group"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼ myGroup`,
`| log in group`,
`| ${BLOCKED_URL}?${now}-1`,
@ -92,7 +92,7 @@ add_task(async function testContentBlockingMessage() {
emitContentBlockedMessage(now);
const { node: warningGroupNode } = await onContentBlockingWarningGroupMessage;
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`▼ myGroup`,
`| log in group`,
@ -102,7 +102,7 @@ add_task(async function testContentBlockingMessage() {
warningGroupNode.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, BLOCKED_URL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?${now}-1`,
`| ${BLOCKED_URL}?${now}-2`,
@ -118,7 +118,7 @@ add_task(async function testContentBlockingMessage() {
await onContentBlockingWarningMessage;
ok(true, "The new tracking protection message is displayed");
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?${now}-1`,
`| ${BLOCKED_URL}?${now}-2`,
@ -134,7 +134,7 @@ add_task(async function testContentBlockingMessage() {
});
await onInGroupMessage;
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?${now}-1`,
`| ${BLOCKED_URL}?${now}-2`,
@ -148,7 +148,7 @@ add_task(async function testContentBlockingMessage() {
consoleGroupMessageNode.querySelector(".arrow").click();
await waitFor(() => !findMessage(hud, "log in group"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?${now}-1`,
`| ${BLOCKED_URL}?${now}-2`,
@ -160,7 +160,7 @@ add_task(async function testContentBlockingMessage() {
warningGroupNode.querySelector(".arrow").click();
await waitFor(() => !findMessage(hud, BLOCKED_URL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`▶︎ myGroup`,
]);
@ -169,7 +169,7 @@ add_task(async function testContentBlockingMessage() {
consoleGroupMessageNode.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, "log in group"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`▼ myGroup`,
`| log in group`,
@ -180,7 +180,7 @@ add_task(async function testContentBlockingMessage() {
consoleGroupMessageNode.querySelector(".arrow").click();
await waitFor(() => !findMessage(hud, "log in group"));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`▶︎ myGroup`,
]);
@ -189,7 +189,7 @@ add_task(async function testContentBlockingMessage() {
warningGroupNode.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, BLOCKED_URL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?${now}-1`,
`| ${BLOCKED_URL}?${now}-2`,

Просмотреть файл

@ -66,7 +66,7 @@ add_task(async function testContentBlockingMessage() {
emitContentBlockedMessage(hud);
await onContentBlockingWarningMessage;
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`${BLOCKED_URL}?1`,
`simple message 1`,
`${BLOCKED_URL}?2`,
@ -84,7 +84,7 @@ add_task(async function testContentBlockingMessage() {
"The badge has the expected text"
);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`simple message 1`,
]);
@ -100,7 +100,7 @@ add_task(async function testContentBlockingMessage() {
await waitFor(() => findMessage(hud, `${BLOCKED_URL}?4`));
// Warning messages are displayed at the expected positions.
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`${BLOCKED_URL}?1`,
`simple message 1`,
`${BLOCKED_URL}?2`,
@ -115,7 +115,7 @@ add_task(async function testContentBlockingMessage() {
findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL)
);
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▶︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`simple message 1`,
]);
@ -124,7 +124,7 @@ add_task(async function testContentBlockingMessage() {
warningGroupMessage1.querySelector(".arrow").click();
await waitFor(() => findMessage(hud, BLOCKED_URL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -150,7 +150,7 @@ add_task(async function testContentBlockingMessage() {
await logString(hud, "simple message 2");
// nothing is grouped.
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`${BLOCKED_URL}?1`,
`simple message 1`,
`${BLOCKED_URL}?2`,
@ -167,7 +167,7 @@ add_task(async function testContentBlockingMessage() {
await toggleWarningGroupPreference(hud);
await waitFor(() => findMessage(hud, CONTENT_BLOCKING_GROUP_LABEL));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -192,7 +192,7 @@ add_task(async function testContentBlockingMessage() {
.node;
await waitForBadgeNumber(warningGroupMessage2, "2");
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`▼︎⚠ ${CONTENT_BLOCKING_GROUP_LABEL}`,
`| ${BLOCKED_URL}?1`,
`| ${BLOCKED_URL}?2`,
@ -210,7 +210,7 @@ add_task(async function testContentBlockingMessage() {
await toggleWarningGroupPreference(hud);
await waitFor(() => findMessage(hud, `${BLOCKED_URL}?6`));
checkConsoleOutputForWarningGroup(hud, [
await checkConsoleOutputForWarningGroup(hud, [
`${BLOCKED_URL}?1`,
`simple message 1`,
`${BLOCKED_URL}?2`,

Просмотреть файл

@ -22,6 +22,12 @@ Services.scriptloader.loadSubScript(
this
);
/* import-globals-from ./shared-head.js */
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/webconsole/test/browser/shared-head.js",
this
);
var {
BrowserConsoleManager,
} = require("devtools/client/webconsole/browser-console-manager");
@ -348,6 +354,7 @@ function findMessages(hud, text, selector = ".message") {
);
return elements;
}
/**
* Wait for a message to be logged and ensure it is logged only once.
*
@ -363,8 +370,8 @@ async function checkUniqueMessageExists(hud, msg, selector) {
info(`Checking "${msg}" was logged`);
let messages;
try {
messages = await waitFor(() => {
const msgs = findMessages(hud, msg, selector);
messages = await waitFor(async () => {
const msgs = await findMessagesVirtualized({ hud, text: msg, selector });
return msgs.length > 0 ? msgs : null;
});
} catch (e) {
@ -1516,8 +1523,8 @@ function isScrolledToBottom(container) {
* Start the string with "| " to indicate that the message is
* inside a group and should be indented.
*/
function checkConsoleOutputForWarningGroup(hud, expectedMessages) {
const messages = findMessages(hud, "");
async function checkConsoleOutputForWarningGroup(hud, expectedMessages) {
const messages = await findAllMessagesVirtualized(hud);
is(
messages.length,
expectedMessages.length,
@ -1540,8 +1547,12 @@ function checkConsoleOutputForWarningGroup(hud, expectedMessages) {
return groups[0].startsWith("▶︎⚠") || groups[0].startsWith("▼⚠");
};
expectedMessages.forEach((expectedMessage, i) => {
const message = messages[i];
for (let [i, expectedMessage] of expectedMessages.entries()) {
// Refresh the reference to the message, as it may have been scrolled out of existence.
const message = await findMessageVirtualized({
hud,
messageId: messages[i].getAttribute("data-message-id"),
});
info(`Checking "${expectedMessage}"`);
// Collapsed Warning group
@ -1618,7 +1629,7 @@ function checkConsoleOutputForWarningGroup(hud, expectedMessages) {
`Message includes ` +
`the expected "${expectedMessage}" content - "${message.textContent.trim()}"`
);
});
}
}
/**

Просмотреть файл

@ -0,0 +1,307 @@
/* 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/>. */
/**
* Helper methods for finding messages in the virtualized output of the
* webconsole. This file can be safely required from other panel test
* files.
*/
"use strict";
/* eslint-disable no-unused-vars */
// Assume that shared-head is always imported before this file
/* import-globals-from ../../../shared/test/shared-head.js */
/**
* Find a message in the output, scrolling through the output from top
* to bottom in order to make sure the messages are actually rendered.
*
* @param object hud
* The web console.
* @param string text
* A substring that can be found in the message.
* @param selector [optional]
* The selector to use in finding the message.
* @param messageId [optional]
* A message ID to look for. This could be baked into the selector, but
* is provided as a convenience.
* @return {Node} the node corresponding the found message
*/
async function findMessageVirtualized({ hud, text, selector, messageId }) {
const elements = await findMessagesVirtualized({
hud,
text,
selector,
expectedCount: 1,
messageId,
});
return elements.at(-1);
}
/**
* Find all messages in the output, scrolling through the output from top
* to bottom in order to make sure the messages are actually rendered.
*
* @param object hud
* The web console.
* @return {Array} all of the message nodes in the console output. Some of
* these may be stale from having been scrolled out of view.
*/
async function findAllMessagesVirtualized(hud) {
return findMessagesVirtualized({ hud });
}
// This is just a reentrancy guard. Because findMessagesVirtualized mucks
// around with the scroll position, if we do something like
// let promise1 = findMessagesVirtualized(...);
// let promise2 = findMessagesVirtualized(...);
// await promise1;
// await promise2;
// then the two calls will end up messing up each other's expected scroll
// position, at which point they could get stuck. This lets us throw an
// error when that happens.
let gInFindMessagesVirtualized = false;
// And this lets us get a little more information in the error - it just holds
// the stack of the prior call.
let gFindMessagesVirtualizedStack = null;
/**
* Find multiple messages in the output, scrolling through the output from top
* to bottom in order to make sure the messages are actually rendered.
*
* @param object options
* @param object options.hud
* The web console.
* @param options.text [optional]
* A substring that can be found in the message.
* @param options.selector [optional]
* The selector to use in finding the message.
* @param options.expectedCount [optional]
* The number of messages to get. This lets us stop scrolling early if
* we find that number of messages.
* @param options.messageId [optional]
* A message ID to look for. This could be baked into the selector, but
* is provided as a convenience.
* @return {Array} all of the message nodes in the console output matching the
* provided filters. If expectedCount is greater than 1, or equal to -1,
* some of these may be stale from having been scrolled out of view.
*/
async function findMessagesVirtualized({
hud,
text,
selector,
expectedCount,
messageId,
}) {
if (text === undefined) {
text = "";
}
if (selector === undefined) {
selector = ".message";
}
if (expectedCount === undefined) {
expectedCount = -1;
}
const outputNode = hud.ui.outputNode;
const scrollport = outputNode.querySelector(".webconsole-output");
function getVisibleMessageIds() {
return JSON.parse(scrollport.getAttribute("data-visible-messages"));
}
function getVisibleMessageMap() {
return new Map(
JSON.parse(
scrollport.getAttribute("data-visible-messages")
).map((id, i) => [id, i])
);
}
function getMessageIndex(message) {
return getVisibleMessageIds().indexOf(
message.getAttribute("data-message-id")
);
}
function getNextMessageId(prevMessage) {
const visible = getVisibleMessageIds();
let index = 0;
if (prevMessage) {
const lastId = prevMessage.getAttribute("data-message-id");
index = visible.indexOf(lastId);
if (index === -1) {
throw new Error(
`Tried to get next message ID for message that doesn't exist. Last seen ID: ${lastId}, all visible ids: [${visible.join(
", "
)}]`
);
}
}
if (index + 1 >= visible.length) {
return null;
}
return visible[index + 1];
}
if (gInFindMessagesVirtualized) {
throw new Error(
`findMessagesVirtualized was re-entered somehow. This is not allowed. Other stack: [${gFindMessagesVirtualizedStack}]`
);
}
try {
gInFindMessagesVirtualized = true;
gFindMessagesVirtualizedStack = new Error().stack;
// The console output will automatically scroll to the bottom of the
// scrollport in certain circumstances. Because we need to scroll the
// output to find all messages, we need to disable this. This attribute
// controls that.
scrollport.setAttribute("disable-autoscroll", "");
// This array is here purely for debugging purposes. We collect the indices
// of every element we see in order to validate that we don't have any gaps
// in the list.
const allIndices = [];
const allElements = [];
const seenIds = new Set();
let lastItem = null;
while (true) {
if (scrollport.scrollHeight > scrollport.clientHeight) {
if (!lastItem && scrollport.scrollTop != 0) {
// For simplicity's sake, we always start from the top of the output.
scrollport.scrollTop = 0;
} else if (!lastItem && scrollport.scrollTop == 0) {
// We want to make sure that we actually change the scroll position
// here, because we're going to wait for an update below regardless,
// just to flush out any changes that may have just happened. If we
// don't do this, and there were no changes before this function was
// called, then we'll just hang on the promise below.
scrollport.scrollTop = 1;
} else {
// This is the core of the loop. Scroll down to the bottom of the
// current scrollport, wait until we see the element after the last
// one we've seen, and then harvest the messages that are displayed.
scrollport.scrollTop = scrollport.scrollTop + scrollport.clientHeight;
}
// Wait for something to happen in the output before checking for our
// expected next message.
await new Promise(resolve =>
hud.ui.once("lazy-message-list-updated-or-noop", resolve)
);
try {
await waitFor(async () => {
const nextMessageId = getNextMessageId(lastItem);
if (
nextMessageId === undefined ||
scrollport.querySelector(`[data-message-id="${nextMessageId}"]`)
) {
return true;
}
// After a scroll, we typically expect to get an updated list of
// elements. However, we have some slack at the top of the list,
// because we draw elements above and below the actual scrollport to
// avoid white flashes when async scrolling.
const scrollTarget = scrollport.scrollTop + scrollport.clientHeight;
scrollport.scrollTop = scrollTarget;
await new Promise(resolve =>
hud.ui.once("lazy-message-list-updated-or-noop", resolve)
);
return false;
});
} catch (e) {
throw new Error(
`Failed waiting for next message ID (${getNextMessageId(
lastItem
)}) Visible messages: [${[
...scrollport.querySelectorAll(".message"),
].map(el => el.getAttribute("data-message-id"))}]`
);
}
}
const bottomPlaceholder = scrollport.querySelector(
".lazy-message-list-bottom"
);
if (!bottomPlaceholder) {
// When there are no messages in the output, there is also no
// top/bottom placeholder. There's nothing more to do at this point,
// so break and return.
break;
}
lastItem = bottomPlaceholder.previousSibling;
// This chunk is just validating that we have no gaps in our output so
// far.
const indices = [...scrollport.querySelectorAll("[data-message-id]")]
.filter(
el => el !== scrollport.firstChild && el !== scrollport.lastChild
)
.map(el => getMessageIndex(el));
allIndices.push(...indices);
allIndices.sort((lhs, rhs) => lhs - rhs);
for (let i = 1; i < allIndices.length; i++) {
if (
allIndices[i] != allIndices[i - 1] &&
allIndices[i] != allIndices[i - 1] + 1
) {
throw new Error(
`Gap detected in virtualized webconsole output between ${
allIndices[i - 1]
} and ${allIndices[i]}. Indices: ${allIndices.join(",")}`
);
}
}
const messages = scrollport.querySelectorAll(selector);
const filtered = [...messages].filter(
el =>
// Core user filters:
el.textContent.includes(text) &&
(!messageId || el.getAttribute("data-message-id") === messageId) &&
// Make sure we don't collect duplicate messages:
!seenIds.has(el.getAttribute("data-message-id"))
);
allElements.push(...filtered);
for (const message of filtered) {
seenIds.add(message.getAttribute("data-message-id"));
}
if (expectedCount >= 0 && allElements.length >= expectedCount) {
break;
}
// If the bottom placeholder has 0 height, it means we've scrolled to the
// bottom and output all the items.
if (bottomPlaceholder.getBoundingClientRect().height == 0) {
break;
}
await waitForTime(0);
}
// Finally, we get the map of message IDs to indices within the output, and
// sort the message nodes according to that index. They can come in out of
// order for a number of reasons (we continue rendering any messages that
// have been expanded, and we always render the topmost and bottommost
// messages for a11y reasons.)
const idsToIndices = getVisibleMessageMap();
allElements.sort(
(lhs, rhs) =>
idsToIndices.get(lhs.getAttribute("data-message-id")) -
idsToIndices.get(rhs.getAttribute("data-message-id"))
);
return allElements;
} finally {
scrollport.removeAttribute("disable-autoscroll");
gInFindMessagesVirtualized = false;
gFindMessagesVirtualizedStack = null;
}
}

Просмотреть файл

@ -13,23 +13,15 @@ const ConsoleOutput = createFactory(
);
const serviceContainer = require("devtools/client/webconsole/test/node/fixtures/serviceContainer");
const { setupStore } = require("devtools/client/webconsole/test/node/helpers");
const { initialize } = require("devtools/client/webconsole/actions/ui");
const {
getInitialMessageCountForViewport,
} = require("devtools/client/webconsole/utils/messages.js");
const MESSAGES_NUMBER = 100;
function getDefaultProps(initialized) {
function getDefaultProps() {
const store = setupStore(
Array.from({ length: MESSAGES_NUMBER })
// Alternate message so we don't trigger the repeat mechanism.
.map((_, i) => (i % 2 ? "console.log(null)" : "console.log(NaN)"))
);
if (initialized) {
store.dispatch(initialize());
}
return {
store,
serviceContainer,
@ -37,22 +29,18 @@ function getDefaultProps(initialized) {
}
describe("ConsoleOutput component:", () => {
it("Render only the last messages that fits the viewport when non-initialized", () => {
// We need to wrap the ConsoleApiElement in a Provider in order for the
// ObjectInspector to work.
const rendered = render(
Provider({ store: setupStore() }, ConsoleOutput(getDefaultProps(false)))
);
const messagesNumber = rendered.find(".message").length;
expect(messagesNumber).toBe(getInitialMessageCountForViewport(window));
});
it("Render every message", () => {
const Services = require("devtools/client/shared/test-helpers/jest-fixtures/Services");
Services.prefs.setBoolPref("devtools.testing", true);
it("Render every message when initialized", () => {
// We need to wrap the ConsoleApiElement in a Provider in order for the
// ObjectInspector to work.
const rendered = render(
Provider({ store: setupStore() }, ConsoleOutput(getDefaultProps(true)))
Provider({ store: setupStore() }, ConsoleOutput(getDefaultProps()))
);
expect(rendered.find(".message").length).toBe(MESSAGES_NUMBER);
Services.prefs.setBoolPref("devtools.testing", false);
const visibleMessages = JSON.parse(rendered.prop("data-visible-messages"));
expect(visibleMessages.length).toBe(MESSAGES_NUMBER);
});
});

Просмотреть файл

@ -603,11 +603,6 @@ function isGroupType(type) {
].includes(type);
}
function getInitialMessageCountForViewport(win) {
const minMessageHeight = 20;
return Math.ceil(win.innerHeight / minMessageHeight);
}
function isPacketPrivate(packet) {
return (
packet.private === true ||
@ -910,7 +905,6 @@ module.exports = {
createSimpleTableMessage,
getArrayTypeNames,
getDescriptorValue,
getInitialMessageCountForViewport,
getNaturalOrder,
getParentWarningGroupMessageId,
getWarningGroupType,

Просмотреть файл

@ -90,6 +90,10 @@ function getObjectInspector(
openLink: serviceContainer.openLink,
sourceMapURLService: serviceContainer.sourceMapURLService,
customFormat: override.customFormat !== false,
setExpanded: override.setExpanded,
initiallyExpanded: override.initiallyExpanded,
queueActorsForCleanup: override.queueActorsForCleanup,
cachedNodes: override.cachedNodes,
urlCropLimit: 120,
renderStacktrace: stacktrace =>
createElement(SmartTrace, {