/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Check console.table calls with all the test cases shown // in the MDN doc (https://developer.mozilla.org/en-US/docs/Web/API/Console/table) const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" + "test/browser/test-console-table.html"; add_task(async function () { const hud = await openNewTabAndConsole(TEST_URI); function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } const holeyArray = []; holeyArray[1] = "apples"; holeyArray[3] = "oranges"; holeyArray[6] = "bananas"; const testCases = [ { info: "Testing when data argument is an array", input: ["apples", "oranges", "bananas"], expected: { columns: ["(index)", "Values"], rows: [ ["0", "apples"], ["1", "oranges"], ["2", "bananas"], ], }, }, { info: "Testing when data argument is an holey array", input: holeyArray, expected: { columns: ["(index)", "Values"], rows: [ ["0", ""], ["1", "apples"], ["2", ""], ["3", "oranges"], ["4", ""], ["5", ""], ["6", "bananas"], ], }, }, { info: "Testing when data argument has holey array", // eslint-disable-next-line no-sparse-arrays input: [[1, , 2]], expected: { columns: ["(index)", "0", "1", "2"], rows: [["0", "1", "", "2"]], }, }, { info: "Testing when data argument is an object", input: new Person("John", "Smith"), expected: { columns: ["(index)", "Values"], rows: [ ["firstName", "John"], ["lastName", "Smith"], ], }, }, { info: "Testing when data argument is an array of arrays", input: [ ["Jane", "Doe"], ["Emily", "Jones"], ], expected: { columns: ["(index)", "0", "1"], rows: [ ["0", "Jane", "Doe"], ["1", "Emily", "Jones"], ], }, }, { info: "Testing when data argument is an array of objects", input: [ new Person("Jack", "Foo"), new Person("Emma", "Bar"), new Person("Michelle", "Rax"), ], expected: { columns: ["(index)", "firstName", "lastName"], rows: [ ["0", "Jack", "Foo"], ["1", "Emma", "Bar"], ["2", "Michelle", "Rax"], ], }, }, { info: "Testing when data argument is an object whose properties are objects", input: { father: new Person("Darth", "Vader"), daughter: new Person("Leia", "Organa"), son: new Person("Luke", "Skywalker"), }, expected: { columns: ["(index)", "firstName", "lastName"], rows: [ ["father", "Darth", "Vader"], ["daughter", "Leia", "Organa"], ["son", "Luke", "Skywalker"], ], }, }, { info: "Testing when data argument is a Set", input: new Set(["a", "b", "c"]), expected: { columns: ["(iteration index)", "Values"], rows: [ ["0", "a"], ["1", "b"], ["2", "c"], ], }, }, { info: "Testing when data argument is a Map", input: new Map([ ["key-a", "value-a"], ["key-b", "value-b"], ]), expected: { columns: ["(iteration index)", "Key", "Values"], rows: [ ["0", "key-a", "value-a"], ["1", "key-b", "value-b"], ], }, }, { info: "Testing when data argument is a Int8Array", input: new Int8Array([1, 2, 3, 4]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1"], ["1", "2"], ["2", "3"], ["3", "4"], ], }, }, { info: "Testing when data argument is a Uint8Array", input: new Uint8Array([1, 2, 3, 4]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1"], ["1", "2"], ["2", "3"], ["3", "4"], ], }, }, { info: "Testing when data argument is a Int16Array", input: new Int16Array([1, 2, 3, 4]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1"], ["1", "2"], ["2", "3"], ["3", "4"], ], }, }, { info: "Testing when data argument is a Uint16Array", input: new Uint16Array([1, 2, 3, 4]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1"], ["1", "2"], ["2", "3"], ["3", "4"], ], }, }, { info: "Testing when data argument is a Int32Array", input: new Int32Array([1, 2, 3, 4]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1"], ["1", "2"], ["2", "3"], ["3", "4"], ], }, }, { info: "Testing when data argument is a Uint32Array", input: new Uint32Array([1, 2, 3, 4]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1"], ["1", "2"], ["2", "3"], ["3", "4"], ], }, }, { info: "Testing when data argument is a Float32Array", input: new Float32Array([1, 2, 3, 4]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1"], ["1", "2"], ["2", "3"], ["3", "4"], ], }, }, { info: "Testing when data argument is a Float64Array", input: new Float64Array([1, 2, 3, 4]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1"], ["1", "2"], ["2", "3"], ["3", "4"], ], }, }, { info: "Testing when data argument is a Uint8ClampedArray", input: new Uint8ClampedArray([1, 2, 3, 4]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1"], ["1", "2"], ["2", "3"], ["3", "4"], ], }, }, { info: "Testing when data argument is a BigInt64Array", // eslint-disable-next-line no-undef input: new BigInt64Array([1n, 2n, 3n, 4n]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1n"], ["1", "2n"], ["2", "3n"], ["3", "4n"], ], }, }, { info: "Testing when data argument is a BigUint64Array", // eslint-disable-next-line no-undef input: new BigUint64Array([1n, 2n, 3n, 4n]), expected: { columns: ["(index)", "Values"], rows: [ ["0", "1n"], ["1", "2n"], ["2", "3n"], ["3", "4n"], ], }, }, { info: "Testing restricting the columns displayed", input: [new Person("Sam", "Wright"), new Person("Elena", "Bartz")], headers: ["firstName"], expected: { columns: ["(index)", "firstName"], rows: [ ["0", "Sam"], ["1", "Elena"], ], }, }, { info: "Testing nested object with falsy values", input: [ { a: null, b: false, c: undefined, d: 0 }, { b: null, c: false, d: undefined, e: 0 }, ], expected: { columns: ["(index)", "a", "b", "c", "d", "e"], rows: [ ["0", "null", "false", "undefined", "0", ""], ["1", "", "null", "false", "undefined", "0"], ], }, }, { info: "Testing invalid headers", input: ["apples", "oranges", "bananas"], headers: [[]], expected: { columns: ["(index)", "Values"], rows: [ ["0", "apples"], ["1", "oranges"], ["2", "bananas"], ], }, }, { info: "Testing overflow-y", input: Array.from({ length: 50 }, (_, i) => `item-${i}`), expected: { columns: ["(index)", "Values"], rows: Array.from({ length: 50 }, (_, i) => [i.toString(), `item-${i}`]), overflow: true, }, }, { info: "Testing table with expandable objects", input: [{ a: { b: 34 } }], expected: { columns: ["(index)", "a"], rows: [["0", "Object { b: 34 }"]], }, async additionalTest(node) { info("Check that object in a cell can be expanded"); const objectNode = node.querySelector(".tree .node"); objectNode.click(); await waitFor(() => node.querySelectorAll(".tree .node").length === 3); const nodes = node.querySelectorAll(".tree .node"); ok(nodes[1].textContent.includes("b: 34")); ok(nodes[2].textContent.includes("")); }, }, { info: "Testing max columns", input: [ Array.from({ length: 30 }).reduce((acc, _, i) => { return { ...acc, ["item" + i]: i, }; }, {}), ], expected: { // We show 21 columns at most columns: [ "(index)", ...Array.from({ length: 20 }, (_, i) => `item${i}`), ], rows: [[0, ...Array.from({ length: 20 }, (_, i) => i)]], }, }, { info: "Testing performance entries", input: "PERFORMANCE_ENTRIES", headers: [ "name", "entryType", "initiatorType", "connectStart", "connectEnd", "fetchStart", ], expected: { columns: [ "(index)", "initiatorType", "fetchStart", "connectStart", "connectEnd", "name", "entryType", ], rows: [[0, "navigation", /\d+/, /\d+/, /\d+/, TEST_URI, "navigation"]], }, }, ]; await SpecialPowers.spawn( gBrowser.selectedBrowser, [testCases.map(({ input, headers }) => ({ input, headers }))], function (tests) { tests.forEach(test => { let { input, headers } = test; if (input === "PERFORMANCE_ENTRIES") { input = content.wrappedJSObject.performance.getEntriesByType("navigation"); } content.wrappedJSObject.doConsoleTable(input, headers); }); } ); 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()) { // Refresh the reference to the message, as it may have been scrolled out of existence. const node = await findMessageVirtualizedById({ hud, messageId: messages[index].getAttribute("data-message-id"), }); await testItem(testCase, node.querySelector(".consoletable")); } }); async function testItem(testCase, tableNode) { info(testCase.info); const ths = Array.from(tableNode.querySelectorAll("th")); const trs = Array.from(tableNode.querySelectorAll("tbody tr")); is( JSON.stringify(ths.map(column => column.textContent)), JSON.stringify(testCase.expected.columns), `${testCase.info} | table has the expected columns` ); is( trs.length, testCase.expected.rows.length, `${testCase.info} | table has the expected number of rows` ); testCase.expected.rows.forEach((expectedRow, rowIndex) => { const rowCells = Array.from(trs[rowIndex].querySelectorAll("td")).map( x => x.textContent ); const isRegex = x => x && x.constructor.name === "RegExp"; const hasRegExp = expectedRow.find(isRegex); if (hasRegExp) { is( rowCells.length, expectedRow.length, `${testCase.info} | row ${rowIndex} has the expected number of cell` ); rowCells.forEach((cell, i) => { const expected = expectedRow[i]; const info = `${testCase.info} | row ${rowIndex} cell ${i} has the expected content`; if (isRegex(expected)) { ok(expected.test(cell), info); } else { is(cell, `${expected}`, info); } }); } else { is( rowCells.join(" | "), expectedRow.join(" | "), `${testCase.info} | row has the expected content` ); } }); if (testCase.expected.overflow) { ok( tableNode.isConnected, "Node must be connected to test overflow. It is likely scrolled out of view." ); const tableWrapperNode = tableNode.closest(".consoletable-wrapper"); Assert.greater( tableWrapperNode.scrollHeight, tableWrapperNode.clientHeight, testCase.info + " table overflows" ); Assert.notStrictEqual( getComputedStyle(tableWrapperNode).overflowY, "hidden", "table can be scrolled" ); } if (typeof testCase.additionalTest === "function") { await testCase.additionalTest(tableNode); } }