mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-06 20:11:38 +08:00
DISABLE_THIRD_PARTY_CHECK=big clang format run against all files in front_end and test/ Bug: 1319340 Change-Id: I1fff63cb07544ccd0e3059fc901f9a48ff4af13b Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3598879 Reviewed-by: Alex Rudenko <alexrudenko@chromium.org> Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Wolfgang Beyer <wolfi@chromium.org>
321 lines
13 KiB
TypeScript
321 lines
13 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 type {puppeteer} from '../../shared/helper.js';
|
|
import {
|
|
$$,
|
|
assertNotNullOrUndefined,
|
|
click,
|
|
getBrowserAndPages,
|
|
goToResource,
|
|
step,
|
|
waitFor,
|
|
waitForElementsWithTextContent,
|
|
waitForElementWithTextContent,
|
|
waitForFunction,
|
|
waitForNoElementsWithTextContent,
|
|
} from '../../shared/helper.js';
|
|
import {describe, it} from '../../shared/mocha-extensions.js';
|
|
import {
|
|
changeAllocationSampleViewViaDropdown,
|
|
changeViewViaDropdown,
|
|
findSearchResult,
|
|
getDataGridRows,
|
|
navigateToMemoryTab,
|
|
setSearchFilter,
|
|
takeAllocationProfile,
|
|
takeAllocationTimelineProfile,
|
|
takeHeapSnapshot,
|
|
waitForNonEmptyHeapSnapshotData,
|
|
waitForRetainerChain,
|
|
waitForSearchResultNumber,
|
|
waitUntilRetainerChainSatisfies,
|
|
} from '../helpers/memory-helpers.js';
|
|
|
|
describe('The Memory Panel', async function() {
|
|
// These tests render large chunks of data into DevTools and filter/search
|
|
// through it. On bots with less CPU power, these can fail because the
|
|
// rendering takes a long time, so we allow a much larger timeout.
|
|
if (this.timeout() !== 0) {
|
|
this.timeout(100000);
|
|
}
|
|
|
|
it('Loads content', async () => {
|
|
await goToResource('memory/default.html');
|
|
await navigateToMemoryTab();
|
|
});
|
|
|
|
it('Can take several heap snapshots ', async () => {
|
|
await goToResource('memory/default.html');
|
|
await navigateToMemoryTab();
|
|
await takeHeapSnapshot();
|
|
await waitForNonEmptyHeapSnapshotData();
|
|
await takeHeapSnapshot();
|
|
await waitForNonEmptyHeapSnapshotData();
|
|
const heapSnapShots = await $$('.heap-snapshot-sidebar-tree-item');
|
|
assert.strictEqual(heapSnapShots.length, 2);
|
|
});
|
|
|
|
it('Shows a DOM node and its JS wrapper as a single node', async () => {
|
|
await goToResource('memory/detached-node.html');
|
|
await navigateToMemoryTab();
|
|
await takeHeapSnapshot();
|
|
await waitForNonEmptyHeapSnapshotData();
|
|
await setSearchFilter('leaking');
|
|
await waitForSearchResultNumber(4);
|
|
await findSearchResult(async p => {
|
|
const el = await p.$(':scope > td > div > .object-value-function');
|
|
return el !== null && await el.evaluate(el => el.textContent === 'leaking()');
|
|
});
|
|
await waitForRetainerChain([
|
|
'Detached V8EventListener',
|
|
'Detached EventListener',
|
|
'Detached InternalNode',
|
|
'Detached InternalNode',
|
|
'Detached HTMLDivElement',
|
|
'Retainer',
|
|
'Window',
|
|
]);
|
|
});
|
|
|
|
// Flaky test
|
|
it.skipOnPlatforms(
|
|
['mac', 'linux'], '[crbug.com/1134602] Correctly retains the path for event listeners', async () => {
|
|
await goToResource('memory/event-listeners.html');
|
|
await step('taking a heap snapshot', async () => {
|
|
await navigateToMemoryTab();
|
|
await takeHeapSnapshot();
|
|
await waitForNonEmptyHeapSnapshotData();
|
|
});
|
|
await step('searching for the event listener', async () => {
|
|
await setSearchFilter('myEventListener');
|
|
await waitForSearchResultNumber(4);
|
|
});
|
|
|
|
await step('selecting the search result that we need', async () => {
|
|
await findSearchResult(async p => {
|
|
const el = await p.$(':scope > td > div > .object-value-function');
|
|
return el !== null && await el.evaluate(el => el.textContent === 'myEventListener()');
|
|
});
|
|
});
|
|
|
|
await step('waiting for retainer chain', async () => {
|
|
await waitForRetainerChain([
|
|
'V8EventListener',
|
|
'EventListener',
|
|
'InternalNode',
|
|
'InternalNode',
|
|
'HTMLBodyElement',
|
|
'HTMLHtmlElement',
|
|
'HTMLDocument',
|
|
'Window',
|
|
]);
|
|
});
|
|
});
|
|
|
|
it('Puts all ActiveDOMObjects with pending activities into one group', async () => {
|
|
const {frontend} = getBrowserAndPages();
|
|
await goToResource('memory/dom-objects.html');
|
|
await navigateToMemoryTab();
|
|
await takeHeapSnapshot();
|
|
await waitForNonEmptyHeapSnapshotData();
|
|
// The test ensures that the following structure is present:
|
|
// Pending activities
|
|
// -> Pending activities
|
|
// -> InternalNode
|
|
// -> MediaQueryList
|
|
// -> MediaQueryList
|
|
await setSearchFilter('Pending activities');
|
|
// Here and below we have to wait until the elements are actually created
|
|
// and visible.
|
|
const [pendingActivitiesSpan] = await waitForFunction(async () => {
|
|
const elements = await frontend.$x('//span[text()="Pending activities"]');
|
|
return elements.length > 0 ? elements : undefined;
|
|
});
|
|
const [pendingActiviesRow] = await pendingActivitiesSpan.$x('ancestor-or-self::tr');
|
|
await waitForFunction(async () => {
|
|
await click(pendingActivitiesSpan);
|
|
const res = await pendingActiviesRow.evaluate(x => x.classList.toString());
|
|
return res.includes('selected');
|
|
});
|
|
await frontend.keyboard.press('ArrowRight');
|
|
const [internalNodeSpan] = await waitForFunction(async () => {
|
|
const elements = await frontend.$x(
|
|
'//span[text()="InternalNode"][ancestor-or-self::tr[preceding-sibling::*[1][//span[text()="Pending activities"]]]]');
|
|
return elements.length === 1 ? elements : undefined;
|
|
});
|
|
const [internalNodeRow] = await internalNodeSpan.$x('ancestor-or-self::tr');
|
|
await waitForFunction(async () => {
|
|
await click(internalNodeSpan);
|
|
const res = await internalNodeRow.evaluate(x => x.classList.toString());
|
|
return res.includes('selected');
|
|
});
|
|
await frontend.keyboard.press('ArrowRight');
|
|
await waitForFunction(async () => {
|
|
const pendingActiviesChildren = await waitForElementsWithTextContent('MediaQueryList');
|
|
return pendingActiviesChildren.length === 2;
|
|
});
|
|
});
|
|
|
|
it('Shows the correct number of divs for a detached DOM tree correctly', async () => {
|
|
await goToResource('memory/detached-dom-tree.html');
|
|
await navigateToMemoryTab();
|
|
await takeHeapSnapshot();
|
|
await waitForNonEmptyHeapSnapshotData();
|
|
await setSearchFilter('Detached HTMLDivElement');
|
|
await waitForSearchResultNumber(3);
|
|
});
|
|
|
|
it('Shows the correct output for an attached iframe', async () => {
|
|
await goToResource('memory/attached-iframe.html');
|
|
await navigateToMemoryTab();
|
|
await takeHeapSnapshot();
|
|
await waitForNonEmptyHeapSnapshotData();
|
|
await setSearchFilter('Retainer');
|
|
await waitForSearchResultNumber(8);
|
|
await findSearchResult(async p => {
|
|
const el = await p.$(':scope > td > div > .object-value-object');
|
|
return el !== null && await el.evaluate(el => el.textContent === 'Retainer');
|
|
});
|
|
// The following line checks two things: That the property 'aUniqueName'
|
|
// in the iframe is retaining the Retainer class object, and that the
|
|
// iframe window is not detached.
|
|
await waitUntilRetainerChainSatisfies(
|
|
retainerChain => retainerChain.some(
|
|
({propertyName, retainerClassName}) => propertyName === 'aUniqueName' && retainerClassName === 'Window'));
|
|
});
|
|
|
|
it('Correctly shows multiple retainer paths for an object', async () => {
|
|
await goToResource('memory/multiple-retainers.html');
|
|
await navigateToMemoryTab();
|
|
await takeHeapSnapshot();
|
|
await waitForNonEmptyHeapSnapshotData();
|
|
await setSearchFilter('leaking');
|
|
await waitForSearchResultNumber(4);
|
|
await findSearchResult(async p => {
|
|
const el = await p.$(':scope > td > div > .object-value-string');
|
|
return el !== null && await el.evaluate(el => el.textContent === '"leaking"');
|
|
});
|
|
|
|
await waitForFunction(async () => {
|
|
// Wait for all the rows of the data-grid to load.
|
|
const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data');
|
|
return retainerGridElements.length === 9;
|
|
});
|
|
|
|
const sharedInLeakingElementRow = await waitForFunction(async () => {
|
|
const results = await getDataGridRows('.retaining-paths-view table.data');
|
|
const findPromises = await Promise.all(results.map(async e => {
|
|
const textContent = await e.evaluate(el => el.textContent);
|
|
// Can't search for "shared in leaking()" because the different parts are spaced with CSS.
|
|
return textContent && textContent.startsWith('sharedinleaking()') ? e : null;
|
|
}));
|
|
return findPromises.find(result => result !== null);
|
|
});
|
|
|
|
if (!sharedInLeakingElementRow) {
|
|
assert.fail('Could not find data-grid row with "shared in leaking()" text.');
|
|
}
|
|
|
|
const textOfEl = await sharedInLeakingElementRow.evaluate(e => e.textContent || '');
|
|
// Double check we got the right element to avoid a confusing text failure
|
|
// later down the line.
|
|
assert.isTrue(textOfEl.startsWith('sharedinleaking()'));
|
|
|
|
// Have to click it not in the middle as the middle can hold the link to the
|
|
// file in the sources pane and we want to avoid clicking that.
|
|
await click(sharedInLeakingElementRow, {maxPixelsFromLeft: 10});
|
|
const {frontend} = getBrowserAndPages();
|
|
// Expand the data-grid for the shared list
|
|
await frontend.keyboard.press('ArrowRight');
|
|
|
|
// check that we found two V8EventListener objects
|
|
await waitForFunction(async () => {
|
|
const pendingActiviesChildren = await waitForElementsWithTextContent('V8EventListener');
|
|
return pendingActiviesChildren.length === 2;
|
|
});
|
|
|
|
// Now we want to get the two rows below the "shared in leaking()" row and assert on them.
|
|
// Unfortunately they are not structured in the data-grid as children, despite being children in the UI
|
|
// So the best way to get at them is to grab the two subsequent siblings of the "shared in leaking()" row.
|
|
const nextRow =
|
|
await sharedInLeakingElementRow.evaluateHandle<puppeteer.ElementHandle<HTMLElement>>(e => e.nextSibling);
|
|
if (!nextRow) {
|
|
assert.fail('Could not find row below "shared in leaking()" row');
|
|
}
|
|
const nextNextRow = await nextRow.evaluateHandle<puppeteer.ElementHandle<HTMLElement>>(e => e.nextSibling);
|
|
if (!nextNextRow) {
|
|
assert.fail('Could not find 2nd row below "shared in leaking()" row');
|
|
}
|
|
|
|
const childText = await Promise.all([nextRow, nextNextRow].map(async row => await row.evaluate(r => r.innerText)));
|
|
|
|
assert.isTrue(childText[0].includes('inV8EventListener'));
|
|
assert.isTrue(childText[1].includes('inEventListener'));
|
|
});
|
|
|
|
// Flaky test causing build failures
|
|
it.skip('[crbug.com/1239550] Shows the correct output for a detached iframe', async () => {
|
|
await goToResource('memory/detached-iframe.html');
|
|
await navigateToMemoryTab();
|
|
await takeHeapSnapshot();
|
|
await waitForNonEmptyHeapSnapshotData();
|
|
await setSearchFilter('Leak');
|
|
await waitForSearchResultNumber(8);
|
|
await waitUntilRetainerChainSatisfies(
|
|
retainerChain => retainerChain.some(({retainerClassName}) => retainerClassName === 'Detached Window'));
|
|
});
|
|
|
|
it('Shows the a tooltip', async () => {
|
|
await goToResource('memory/detached-dom-tree.html');
|
|
await navigateToMemoryTab();
|
|
await takeHeapSnapshot();
|
|
await waitForNonEmptyHeapSnapshotData();
|
|
await setSearchFilter('Detached HTMLDivElement');
|
|
await waitForSearchResultNumber(3);
|
|
await waitUntilRetainerChainSatisfies(retainerChain => {
|
|
return retainerChain.length > 0 && retainerChain[0].propertyName === 'retaining_wrapper';
|
|
});
|
|
const rows = await getDataGridRows('.retaining-paths-view table.data');
|
|
const propertyNameElement = await rows[0].$('span.property-name');
|
|
assertNotNullOrUndefined(propertyNameElement);
|
|
propertyNameElement.hover();
|
|
const el = await waitFor('div.vbox.flex-auto.no-pointer-events');
|
|
await waitFor('.source-code', el);
|
|
});
|
|
|
|
it('shows the flamechart for an allocation sample', async () => {
|
|
const {frontend} = getBrowserAndPages();
|
|
await goToResource('memory/allocations.html');
|
|
await navigateToMemoryTab();
|
|
void takeAllocationProfile(frontend);
|
|
void changeAllocationSampleViewViaDropdown('Chart');
|
|
await waitFor('canvas.flame-chart-canvas');
|
|
});
|
|
|
|
it('shows allocations for an allocation timeline', async () => {
|
|
const {frontend} = getBrowserAndPages();
|
|
await goToResource('memory/allocations.html');
|
|
await navigateToMemoryTab();
|
|
void takeAllocationTimelineProfile(frontend, {recordStacks: true});
|
|
await changeViewViaDropdown('Allocation');
|
|
|
|
const header = await waitForElementWithTextContent('Live Count');
|
|
const table = await header.evaluateHandle(node => {
|
|
return node.closest('.data-grid');
|
|
});
|
|
await waitFor('.data-grid-data-grid-node', table);
|
|
});
|
|
|
|
it('does not show allocations perspective when stacks not recorded', async () => {
|
|
const {frontend} = getBrowserAndPages();
|
|
await goToResource('memory/allocations.html');
|
|
await navigateToMemoryTab();
|
|
void takeAllocationTimelineProfile(frontend, {recordStacks: false});
|
|
const dropdown = await waitFor('select[aria-label="Perspective"]');
|
|
await waitForNoElementsWithTextContent('Allocation', dropdown);
|
|
});
|
|
});
|