mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-12 17:45:49 +08:00
This CL fixes a problem where Window names retainer chains were not consistenly reported across platforms. In particular, some windows will be called 'Window /' on one platform, but only 'Window' on other platforms (occassionally). This CL normalizes window names further to just "Window". Bug: chromium:1110817 Change-Id: Ie37a4a627bde55b58ff7964701754ac987c081fb Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2412329 Reviewed-by: Jack Franklin <jacktfranklin@chromium.org> Commit-Queue: Sigurd Schneider <sigurds@chromium.org>
223 lines
7.9 KiB
TypeScript
223 lines
7.9 KiB
TypeScript
// 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 * as puppeteer from 'puppeteer';
|
|
|
|
import {$, platform, waitForElementWithTextContent} from '../../shared/helper.js';
|
|
import {$$, click, getBrowserAndPages, pasteText, waitFor, waitForFunction, waitForNone} from '../../shared/helper.js';
|
|
|
|
|
|
const NEW_HEAP_SNAPSHOT_BUTTON = 'button[aria-label="Take heap snapshot"]';
|
|
const MEMORY_PANEL_CONTENT = 'div[aria-label="Memory panel"]';
|
|
const PROFILE_TREE_SIDEBAR = 'div.profiles-tree-sidebar';
|
|
export const MEMORY_TAB_ID = '#tab-heap_profiler';
|
|
const CLASS_FILTER_INPUT = 'div[aria-placeholder="Class filter"]';
|
|
|
|
export async function navigateToMemoryTab() {
|
|
await click(MEMORY_TAB_ID);
|
|
await waitFor(MEMORY_PANEL_CONTENT);
|
|
await waitFor(PROFILE_TREE_SIDEBAR);
|
|
}
|
|
|
|
export async function takeHeapSnapshot() {
|
|
await click(NEW_HEAP_SNAPSHOT_BUTTON);
|
|
await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
|
|
await waitFor('.heap-snapshot-sidebar-tree-item.selected');
|
|
}
|
|
|
|
export async function waitForHeapSnapshotData() {
|
|
await waitFor('#profile-views');
|
|
await waitFor('#profile-views .data-grid');
|
|
const rowCountMatches = async () => {
|
|
const rows = await getDataGridRows('#profile-views table.data');
|
|
if (rows.length > 0) {
|
|
return rows;
|
|
}
|
|
return undefined;
|
|
};
|
|
return await waitForFunction(rowCountMatches);
|
|
}
|
|
|
|
export async function waitForNonEmptyHeapSnapshotData() {
|
|
const rows = await waitForHeapSnapshotData();
|
|
assert.isTrue(rows.length > 0);
|
|
}
|
|
|
|
export async function getDataGridRows(selector: string) {
|
|
// The grid in Memory Tab contains a tree
|
|
const grid = await waitFor(selector);
|
|
return await $$('.data-grid-data-grid-node', grid);
|
|
}
|
|
|
|
export async function setClassFilter(text: string) {
|
|
const classFilter = await waitFor(CLASS_FILTER_INPUT);
|
|
await classFilter.focus();
|
|
pasteText(text);
|
|
}
|
|
|
|
export async function triggerLocalFindDialog(frontend: puppeteer.Page) {
|
|
switch (platform) {
|
|
case 'mac':
|
|
await frontend.keyboard.down('Meta');
|
|
break;
|
|
|
|
default:
|
|
await frontend.keyboard.down('Control');
|
|
}
|
|
|
|
await frontend.keyboard.press('f');
|
|
|
|
switch (platform) {
|
|
case 'mac':
|
|
await frontend.keyboard.up('Meta');
|
|
break;
|
|
|
|
default:
|
|
await frontend.keyboard.up('Control');
|
|
}
|
|
}
|
|
|
|
export async function setSearchFilter(text: string) {
|
|
const {frontend} = getBrowserAndPages();
|
|
const grid = await waitFor('#profile-views table.data');
|
|
await grid.focus();
|
|
await triggerLocalFindDialog(frontend);
|
|
const SEARCH_QUERY = '[aria-label="Find"]';
|
|
const inputElement = await waitFor(SEARCH_QUERY);
|
|
if (!inputElement) {
|
|
assert.fail('Unable to find search input field');
|
|
}
|
|
await inputElement.focus();
|
|
await inputElement.type(text);
|
|
}
|
|
|
|
export async function waitForSearchResultNumber(results: number) {
|
|
const findMatch = async () => {
|
|
const currentMatch = await waitFor('label[for=\'search-input-field\']');
|
|
const currentTextContent = currentMatch && await currentMatch.evaluate(el => el.textContent);
|
|
if (currentTextContent && currentTextContent.endsWith(` ${results}`)) {
|
|
return currentMatch;
|
|
}
|
|
return undefined;
|
|
};
|
|
return await waitForFunction(findMatch);
|
|
}
|
|
|
|
/**
|
|
* Waits for the next selected row in `grid` whose text content is different from `previousContent`.
|
|
*
|
|
* @param grid the grid root element
|
|
* @param previousContent the previously selected text content
|
|
*/
|
|
async function waitForNextSelectedRow(grid: puppeteer.ElementHandle<Element>, previousContent: string) {
|
|
const findMatch = async () => {
|
|
const currentMatch = await grid.$('.data-grid-data-grid-node.selected');
|
|
const currentTextContent = currentMatch && await currentMatch.evaluate(el => el.textContent);
|
|
if (currentMatch && currentTextContent !== previousContent) {
|
|
return currentMatch;
|
|
}
|
|
return undefined;
|
|
};
|
|
return await waitForFunction(findMatch);
|
|
}
|
|
|
|
export async function findSearchResult(p: (el: puppeteer.ElementHandle<Element>) => Promise<boolean>) {
|
|
const grid = await waitFor('#profile-views table.data');
|
|
const next = await waitFor('[aria-label="Search next"]');
|
|
let previousContent = '';
|
|
const findSearchResult = async () => {
|
|
const currentMatch = await waitForNextSelectedRow(grid, previousContent);
|
|
if (currentMatch && await p(currentMatch)) {
|
|
return currentMatch;
|
|
}
|
|
// Since `waitForNextSelectedRow` above waited for the new selection, we haven't found
|
|
// the right search result yet, so click on the button for the next search result.
|
|
previousContent = currentMatch && await currentMatch.evaluate(el => el.textContent) || '';
|
|
await next.click();
|
|
return undefined;
|
|
};
|
|
return await waitForFunction(findSearchResult);
|
|
}
|
|
|
|
const normalizRetainerName = (retainerName: string) => {
|
|
// Retainers starting with `Window /` might have host information in their
|
|
// name, including the port, so we need to strip that. We can't distinguish
|
|
// Window from Window / either, because on Mac it is often just Window.
|
|
if (retainerName.startsWith('Window /')) {
|
|
return 'Window';
|
|
}
|
|
// Retainers including double-colons :: are names from the C++ implementation
|
|
// exposed through Chromium's gn arg `enable_additional_blink_object_names`;
|
|
// these should be considered implementation details, so we normalize them.
|
|
if (retainerName.includes('::')) {
|
|
if (retainerName.startsWith('Detached')) {
|
|
return 'Detached InternalNode';
|
|
}
|
|
return 'InternalNode';
|
|
}
|
|
return retainerName;
|
|
};
|
|
|
|
interface RetainerChainEntry {
|
|
propertyName: string;
|
|
retainerClassName: string;
|
|
}
|
|
|
|
export async function assertRetainerChainSatisfies(p: (retainerChain: Array<RetainerChainEntry>) => boolean) {
|
|
// Give some time for the expansion to finish.
|
|
const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data');
|
|
const retainerChain = [];
|
|
for (let i = 0; i < retainerGridElements.length; ++i) {
|
|
const retainer = retainerGridElements[i];
|
|
const propertyName = await retainer.$eval('span.property-name', el => el.textContent);
|
|
const rawRetainerClassName = await retainer.$eval('span.value', el => el.textContent);
|
|
if (!propertyName) {
|
|
assert.fail('Could not get retainer name');
|
|
}
|
|
if (!rawRetainerClassName) {
|
|
assert.fail('Could not get retainer value');
|
|
}
|
|
const retainerClassName = normalizRetainerName(rawRetainerClassName);
|
|
retainerChain.push({propertyName, retainerClassName});
|
|
if (await retainer.evaluate(el => !el.classList.contains('expanded'))) {
|
|
// Only follow the shortest retainer chain to the end. This relies on
|
|
// the retainer view behavior that auto-expands the shortest retaining
|
|
// chain.
|
|
break;
|
|
}
|
|
}
|
|
return p(retainerChain);
|
|
}
|
|
|
|
export async function waitUntilRetainerChainSatisfies(p: (retainerChain: Array<RetainerChainEntry>) => boolean) {
|
|
await waitForFunction(assertRetainerChainSatisfies.bind(null, p));
|
|
}
|
|
|
|
export async function waitForRetainerChain(expectedRetainers: Array<string>) {
|
|
await waitForFunction(assertRetainerChainSatisfies.bind(null, retainerChain => {
|
|
if (retainerChain.length !== expectedRetainers.length) {
|
|
return false;
|
|
}
|
|
for (let i = 0; i < expectedRetainers.length; ++i) {
|
|
if (retainerChain[i].retainerClassName !== expectedRetainers[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}));
|
|
}
|
|
|
|
export async function changeViewViaDropdown(newPerspective: string) {
|
|
const perspectiveDropdownSelector = 'select[aria-label="Perspective"]';
|
|
const dropdown = await $(perspectiveDropdownSelector) as puppeteer.ElementHandle<HTMLSelectElement>;
|
|
|
|
const optionToSelect = await waitForElementWithTextContent(newPerspective, dropdown);
|
|
const optionValue = await optionToSelect.evaluate(opt => opt.getAttribute('value'));
|
|
if (!optionValue) {
|
|
throw new Error(`Could not find heap snapshot perspective option: ${newPerspective}`);
|
|
}
|
|
dropdown.select(optionValue);
|
|
}
|