blob: aa5bfae9dcfe86dc83182eb1dc77b112f018aba8 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
import {
replacePuppeteerUrl,
} from '../../shared/helper.js';
import {
CONSOLE_ALL_MESSAGES_SELECTOR,
focusConsolePrompt,
getConsoleMessages,
getCurrentConsoleMessages,
getStructuredConsoleMessages,
navigateToConsoleTab,
showVerboseMessages,
typeIntoConsoleAndWaitForResult,
waitForConsoleInfoMessageAndClickOnLink,
waitForLastConsoleMessageToHaveContent,
} from '../helpers/console-helpers.js';
import {
addLogpointForLine,
openSourceCodeEditorForFile,
} from '../helpers/sources-helpers.js';
import type {DevToolsPage} from '../shared/frontend-helper.js';
/* eslint-disable no-console */
describe('The Console Tab', () => {
const tests = [
{
description: 'produces console messages when a page logs using console.log',
evaluate: () => console.log('log'),
expectedMessages: [{
message: 'log',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):1',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
}],
},
{
description: 'produces console messages when a page logs using console.debug',
evaluate: () => console.debug('debug'),
expectedMessages: [{
message: 'debug',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):1',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-verbose-level',
}],
},
{
description: 'produces console messages when a page logs using console.warn',
evaluate: () => console.warn('warn'),
expectedMessages: [{
message: 'warn',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):1',
stackPreview: '\n(anonymous) @ (index):1',
wrapperClasses: 'console-message-wrapper console-from-api console-warning-level',
}],
},
{
description: 'produces console messages when a page logs using console.error',
evaluate: () => console.error('error'),
expectedMessages: [{
message: 'error',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):1',
stackPreview: '\n(anonymous) @ (index):1',
wrapperClasses: 'console-message-wrapper console-from-api console-error-level',
}],
},
{
description: 'produces a single console message when messages are repeated',
evaluate: () => {
for (let i = 0; i < 5; ++i) {
console.log('repeated');
}
},
expectedMessages: [{
message: 'repeated',
messageClasses: 'console-message repeated-message',
repeatCount: '5',
source: '(index):3',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
}],
},
{
description: 'counts how many time console.count has been called with the same message',
evaluate: () => {
for (let i = 0; i < 2; ++i) {
console.count('count');
}
},
expectedMessages: [
{
message: 'count: 1',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):3',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
},
{
message: 'count: 2',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):3',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
},
],
},
{
description: 'creates an empty group message using console.group/console.groupEnd',
evaluate: () => {
console.group('group');
console.groupEnd();
},
expectedMessages: [
{
message: 'group',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):2',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-group-title console-from-api console-info-level',
},
],
},
{
description: 'logs multiple arguments using console.log',
evaluate: () => {
console.log('1', '2', '3');
},
expectedMessages: [
{
message: '1 2 3',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):2',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
},
],
},
{
description: 'creates a collapsed group using console.groupCollapsed with all messages in between hidden',
evaluate: () => {
console.groupCollapsed('groupCollapsed');
console.log({property: 'value'});
console.log(42);
console.log(true);
console.log(null);
console.log(undefined);
console.log(document);
console.log(function() {});
console.log(function f() {});
console.log([1, 2, 3]);
console.log(/regexp.*/);
console.groupEnd();
},
expectedMessages: [
{
message: 'groupCollapsed',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):2',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-group-title console-from-api console-info-level',
},
],
},
{
description: 'logs console.count messages with and without arguments',
evaluate: () => {
console.count();
console.count();
console.count();
console.count('title');
console.count('title');
console.count('title');
},
expectedMessages: [
{
message: 'default: 1',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):2',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
},
{
message: 'default: 2',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):3',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
},
{
message: 'default: 3',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):4',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
},
{
message: 'title: 1',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):5',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
},
{
message: 'title: 2',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):6',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
},
{
message: 'title: 3',
messageClasses: 'console-message',
repeatCount: null,
source: '(index):7',
stackPreview: null,
wrapperClasses: 'console-message-wrapper console-from-api console-info-level',
},
],
},
];
for (const test of tests) {
it(test.description, async ({devToolsPage, inspectedPage}) => {
await navigateToConsoleTab(devToolsPage);
await showVerboseMessages(devToolsPage);
await inspectedPage.evaluate(test.evaluate);
const actualMessages = await devToolsPage.waitForFunction(async () => {
const messages = await getStructuredConsoleMessages(devToolsPage);
return messages.length === test.expectedMessages.length ? messages : undefined;
});
for (const message of actualMessages) {
if (message.source?.includes('pptr:')) {
message.source = replacePuppeteerUrl(message.source);
}
if (message.stackPreview?.includes('pptr:')) {
message.stackPreview = replacePuppeteerUrl(message.stackPreview);
}
}
assert.deepEqual(actualMessages, test.expectedMessages, 'Console message does not match the expected message');
});
}
describe('keyboard navigation', () => {
it('can navigate between individual messages', async ({devToolsPage, inspectedPage}) => {
await getConsoleMessages('focus-interaction', undefined, undefined, devToolsPage, inspectedPage);
await focusConsolePrompt(devToolsPage);
await devToolsPage.tabBackward();
assert.strictEqual(await devToolsPage.activeElementTextContent(), 'focus-interaction.html:9');
await devToolsPage.pressKey('ArrowUp');
assert.strictEqual(await devToolsPage.activeElementTextContent(), 'focus-interaction.html:9 Third message');
await devToolsPage.pressKey('ArrowUp');
assert.strictEqual(await devToolsPage.activeElementTextContent(), 'focus-interaction.html:8');
await devToolsPage.pressKey('ArrowDown');
assert.strictEqual(await devToolsPage.activeElementTextContent(), 'focus-interaction.html:9 Third message');
await devToolsPage
.tabBackward(); // Focus should now be on the console settings, e.g. out of the list of console messages
assert.strictEqual(await devToolsPage.activeElementAccessibleName(), 'Console settings');
await devToolsPage.tabForward(); // Focus is now back to the list, selecting the last message source URL
assert.strictEqual(await devToolsPage.activeElementTextContent(), 'focus-interaction.html:9');
await devToolsPage.tabForward();
assert.strictEqual(await devToolsPage.activeElementAccessibleName(), 'Console prompt');
});
it('should not lose focus on prompt when logging and scrolling', async ({inspectedPage, devToolsPage}) => {
await getConsoleMessages('focus-interaction', undefined, undefined, devToolsPage, inspectedPage);
await focusConsolePrompt(devToolsPage);
await inspectedPage.evaluate(() => {
console.log('New message');
});
await waitForLastConsoleMessageToHaveContent('New message', devToolsPage);
assert.strictEqual(await devToolsPage.activeElementAccessibleName(), 'Console prompt');
await inspectedPage.evaluate(() => {
for (let i = 0; i < 100; i++) {
console.log(`Message ${i}`);
}
});
await waitForLastConsoleMessageToHaveContent('Message 99', devToolsPage);
assert.strictEqual(await devToolsPage.activeElementAccessibleName(), 'Console prompt');
const consolePrompt = await devToolsPage.activeElement();
const wrappingBox = await consolePrompt.boundingBox();
if (!wrappingBox) {
throw new Error('Can\'t compute bounding box of console prompt.');
}
// +20 to move from the top left point so we are definitely scrolling
// within the container
await devToolsPage.page.mouse.move(wrappingBox.x + 20, wrappingBox.y + 5);
await devToolsPage.page.mouse.wheel({deltaY: -500});
assert.strictEqual(await devToolsPage.activeElementAccessibleName(), 'Console prompt');
});
});
describe('Console log message formatters', () => {
async function getConsoleMessageTextChunksWithStyle(
devToolsPage: DevToolsPage, styles: Array<keyof CSSStyleDeclaration> = []): Promise<string[][][]> {
return await devToolsPage.evaluate((selector, styles) => {
return [...document.querySelectorAll(selector)].map(message => [...message.childNodes].map(node => {
// For all nodes, extract text.
const result = [node.textContent as string];
// For element nodes, get the requested styles.
for (const style of styles) {
result.push(((node as HTMLElement).style?.[style] as string) ?? '');
}
return result;
}));
}, CONSOLE_ALL_MESSAGES_SELECTOR, styles);
}
async function waitForConsoleMessages(count: number, devToolsPage: DevToolsPage): Promise<void> {
await devToolsPage.waitForFunction(async () => {
const messages = await getCurrentConsoleMessages(false, undefined, undefined, devToolsPage);
return messages.length === count ? messages : null;
});
}
it('expand primitive formatters', async ({devToolsPage, inspectedPage}) => {
await navigateToConsoleTab(devToolsPage);
await inspectedPage.evaluate(() => {
console.log('--%s--', 'text');
console.log('--%s--', '%s%i', 'u', 2);
console.log('Number %i', 42);
console.log('Float %f', 1.5);
});
await waitForConsoleMessages(4, devToolsPage);
const texts = await getConsoleMessageTextChunksWithStyle(devToolsPage);
assert.deepEqual(texts, [[['--text--']], [['--u2--']], [['Number 42']], [['Float 1.5']]]);
});
it('expand %c formatter with color style', async ({devToolsPage, inspectedPage}) => {
await navigateToConsoleTab(devToolsPage);
await inspectedPage.evaluate(() => console.log('PRE%cRED%cBLUE', 'color:red', 'color:blue'));
await waitForConsoleMessages(1, devToolsPage);
// Extract the text and color.
const textsAndStyles = await getConsoleMessageTextChunksWithStyle(devToolsPage, ['color']);
assert.deepEqual(textsAndStyles, [[['PRE', ''], ['RED', 'red'], ['BLUE', 'blue']]]);
});
it('expand %c formatter with background image in data URL', async ({devToolsPage, inspectedPage}) => {
await navigateToConsoleTab(devToolsPage);
await inspectedPage.evaluate(
() => console.log(
'PRE%cBG',
'background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAAAAABzHgM7AAAAF0lEQVR42mM4Awb/wYCBYg6EgghRzAEAWDWBGQVyKPMAAAAASUVORK5CYII=);'));
await waitForConsoleMessages(1, devToolsPage);
// Check that the 'BG' text has the background image set.
const textsAndStyles = await getConsoleMessageTextChunksWithStyle(devToolsPage, ['backgroundImage']);
assert.lengthOf(textsAndStyles, 1);
const message = textsAndStyles[0];
assert.lengthOf(message, 2);
const textWithBackground = message[1];
assert.strictEqual(textWithBackground[0], 'BG');
assert.include(textWithBackground[1], 'data:image/png;base64');
});
it('filter out %c formatter if background image is remote URL', async ({devToolsPage, inspectedPage}) => {
await navigateToConsoleTab(devToolsPage);
await inspectedPage.evaluate(() => console.log('PRE%cBG', 'background-image: url(http://localhost/image.png)'));
await waitForConsoleMessages(1, devToolsPage);
// Check that the 'BG' text has no background image.
const textsAndStyles = await getConsoleMessageTextChunksWithStyle(devToolsPage, ['backgroundImage']);
assert.deepEqual(textsAndStyles, [[['PRE', ''], ['BG', '']]]);
});
});
describe('message anchor', () => {
it('opens the breakpoint edit dialog for logpoint messages', async ({devToolsPage, inspectedPage}) => {
await openSourceCodeEditorForFile('logpoint.js', 'logpoint.html', devToolsPage, inspectedPage);
await addLogpointForLine(3, 'x', devToolsPage);
await inspectedPage.evaluate('triggerLogpoint(42)');
await navigateToConsoleTab(devToolsPage);
await waitForConsoleInfoMessageAndClickOnLink(devToolsPage);
await devToolsPage.waitFor('.sources-edit-breakpoint-dialog');
});
});
describe('for memory objects', () => {
const MEMORY_ICON_SELECTOR = '[aria-label="Open in Memory inspector panel"]';
it('shows one memory icon to open memory inspector for ArrayBuffers (description)', async ({devToolsPage}) => {
await navigateToConsoleTab(devToolsPage);
await typeIntoConsoleAndWaitForResult('new ArrayBuffer(10)', 1, undefined, devToolsPage);
// We expect one memory icon directly next to the description.
let memoryIcons = await devToolsPage.$$(MEMORY_ICON_SELECTOR);
assert.lengthOf(memoryIcons, 1);
// Expand the object, and wait until it has completely expanded and the last property is shown.
await devToolsPage.click('.console-object');
await devToolsPage.waitForElementWithTextContent('[[ArrayBufferData]]');
// We still expect only to see one memory icon.
memoryIcons = await devToolsPage.$$(MEMORY_ICON_SELECTOR);
assert.lengthOf(memoryIcons, 1);
});
it('shows two memory icons to open memory inspector for a TypedArray (description, buffer)',
async ({devToolsPage}) => {
await navigateToConsoleTab(devToolsPage);
await typeIntoConsoleAndWaitForResult('new Uint8Array(10)', 1, undefined, devToolsPage);
// We expect one memory icon directly next to the description.
let memoryIcons = await devToolsPage.$$(MEMORY_ICON_SELECTOR);
assert.lengthOf(memoryIcons, 1);
// Expand the object, and wait until it has completely expanded and the last property is shown.
await devToolsPage.click('.console-object');
await devToolsPage.waitForElementWithTextContent('[[Prototype]]');
// We expect to see in total two memory icons: one for the buffer, one next to the description.
memoryIcons = await devToolsPage.$$(MEMORY_ICON_SELECTOR);
assert.lengthOf(memoryIcons, 2);
// Confirm that the second memory icon is next to the `buffer`.
const arrayBufferProperty = await devToolsPage.waitFor('.object-value-arraybuffer');
const arrayBufferMemoryIcon = await devToolsPage.$(MEMORY_ICON_SELECTOR, arrayBufferProperty);
assert.isOk(arrayBufferMemoryIcon);
});
it('shows two memory icons to open memory inspector for a DataView (description, buffer)',
async ({devToolsPage}) => {
await navigateToConsoleTab(devToolsPage);
await typeIntoConsoleAndWaitForResult('new DataView(new Uint8Array(10).buffer)', 1, undefined, devToolsPage);
// We expect one memory icon directly next to the description.
let memoryIcons = await devToolsPage.$$(MEMORY_ICON_SELECTOR);
assert.lengthOf(memoryIcons, 1);
// Expand the object, and wait until it has completely expanded and the last property is shown.
await devToolsPage.click('.console-object');
await devToolsPage.waitForElementWithTextContent('[[Prototype]]');
// We expect to see in total two memory icons: one for the buffer, one next to the description.
memoryIcons = await devToolsPage.$$(MEMORY_ICON_SELECTOR);
assert.lengthOf(memoryIcons, 2);
// Confirm that the second memory icon is next to the `buffer`.
const arrayBufferProperty = await devToolsPage.waitFor('.object-value-arraybuffer');
const arrayBufferMemoryIcon = await devToolsPage.$(MEMORY_ICON_SELECTOR, arrayBufferProperty);
assert.isOk(arrayBufferMemoryIcon);
});
it('shows two memory icons to open memory inspector for WebAssembly memory (description, buffer)',
async ({devToolsPage}) => {
await navigateToConsoleTab(devToolsPage);
await typeIntoConsoleAndWaitForResult('new WebAssembly.Memory({initial: 10})', 1, undefined, devToolsPage);
// We expect one memory icon directly next to the description.
let memoryIcons = await devToolsPage.$$(MEMORY_ICON_SELECTOR);
assert.lengthOf(memoryIcons, 1);
// Expand the object, and wait until it has completely expanded and the last property is shown.
await devToolsPage.click('.console-object');
await devToolsPage.waitForElementWithTextContent('[[Prototype]]');
// We expect to see in total two memory icons: one for the buffer, one next to the description.
memoryIcons = await devToolsPage.$$(MEMORY_ICON_SELECTOR);
assert.lengthOf(memoryIcons, 2);
// Confirm that the second memory icon is next to the `buffer`.
const arrayBufferProperty = await devToolsPage.waitFor('.object-value-arraybuffer');
const arrayBufferMemoryIcon = await devToolsPage.$(MEMORY_ICON_SELECTOR, arrayBufferProperty);
assert.isOk(arrayBufferMemoryIcon);
});
});
describe('Console search scroll behavior', () => {
it('should not auto-scroll to search result when console messages are updated',
async ({devToolsPage, inspectedPage}) => {
await navigateToConsoleTab(devToolsPage);
await inspectedPage.evaluate(() => {
console.log('search-term-initial');
for (let i = 0; i < 25; i++) {
console.log(`background-message-${i}`);
}
});
await devToolsPage.waitForFunction(async () => {
const messages = await getCurrentConsoleMessages(false, undefined, undefined, devToolsPage);
return messages.length >= 26;
});
await devToolsPage.pressKey('f', {control: true});
await devToolsPage.waitFor('.search-bar:not(.hidden)');
await devToolsPage.typeText('search-term-initial');
await devToolsPage.waitFor('.search-results-matches');
const initialScrollPosition = await devToolsPage.evaluate(() => {
const viewport = document.querySelector('.console-viewport');
if (viewport) {
viewport.scrollTop = 400;
return viewport.scrollTop;
}
return 0;
});
await inspectedPage.evaluate(() => {
for (let i = 0; i < 10; i++) {
console.log(`new-message-${i}-after-search`);
}
});
await devToolsPage.waitForFunction(async () => {
const messages = await getCurrentConsoleMessages(false, undefined, undefined, devToolsPage);
return messages.length >= 36;
});
const finalScrollPosition = await devToolsPage.evaluate(() => {
const viewport = document.querySelector('.console-viewport');
return viewport ? viewport.scrollTop : 0;
});
const scrollDifference = Math.abs(finalScrollPosition - initialScrollPosition);
assert.isBelow(
scrollDifference, 50,
`Scroll position should be maintained when new messages arrive. Difference: ${scrollDifference}px`);
});
});
});