[go: nahoru, domu]

blob: 88f78cf4d8ca090ed47dc15ffd33432a5e03b4db [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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 type * as puppeteer from 'puppeteer';
import {
$,
$$,
assertNotNullOrUndefined,
click,
getBrowserAndPages,
goToResource,
pasteText,
timeout,
waitFor,
waitForAria,
waitForFunction,
} from '../../shared/helper.js';
import {AsyncScope} from '../../shared/async-scope.js';
export const CONSOLE_TAB_SELECTOR = '#tab-console';
export const CONSOLE_MESSAGES_SELECTOR = '.console-group-messages';
export const CONSOLE_MESSAGES_TEXT_SELECTOR = '.source-code .console-message-text';
export const CONSOLE_ALL_MESSAGES_SELECTOR = `${CONSOLE_MESSAGES_SELECTOR} ${CONSOLE_MESSAGES_TEXT_SELECTOR}`;
export const CONSOLE_INFO_MESSAGES_SELECTOR =
`${CONSOLE_MESSAGES_SELECTOR} .console-info-level ${CONSOLE_MESSAGES_TEXT_SELECTOR}`;
export const CONSOLE_ERROR_MESSAGES_SELECTOR =
`${CONSOLE_MESSAGES_SELECTOR} .console-error-level ${CONSOLE_MESSAGES_TEXT_SELECTOR}`;
export const CONSOLE_MESSAGE_TEXT_AND_ANCHOR_SELECTOR = '.console-group-messages .source-code';
export const LOG_LEVELS_SELECTOR = '[aria-label^="Log level: "]';
export const LOG_LEVELS_VERBOSE_OPTION_SELECTOR = '[aria-label^="Verbose"]';
export const CONSOLE_PROMPT_SELECTOR = '.console-prompt-editor-container';
export const CONSOLE_VIEW_SELECTOR = '.console-view';
export const CONSOLE_TOOLTIP_SELECTOR = '.cm-tooltip';
export const CONSOLE_COMPLETION_HINT_SELECTOR = '.cm-completionHint';
export const STACK_PREVIEW_CONTAINER = '.stack-preview-container';
export const CONSOLE_MESSAGE_WRAPPER_SELECTOR = '.console-group-messages .console-message-wrapper';
export const CONSOLE_SELECTOR = '.console-user-command-result';
export const CONSOLE_SETTINGS_SELECTOR = '[aria-label^="Console settings"]';
export const AUTOCOMPLETE_FROM_HISTORY_SELECTOR = '[title="Autocomplete from history"]';
export const SHOW_CORS_ERRORS_SELECTOR = '[title="Show CORS errors in console"]';
export const LOG_XML_HTTP_REQUESTS_SELECTOR = '[title="Log XMLHttpRequests"]';
export const CONSOLE_CREATE_LIVE_EXPRESSION_SELECTOR = '[aria-label^="Create live expression"]';
export const Level = {
All: CONSOLE_ALL_MESSAGES_SELECTOR,
Info: CONSOLE_INFO_MESSAGES_SELECTOR,
Error: CONSOLE_ERROR_MESSAGES_SELECTOR,
};
export async function deleteConsoleMessagesFilter(frontend: puppeteer.Page) {
await waitFor('.console-main-toolbar');
const main = await $('.console-main-toolbar');
await frontend.evaluate(n => {
const deleteButton = n.shadowRoot?.querySelector('.search-cancel-button') as HTMLElement;
if (deleteButton) {
deleteButton.click();
}
}, main);
}
export async function filterConsoleMessages(frontend: puppeteer.Page, filter: string) {
await waitFor('.console-main-toolbar');
const main = await $('.console-main-toolbar');
await frontend.evaluate(n => {
const toolbar = n.shadowRoot?.querySelector('.toolbar-input-prompt.text-prompt') as HTMLElement;
toolbar.focus();
}, main);
await pasteText(filter);
await frontend.keyboard.press('Enter');
}
export async function waitForConsoleMessagesToBeNonEmpty(numberOfMessages: number) {
await waitForFunction(async () => {
const messages = await $$(CONSOLE_ALL_MESSAGES_SELECTOR);
if (messages.length < numberOfMessages) {
return false;
}
const textContents =
await Promise.all(messages.map(message => message.evaluate(message => message.textContent || '')));
return textContents.every(text => text !== '');
});
}
export async function waitForLastConsoleMessageToHaveContent(expectedTextContent: string) {
await waitForFunction(async () => {
const messages = await $$(CONSOLE_ALL_MESSAGES_SELECTOR);
if (messages.length === 0) {
return false;
}
const lastMessageContent = await messages[messages.length - 1].evaluate(message => message.textContent);
return lastMessageContent === expectedTextContent;
});
}
export async function getConsoleMessages(testName: string, withAnchor = false, callback?: () => Promise<void>) {
// Ensure Console is loaded before the page is loaded to avoid a race condition.
await navigateToConsoleTab();
// Have the target load the page.
await goToResource(`console/${testName}.html`);
return getCurrentConsoleMessages(withAnchor, Level.All, callback);
}
export async function getCurrentConsoleMessages(withAnchor = false, level = Level.All, callback?: () => Promise<void>) {
const {frontend} = getBrowserAndPages();
const asyncScope = new AsyncScope();
await navigateToConsoleTab();
// Get console messages that were logged.
await waitFor(CONSOLE_MESSAGES_SELECTOR, undefined, asyncScope);
if (callback) {
await callback();
}
// Ensure all messages are populated.
await asyncScope.exec(() => frontend.waitForFunction((CONSOLE_FIRST_MESSAGES_SELECTOR: string) => {
const messages = document.querySelectorAll(CONSOLE_FIRST_MESSAGES_SELECTOR);
if (messages.length === 0) {
return false;
}
return Array.from(messages).every(message => message.childNodes.length > 0);
}, {timeout: 0, polling: 'mutation'}, CONSOLE_ALL_MESSAGES_SELECTOR));
const selector = withAnchor ? CONSOLE_MESSAGE_TEXT_AND_ANCHOR_SELECTOR : level;
// FIXME(crbug/1112692): Refactor test to remove the timeout.
await timeout(100);
// Get the messages from the console.
return frontend.evaluate(selector => {
return Array.from(document.querySelectorAll(selector)).map(message => message.textContent as string);
}, selector);
}
export async function getLastConsoleMessages(offset: number = 0) {
return (await getCurrentConsoleMessages()).at(-1 - offset);
}
export async function maybeGetCurrentConsoleMessages(withAnchor = false, callback?: () => Promise<void>) {
const {frontend} = getBrowserAndPages();
const asyncScope = new AsyncScope();
await navigateToConsoleTab();
// Get console messages that were logged.
await waitFor(CONSOLE_MESSAGES_SELECTOR, undefined, asyncScope);
if (callback) {
await callback();
}
const selector = withAnchor ? CONSOLE_MESSAGE_TEXT_AND_ANCHOR_SELECTOR : CONSOLE_ALL_MESSAGES_SELECTOR;
// FIXME(crbug/1112692): Refactor test to remove the timeout.
await timeout(100);
// Get the messages from the console.
return frontend.evaluate(selector => {
return Array.from(document.querySelectorAll(selector)).map(message => message.textContent);
}, selector);
}
export async function getStructuredConsoleMessages() {
const {frontend} = getBrowserAndPages();
const asyncScope = new AsyncScope();
await navigateToConsoleTab();
// Get console messages that were logged.
await waitFor(CONSOLE_MESSAGES_SELECTOR, undefined, asyncScope);
// Ensure all messages are populated.
await asyncScope.exec(() => frontend.waitForFunction((CONSOLE_FIRST_MESSAGES_SELECTOR: string) => {
return Array.from(document.querySelectorAll(CONSOLE_FIRST_MESSAGES_SELECTOR))
.every(message => message.childNodes.length > 0);
}, {timeout: 0}, CONSOLE_ALL_MESSAGES_SELECTOR));
return frontend.evaluate((CONSOLE_MESSAGE_WRAPPER_SELECTOR, STACK_PREVIEW_CONTAINER) => {
return Array.from(document.querySelectorAll(CONSOLE_MESSAGE_WRAPPER_SELECTOR)).map(wrapper => {
const message = wrapper.querySelector('.console-message-text')?.textContent;
const source = wrapper.querySelector('.devtools-link')?.textContent;
const consoleMessage = wrapper.querySelector('.console-message');
const repeatCount = wrapper.querySelector('.console-message-repeat-count');
const stackPreviewRoot = wrapper.querySelector('.hidden > span');
const stackPreview = stackPreviewRoot?.shadowRoot?.querySelector(STACK_PREVIEW_CONTAINER) ?? null;
return {
message,
messageClasses: consoleMessage?.className,
repeatCount: repeatCount ? repeatCount?.textContent : null,
source,
stackPreview: stackPreview ? stackPreview?.textContent : null,
wrapperClasses: wrapper?.className,
};
});
}, CONSOLE_MESSAGE_WRAPPER_SELECTOR, STACK_PREVIEW_CONTAINER);
}
export async function focusConsolePrompt() {
await waitFor(CONSOLE_PROMPT_SELECTOR);
await click(CONSOLE_PROMPT_SELECTOR);
await waitFor('[aria-label="Console prompt"]');
// FIXME(crbug/1112692): Refactor test to remove the timeout.
await timeout(50);
}
export async function showVerboseMessages() {
await click(LOG_LEVELS_SELECTOR);
await click(LOG_LEVELS_VERBOSE_OPTION_SELECTOR);
}
export async function typeIntoConsole(frontend: puppeteer.Page, message: string) {
const asyncScope = new AsyncScope();
const consoleElement = await waitFor(CONSOLE_PROMPT_SELECTOR, undefined, asyncScope);
await consoleElement.click();
await consoleElement.type(message);
// Wait for autocomplete text to catch up.
const line = await waitFor('[aria-label="Console prompt"]', consoleElement, asyncScope);
const autocomplete = await $(CONSOLE_TOOLTIP_SELECTOR);
// The autocomplete element doesn't exist until the first autocomplete suggestion
// is actually given.
// Sometimes the autocomplete suggests `assert` when typing `console.clear()` which made a test flake.
// The following checks if there is any autocomplete text and dismisses it by pressing escape.
if (autocomplete && await autocomplete.evaluate(e => e.textContent)) {
consoleElement.press('Escape');
}
await asyncScope.exec(
() =>
frontend.waitForFunction((msg: string, ln: Element) => ln.textContent === msg, {timeout: 0}, message, line));
await consoleElement.press('Enter');
}
export async function typeIntoConsoleAndWaitForResult(frontend: puppeteer.Page, message: string) {
// Get the current number of console results so we can check we increased it.
const originalLength = await frontend.evaluate(() => {
return document.querySelectorAll('.console-user-command-result').length;
});
await typeIntoConsole(frontend, message);
await new AsyncScope().exec(() => frontend.waitForFunction((originalLength: number) => {
return document.querySelectorAll('.console-user-command-result').length === originalLength + 1;
}, {timeout: 0}, originalLength));
}
export async function unifyLogVM(actualLog: string, expectedLog: string) {
const actualLogArray = actualLog.trim().split('\n').map(s => s.trim());
const expectedLogArray = expectedLog.trim().split('\n').map(s => s.trim());
if (actualLogArray.length !== expectedLogArray.length) {
throw 'logs are not the same length';
}
for (let index = 0; index < actualLogArray.length; index++) {
const repl = actualLogArray[index].match(/VM\d+:/g);
if (repl) {
expectedLogArray[index] = expectedLogArray[index].replace(/VM\d+:/g, repl[0]);
}
}
return expectedLogArray.join('\n');
}
export async function switchToTopExecutionContext(frontend: puppeteer.Page) {
const dropdown = await waitFor('[aria-label^="JavaScript context:"]');
// Use keyboard to open drop down, select first item.
await dropdown.press('Space');
await frontend.keyboard.press('Home');
await frontend.keyboard.press('Space');
// Double-check that it worked.
await waitFor('[aria-label="JavaScript context: top"]');
}
export async function navigateToConsoleTab() {
// Locate the button for switching to the console tab.
await click(CONSOLE_TAB_SELECTOR);
await waitFor(CONSOLE_VIEW_SELECTOR);
}
export async function waitForConsoleInfoMessageAndClickOnLink() {
const consoleMessage = await waitFor('div.console-group-messages .console-info-level span.source-code');
await click('span.devtools-link', {root: consoleMessage});
}
export async function navigateToIssuesPanelViaInfoBar() {
// Navigate to Issues panel
await waitFor('#console-issues-counter');
await click('#console-issues-counter');
await waitFor('.issues-pane');
}
export async function turnOffHistoryAutocomplete() {
await click(CONSOLE_SETTINGS_SELECTOR);
await waitFor(AUTOCOMPLETE_FROM_HISTORY_SELECTOR);
await click(AUTOCOMPLETE_FROM_HISTORY_SELECTOR);
}
export async function toggleShowCorsErrors() {
await click(CONSOLE_SETTINGS_SELECTOR);
await waitFor(SHOW_CORS_ERRORS_SELECTOR);
await click(SHOW_CORS_ERRORS_SELECTOR);
await click(CONSOLE_SETTINGS_SELECTOR);
}
export async function toggleConsoleSetting(settingSelector: string) {
await click(CONSOLE_SETTINGS_SELECTOR);
await waitFor(settingSelector);
await click(settingSelector);
await click(CONSOLE_SETTINGS_SELECTOR);
}
async function getIssueButtonLabel(): Promise<string|null> {
const infobarButton = await waitFor('#console-issues-counter');
const iconButton = await waitFor('icon-button', infobarButton);
const titleElement = await waitFor('.icon-button-title', iconButton);
assertNotNullOrUndefined(titleElement);
const infobarButtonText = await titleElement.evaluate(node => (node as HTMLElement).textContent);
return infobarButtonText;
}
export async function waitForIssueButtonLabel(expectedLabel: string) {
await waitForFunction(async () => {
const label = await getIssueButtonLabel();
return expectedLabel === label;
});
}
export async function clickOnContextMenu(selectorForNode: string, ctxMenuItemName: string) {
await click(selectorForNode, {clickOptions: {button: 'right'}});
const copyButton = await waitForAria(ctxMenuItemName);
await copyButton.click();
}
/**
* Creates a function that runs a command and checks the nth output from the
* bottom (checks last message by default)
*/
export function checkCommandResultFunction(offset: number = 0) {
return async function(command: string, expected: string, message?: string) {
await typeIntoConsoleAndWaitForResult(getBrowserAndPages().frontend, command);
assert.strictEqual(await getLastConsoleMessages(offset), expected, message);
};
}
export async function getLastConsoleStacktrace(offset: number = 0) {
return (await getStructuredConsoleMessages()).at(-1 - offset)?.stackPreview as string;
}
export async function checkCommandStacktrace(command: string, expected: string, offset: number = 0) {
await typeIntoConsoleAndWaitForResult(getBrowserAndPages().frontend, command);
await unifyLogVM(await getLastConsoleStacktrace(offset), expected);
}