Bug 1326182 - Send all the console.table items in the console message. r=Honza.

This allows us to not have to reach for the server
again from the client to get all the data we need
to render the table.

We still need to keep the old function to handle
connections to older servers. We'll use Bug 1597955
in Firefox 74 to clean the bits we don't need anymore.

A test is added to ensure the bug is fixed and we don't
regress; stubs are updated.

Differential Revision: https://phabricator.services.mozilla.com/D53962

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Nicolas Chevobbe 2019-11-22 09:48:58 +00:00
Родитель b58a9d6894
Коммит ed982614a2
8 изменённых файлов: 363 добавлений и 41 удалений

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

@ -12,6 +12,7 @@ const actions = require("devtools/client/webconsole/actions/messages");
const { const {
l10n, l10n,
getArrayTypeNames, getArrayTypeNames,
getDescriptorValue,
} = require("devtools/client/webconsole/utils/messages"); } = require("devtools/client/webconsole/utils/messages");
loader.lazyGetter(this, "MODE", function() { loader.lazyGetter(this, "MODE", function() {
return require("devtools/client/shared/components/reps/reps").MODE; return require("devtools/client/shared/components/reps/reps").MODE;
@ -37,6 +38,7 @@ class ConsoleTable extends Component {
parameters: PropTypes.array.isRequired, parameters: PropTypes.array.isRequired,
serviceContainer: PropTypes.object.isRequired, serviceContainer: PropTypes.object.isRequired,
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
// Remove in Firefox 74. See Bug 1597955.
tableData: PropTypes.object, tableData: PropTypes.object,
}; };
} }
@ -47,10 +49,13 @@ class ConsoleTable extends Component {
this.getRows = this.getRows.bind(this); this.getRows = this.getRows.bind(this);
} }
// Remove in Firefox 74. See Bug 1597955.
componentWillMount() { componentWillMount() {
const { id, dispatch, parameters, tableData } = this.props; const { id, dispatch, parameters, tableData } = this.props;
if (!Array.isArray(parameters) || parameters.length === 0 || tableData) { const data = tableData || (parameters[0] && parameters[0].ownProperties);
if (!Array.isArray(parameters) || parameters.length === 0 || data) {
return; return;
} }
@ -110,20 +115,27 @@ class ConsoleTable extends Component {
render() { render() {
const { parameters, tableData } = this.props; const { parameters, tableData } = this.props;
const headersGrip = parameters[1]; const [valueGrip, headersGrip] = parameters;
const headers = const headers =
headersGrip && headersGrip.preview ? headersGrip.preview.items : null; headersGrip && headersGrip.preview ? headersGrip.preview.items : null;
// if tableData is nullable, we don't show anything. const data =
if (!tableData) { valueGrip && valueGrip.ownProperties
? valueGrip.ownProperties
: tableData;
// if we don't have any data, don't show anything.
if (!data) {
return null; return null;
} }
const { columns, items } = getTableItems( const dataType = getParametersDataType(parameters);
tableData,
getParametersDataType(parameters), // Remove deprecatedGetTableItems in Firefox 74. See Bug 1597955.
headers const { columns, items } =
); valueGrip && valueGrip.ownProperties
? getTableItems(data, dataType, headers)
: deprecatedGetTableItems(data, dataType, headers);
return dom.div( return dom.div(
{ {
@ -146,10 +158,11 @@ function getParametersDataType(parameters = null) {
return parameters[0].class; return parameters[0].class;
} }
function getTableItems(data = {}, type, headers = null) { const INDEX_NAME = "_index";
const INDEX_NAME = "_index"; const VALUE_NAME = "_value";
const VALUE_NAME = "_value";
const namedIndexes = { function getNamedIndexes(type) {
return {
[INDEX_NAME]: getArrayTypeNames() [INDEX_NAME]: getArrayTypeNames()
.concat("Object") .concat("Object")
.includes(type) .includes(type)
@ -158,6 +171,19 @@ function getTableItems(data = {}, type, headers = null) {
[VALUE_NAME]: l10n.getStr("table.value"), [VALUE_NAME]: l10n.getStr("table.value"),
key: l10n.getStr("table.key"), key: l10n.getStr("table.key"),
}; };
}
function hasValidCustomHeaders(headers) {
return (
Array.isArray(headers) &&
headers.every(
header => typeof header === "string" || Number.isInteger(Number(header))
)
);
}
function getTableItems(data = {}, type, headers = null) {
const namedIndexes = getNamedIndexes(type);
let columns = new Map(); let columns = new Map();
const items = []; const items = [];
@ -167,11 +193,7 @@ function getTableItems(data = {}, type, headers = null) {
Object.keys(item).forEach(key => addColumn(key)); Object.keys(item).forEach(key => addColumn(key));
}; };
const hasValidCustomHeaders = const validCustomHeaders = hasValidCustomHeaders(headers);
Array.isArray(headers) &&
headers.every(
header => typeof header === "string" || Number.isInteger(Number(header))
);
const addColumn = function(columnIndex) { const addColumn = function(columnIndex) {
const columnExists = columns.has(columnIndex); const columnExists = columns.has(columnIndex);
@ -180,7 +202,99 @@ function getTableItems(data = {}, type, headers = null) {
if ( if (
!columnExists && !columnExists &&
!hasMaxColumns && !hasMaxColumns &&
(!hasValidCustomHeaders || (!validCustomHeaders ||
headers.includes(columnIndex) ||
columnIndex === INDEX_NAME)
) {
columns.set(columnIndex, namedIndexes[columnIndex] || columnIndex);
}
};
for (let [index, property] of Object.entries(data)) {
if (type !== "Object" && index == parseInt(index, 10)) {
index = parseInt(index, 10);
}
const item = {
[INDEX_NAME]: index,
};
const propertyValue = getDescriptorValue(property);
if (propertyValue && propertyValue.ownProperties) {
const entries = propertyValue.ownProperties;
for (const [key, entry] of Object.entries(entries)) {
item[key] = getDescriptorValue(entry);
}
} else if (
propertyValue &&
propertyValue.preview &&
(type === "Map" || type === "WeakMap")
) {
item.key = propertyValue.preview.key;
item[VALUE_NAME] = propertyValue.preview.value;
} else {
item[VALUE_NAME] = propertyValue;
}
addItem(item);
if (items.length === TABLE_ROW_MAX_ITEMS) {
break;
}
}
// Some headers might not be present in the items, so we make sure to
// return all the headers set by the user.
if (validCustomHeaders) {
headers.forEach(header => addColumn(header));
}
// We want to always have the index column first
if (columns.has(INDEX_NAME)) {
const index = columns.get(INDEX_NAME);
columns.delete(INDEX_NAME);
columns = new Map([[INDEX_NAME, index], ...columns.entries()]);
}
// We want to always have the values column last
if (columns.has(VALUE_NAME)) {
const index = columns.get(VALUE_NAME);
columns.delete(VALUE_NAME);
columns.set(VALUE_NAME, index);
}
return {
columns,
items,
};
}
/**
* This function is handling console.table packets from pre-Firefox72 servers.
* Remove in Firefox 74. See Bug 1597955.
*/
function deprecatedGetTableItems(data = {}, type, headers = null) {
const namedIndexes = getNamedIndexes(type);
let columns = new Map();
const items = [];
const addItem = function(item) {
items.push(item);
Object.keys(item).forEach(key => addColumn(key));
};
const validCustomHeaders = hasValidCustomHeaders(headers);
const addColumn = function(columnIndex) {
const columnExists = columns.has(columnIndex);
const hasMaxColumns = columns.size == TABLE_COLUMN_MAX_ITEMS;
if (
!columnExists &&
!hasMaxColumns &&
(!validCustomHeaders ||
headers.includes(columnIndex) || headers.includes(columnIndex) ||
columnIndex === INDEX_NAME) columnIndex === INDEX_NAME)
) { ) {
@ -234,7 +348,7 @@ function getTableItems(data = {}, type, headers = null) {
// Some headers might not be present in the items, so we make sure to // Some headers might not be present in the items, so we make sure to
// return all the headers set by the user. // return all the headers set by the user.
if (hasValidCustomHeaders) { if (validCustomHeaders) {
headers.forEach(header => addColumn(header)); headers.forEach(header => addColumn(header));
} }

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

@ -47,6 +47,12 @@ loader.lazyRequireGetter(
"devtools/client/webconsole/utils/messages", "devtools/client/webconsole/utils/messages",
true true
); );
loader.lazyRequireGetter(
this,
"getDescriptorValue",
"devtools/client/webconsole/utils/messages",
true
);
loader.lazyRequireGetter( loader.lazyRequireGetter(
this, this,
"getParentWarningGroupMessageId", "getParentWarningGroupMessageId",
@ -1364,6 +1370,18 @@ function isTextInParameter(text, regex, parameter) {
} }
} }
if (parameter && parameter.ownProperties) {
for (const [key, desc] of Object.entries(parameter.ownProperties)) {
if (matchStr(key)) {
return true;
}
if (isTextInParameter(text, regex, getDescriptorValue(desc))) {
return true;
}
}
}
return false; return false;
} }

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

@ -326,6 +326,7 @@ skip-if = true # Bug XXX # Bug 1405250
[browser_webconsole_console_group.js] [browser_webconsole_console_group.js]
[browser_webconsole_console_logging_workers_api.js] [browser_webconsole_console_logging_workers_api.js]
skip-if = e10s # SharedWorkers console events are not received on the current process because they could run on any process. skip-if = e10s # SharedWorkers console events are not received on the current process because they could run on any process.
[browser_webconsole_console_table_post_alterations.js]
[browser_webconsole_console_table.js] [browser_webconsole_console_table.js]
[browser_webconsole_console_timeStamp.js] [browser_webconsole_console_timeStamp.js]
[browser_webconsole_console_trace_distinct.js] [browser_webconsole_console_trace_distinct.js]

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

@ -258,7 +258,7 @@ add_task(async function() {
input: [{ a: { b: 34 } }], input: [{ a: { b: 34 } }],
expected: { expected: {
columns: ["(index)", "a"], columns: ["(index)", "a"],
rows: [["0", "Object { }"]], rows: [["0", "Object { b: 34 }"]],
}, },
additionalTest: async function(node) { additionalTest: async function(node) {
info("Check that object in a cell can be expanded"); info("Check that object in a cell can be expanded");
@ -312,7 +312,7 @@ async function testItem(testCase, node) {
is( is(
JSON.stringify(testCase.expected.columns), JSON.stringify(testCase.expected.columns),
JSON.stringify(columns.map(column => column.textContent)), JSON.stringify(columns.map(column => column.textContent)),
"table has the expected columns" `${testCase.info} | table has the expected columns`
); );
// We don't really have rows since we are using a CSS grid in order to have a sticky // We don't really have rows since we are using a CSS grid in order to have a sticky
@ -321,14 +321,18 @@ async function testItem(testCase, node) {
is( is(
testCase.expected.rows.length, testCase.expected.rows.length,
cells.length / columnsNumber, cells.length / columnsNumber,
"table has the expected number of rows" `${testCase.info} | table has the expected number of rows`
); );
testCase.expected.rows.forEach((expectedRow, rowIndex) => { testCase.expected.rows.forEach((expectedRow, rowIndex) => {
const startIndex = rowIndex * columnsNumber; const startIndex = rowIndex * columnsNumber;
// Slicing the cells array so we can get the current "row". // Slicing the cells array so we can get the current "row".
const rowCells = cells.slice(startIndex, startIndex + columnsNumber); const rowCells = cells.slice(startIndex, startIndex + columnsNumber);
is(rowCells.map(x => x.textContent).join(" | "), expectedRow.join(" | ")); is(
rowCells.map(x => x.textContent).join(" | "),
expectedRow.join(" | "),
`${testCase.info} | row has the expected content`
);
}); });
if (testCase.expected.overflow) { if (testCase.expected.overflow) {

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

@ -0,0 +1,63 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Check that calling console.table on a variable which is modified after the
// console.table call only shows data for when the variable was logged.
const TEST_URI = `data:text/html,Test console.table with modified variable`;
add_task(async function() {
const hud = await openNewTabAndConsole(TEST_URI);
await ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
const x = ["a", "b"];
content.wrappedJSObject.console.table(x);
x.push("c");
content.wrappedJSObject.console.table(x);
x.sort((a, b) => b - a);
content.wrappedJSObject.console.table(x);
});
const [table1, table2, table3] = await waitFor(() => {
const res = hud.ui.outputNode.querySelectorAll(
".message .new-consoletable"
);
if (res.length === 3) {
return res;
}
return null;
});
info("Check the rows of the first table");
checkTable(table1, [[0, "a"], [1, "b"]]);
info("Check the rows of the table after adding an element to the array");
checkTable(table2, [[0, "a"], [1, "b"], [2, "c"]]);
info("Check the rows of the table after sorting the array");
checkTable(table3, [[0, "c"], [1, "b"], [2, "a"]]);
});
function checkTable(node, expectedRows) {
const columns = Array.from(node.querySelectorAll("[role=columnheader]"));
const columnsNumber = columns.length;
const cells = Array.from(node.querySelectorAll("[role=gridcell]"));
// We don't really have rows since we are using a CSS grid in order to have a sticky
// header on the table. So we check the "rows" by dividing the number of cells by the
// number of columns.
is(
cells.length / columnsNumber,
expectedRows.length,
"table has the expected number of rows"
);
expectedRows.forEach((expectedRow, rowIndex) => {
const startIndex = rowIndex * columnsNumber;
// Slicing the cells array so we can get the current "row".
const rowCells = cells.slice(startIndex, startIndex + columnsNumber);
is(rowCells.map(x => x.textContent).join(" | "), expectedRow.join(" | "));
});
}

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

@ -458,14 +458,26 @@ stubPackets.set(`console.table(['red', 'green', 'blue']);`, {
"extensible": true, "extensible": true,
"frozen": false, "frozen": false,
"sealed": false, "sealed": false,
"preview": { "preview": null,
"kind": "ArrayLike", "ownProperties": {
"length": 3, "0": {
"items": [ "configurable": true,
"red", "enumerable": true,
"green", "writable": true,
"blue" "value": "red"
] },
"1": {
"configurable": true,
"enumerable": true,
"writable": true,
"value": "green"
},
"2": {
"configurable": true,
"enumerable": true,
"writable": true,
"value": "blue"
}
} }
} }
], ],
@ -1150,14 +1162,26 @@ stubPackets.set(`console.table(['a', 'b', 'c'])`, {
"extensible": true, "extensible": true,
"frozen": false, "frozen": false,
"sealed": false, "sealed": false,
"preview": { "preview": null,
"kind": "ArrayLike", "ownProperties": {
"length": 3, "0": {
"items": [ "configurable": true,
"a", "enumerable": true,
"b", "writable": true,
"c" "value": "a"
] },
"1": {
"configurable": true,
"enumerable": true,
"writable": true,
"value": "b"
},
"2": {
"configurable": true,
"enumerable": true,
"writable": true,
"value": "c"
}
} }
} }
], ],

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

@ -689,9 +689,25 @@ function getArrayTypeNames() {
]; ];
} }
function getDescriptorValue(descriptor) {
if (!descriptor) {
return descriptor;
}
if (Object.prototype.hasOwnProperty.call(descriptor, "safeGetterValues")) {
return descriptor.safeGetterValues;
}
if (Object.prototype.hasOwnProperty.call(descriptor, "value")) {
return descriptor.value;
}
return descriptor;
}
module.exports = { module.exports = {
createWarningGroupMessage, createWarningGroupMessage,
getArrayTypeNames, getArrayTypeNames,
getDescriptorValue,
getInitialMessageCountForViewport, getInitialMessageCountForViewport,
getParentWarningGroupMessageId, getParentWarningGroupMessageId,
getWarningGroupType, getWarningGroupType,

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

@ -17,6 +17,7 @@ const { ObjectActor } = require("devtools/server/actors/object");
const { LongStringActor } = require("devtools/server/actors/string"); const { LongStringActor } = require("devtools/server/actors/string");
const { const {
createValueGrip, createValueGrip,
isArray,
stringIsLong, stringIsLong,
} = require("devtools/server/actors/object/utils"); } = require("devtools/server/actors/object/utils");
const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const DevToolsUtils = require("devtools/shared/DevToolsUtils");
@ -2020,7 +2021,6 @@ const WebConsoleActor = ActorClassWithSpec(webconsoleSpec, {
const result = WebConsoleUtils.cloneObject(message); const result = WebConsoleUtils.cloneObject(message);
result.workerType = WebConsoleUtils.getWorkerType(result) || "none"; result.workerType = WebConsoleUtils.getWorkerType(result) || "none";
result.sourceId = this.getActorIdForInternalSourceId(result.sourceId); result.sourceId = this.getActorIdForInternalSourceId(result.sourceId);
delete result.wrappedJSObject; delete result.wrappedJSObject;
@ -2046,12 +2046,94 @@ const WebConsoleActor = ActorClassWithSpec(webconsoleSpec, {
return this.createValueGrip(string); return this.createValueGrip(string);
}); });
if (result.level === "table") {
const tableItems = this._getConsoleTableMessageItems(result);
if (tableItems) {
result.arguments[0].ownProperties = tableItems;
result.arguments[0].preview = null;
}
// Only return the 2 first params.
result.arguments = result.arguments.slice(0, 2);
}
result.category = message.category || "webdev"; result.category = message.category || "webdev";
result.innerWindowID = message.innerID; result.innerWindowID = message.innerID;
return result; return result;
}, },
/**
* Return the properties needed to display the appropriate table for a given
* console.table call.
* This function does a little more than creating an ObjectActor for the first
* parameter of the message. When layout out the console table in the output, we want
* to be able to look into sub-properties so the table can have a different layout (
* for arrays of arrays, objects with objects properties, arrays of objects, ).
* So here we need to retrieve the properties of the first parameter, and also all the
* sub-properties we might need.
*
* @param {Object} result: The console.table message.
* @returns {Object} An object containing the properties of the first argument of the
* console.table call.
*/
_getConsoleTableMessageItems: function(result) {
if (
!result ||
!Array.isArray(result.arguments) ||
result.arguments.length == 0
) {
return null;
}
const [tableItemGrip] = result.arguments;
const dataType = tableItemGrip.class;
const needEntries = ["Map", "WeakMap", "Set", "WeakSet"].includes(dataType);
const ignoreNonIndexedProperties = isArray(tableItemGrip);
const tableItemActor = this.getActorByID(tableItemGrip.actor);
if (!tableItemActor) {
return null;
}
// Retrieve the properties (or entries for Set/Map) of the console table first arg.
const iterator = needEntries
? tableItemActor.enumEntries()
: tableItemActor.enumProperties({
ignoreNonIndexedProperties,
});
const { ownProperties } = iterator.all();
// The iterator returns a descriptor for each property, wherein the value could be
// in one of those sub-property.
const descriptorKeys = ["safeGetterValues", "getterValue", "value"];
Object.values(ownProperties).forEach(desc => {
if (typeof desc !== "undefined") {
descriptorKeys.forEach(key => {
if (desc && desc.hasOwnProperty(key)) {
const grip = desc[key];
// We need to load sub-properties as well to render the table in a nice way.
const actor = grip && this.getActorByID(grip.actor);
if (actor) {
const res = actor
.enumProperties({
ignoreNonIndexedProperties: isArray(grip),
})
.all();
if (res && res.ownProperties) {
desc[key].ownProperties = res.ownProperties;
}
}
}
});
}
});
return ownProperties;
},
/** /**
* Find the XUL window that owns the content window. * Find the XUL window that owns the content window.
* *