Move Memory test to E2E non hosted

Fixed: 416405547
Change-Id: I7d84ed6a64f4c3a2d2c1bed1c96d1b7134f61bb1
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6759939
Auto-Submit: Nikolay Vitkov <[email protected]>
Reviewed-by: Simon Zünd <[email protected]>
Commit-Queue: Simon Zünd <[email protected]>
diff --git a/test/e2e/BUILD.gn b/test/e2e/BUILD.gn
index c8ceed5..ef050bb 100644
--- a/test/e2e/BUILD.gn
+++ b/test/e2e/BUILD.gn
@@ -24,7 +24,6 @@
     "host",
     "issues",
     "lighthouse",
-    "memory",
     "network",
     "panels",
     "performance",
diff --git a/test/e2e/helpers/memory-helpers.ts b/test/e2e/helpers/memory-helpers.ts
index 1ae6f6c..6e0fdd8 100644
--- a/test/e2e/helpers/memory-helpers.ts
+++ b/test/e2e/helpers/memory-helpers.ts
@@ -6,19 +6,9 @@
 import type * as puppeteer from 'puppeteer-core';
 
 import {
-  $,
-  $$,
-  click,
-  clickElement,
-  getBrowserAndPages,
-  pasteText,
   platform,
-  waitFor,
-  waitForAria,
-  waitForElementWithTextContent,
-  waitForFunction,
-  waitForNone,
 } from '../../shared/helper.js';
+import {getBrowserAndPagesWrappers} from '../../shared/non_hosted_wrappers.js';
 
 const NEW_HEAP_SNAPSHOT_BUTTON = 'devtools-button[aria-label="Take heap snapshot"]';
 const MEMORY_PANEL_CONTENT = 'div[aria-label="Memory panel"]';
@@ -27,114 +17,115 @@
 const CLASS_FILTER_INPUT = 'div[aria-placeholder="Filter by class"]';
 const SELECTED_RESULT = '#profile-views table.data tr.data-grid-data-grid-node.revealed.parent.selected';
 
-export async function navigateToMemoryTab() {
-  await click(MEMORY_TAB_ID);
-  await waitFor(MEMORY_PANEL_CONTENT);
-  await waitFor(PROFILE_TREE_SIDEBAR);
+export async function navigateToMemoryTab(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  await devToolsPage.click(MEMORY_TAB_ID);
+  await devToolsPage.waitFor(MEMORY_PANEL_CONTENT);
+  await devToolsPage.waitFor(PROFILE_TREE_SIDEBAR);
 }
 
-export async function takeDetachedElementsProfile() {
-  const radioButton = await $('//label[text()="Detached elements"]', undefined, 'xpath');
-  await clickElement(radioButton);
-  await click('devtools-button[aria-label="Obtain detached elements"]');
-  await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
-  await waitFor('.heap-snapshot-sidebar-tree-item.selected');
+export async function takeDetachedElementsProfile(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const radioButton = await devToolsPage.$('//label[text()="Detached elements"]', undefined, 'xpath');
+  await devToolsPage.clickElement(radioButton);
+  await devToolsPage.click('devtools-button[aria-label="Obtain detached elements"]');
+  await devToolsPage.waitForNone('.heap-snapshot-sidebar-tree-item.wait');
+  await devToolsPage.waitFor('.heap-snapshot-sidebar-tree-item.selected');
 }
 
-export async function takeAllocationProfile() {
-  const radioButton = await $('//label[text()="Allocation sampling"]', undefined, 'xpath');
-  await clickElement(radioButton);
-  await click('devtools-button[aria-label="Start heap profiling"]');
+export async function takeAllocationProfile(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const radioButton = await devToolsPage.$('//label[text()="Allocation sampling"]', undefined, 'xpath');
+  await devToolsPage.clickElement(radioButton);
+  await devToolsPage.click('devtools-button[aria-label="Start heap profiling"]');
   await new Promise(r => setTimeout(r, 200));
-  await click('devtools-button[aria-label="Stop heap profiling"]');
-  await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
-  await waitFor('.heap-snapshot-sidebar-tree-item.selected');
+  await devToolsPage.click('devtools-button[aria-label="Stop heap profiling"]');
+  await devToolsPage.waitForNone('.heap-snapshot-sidebar-tree-item.wait');
+  await devToolsPage.waitFor('.heap-snapshot-sidebar-tree-item.selected');
 }
 
-export async function takeAllocationTimelineProfile({recordStacks}: {recordStacks: boolean} = {
-  recordStacks: false,
-}) {
-  const radioButton = await $('//label[text()="Allocations on timeline"]', undefined, 'xpath');
-  await clickElement(radioButton);
+export async function takeAllocationTimelineProfile(
+    {recordStacks}: {recordStacks: boolean} = {
+      recordStacks: false,
+    },
+    devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const radioButton = await devToolsPage.$('//label[text()="Allocations on timeline"]', undefined, 'xpath');
+  await devToolsPage.clickElement(radioButton);
   if (recordStacks) {
-    await click('[title="Allocation stack traces (more overhead)"]');
+    await devToolsPage.click('[title="Allocation stack traces (more overhead)"]');
   }
-  await click('devtools-button[aria-label="Start recording heap profile"]');
+  await devToolsPage.click('devtools-button[aria-label="Start recording heap profile"]');
   await new Promise(r => setTimeout(r, 200));
-  await click('devtools-button[aria-label="Stop recording heap profile"]');
-  await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
-  await waitFor('.heap-snapshot-sidebar-tree-item.selected');
+  await devToolsPage.click('devtools-button[aria-label="Stop recording heap profile"]');
+  await devToolsPage.waitForNone('.heap-snapshot-sidebar-tree-item.wait');
+  await devToolsPage.waitFor('.heap-snapshot-sidebar-tree-item.selected');
 }
 
-export async function takeHeapSnapshot(name = 'Snapshot 1') {
-  await click(NEW_HEAP_SNAPSHOT_BUTTON);
-  await waitForNone('.heap-snapshot-sidebar-tree-item.wait');
-  await waitForFunction(async () => {
-    const selected = await waitFor('.heap-snapshot-sidebar-tree-item.selected');
-    const title = await waitFor('span.title', selected);
+export async function takeHeapSnapshot(name = 'Snapshot 1', devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  await devToolsPage.click(NEW_HEAP_SNAPSHOT_BUTTON);
+  await devToolsPage.waitForNone('.heap-snapshot-sidebar-tree-item.wait');
+  await devToolsPage.waitForFunction(async () => {
+    const selected = await devToolsPage.waitFor('.heap-snapshot-sidebar-tree-item.selected');
+    const title = await devToolsPage.waitFor('span.title', selected);
     return (await title.evaluate(e => e.textContent)) === name ? title : undefined;
   });
 }
 
-export async function waitForHeapSnapshotData() {
-  await waitFor('#profile-views');
-  await waitFor('#profile-views .data-grid');
+export async function waitForHeapSnapshotData(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  await devToolsPage.waitFor('#profile-views');
+  await devToolsPage.waitFor('#profile-views .data-grid');
   const rowCountMatches = async () => {
-    const rows = await getDataGridRows('#profile-views table.data');
+    const rows = await getDataGridRows('#profile-views table.data', devToolsPage);
     if (rows.length > 0) {
       return rows;
     }
     return undefined;
   };
-  return await waitForFunction(rowCountMatches);
+  return await devToolsPage.waitForFunction(rowCountMatches);
 }
 
-export async function waitForNonEmptyHeapSnapshotData() {
-  const rows = await waitForHeapSnapshotData();
+export async function waitForNonEmptyHeapSnapshotData(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const rows = await waitForHeapSnapshotData(devToolsPage);
   assert.isTrue(rows.length > 0);
 }
 
-export async function getDataGridRows(selector: string) {
+export async function getDataGridRows(selector: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
   // The grid in Memory Tab contains a tree
-  const grid = await waitFor(selector);
-  return await $$('.data-grid-data-grid-node', grid);
+  const grid = await devToolsPage.waitFor(selector);
+  return await devToolsPage.$$('.data-grid-data-grid-node', grid);
 }
 
-export async function setClassFilter(text: string) {
-  const classFilter = await waitFor(CLASS_FILTER_INPUT);
+export async function setClassFilter(text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const classFilter = await devToolsPage.waitFor(CLASS_FILTER_INPUT);
   await classFilter.focus();
-  void pasteText(text);
+  void devToolsPage.pasteText(text);
 }
 
-export async function triggerLocalFindDialog(frontend: puppeteer.Page) {
+export async function triggerLocalFindDialog(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
   switch (platform) {
     case 'mac':
-      await frontend.keyboard.down('Meta');
+      await devToolsPage.page.keyboard.down('Meta');
       break;
 
     default:
-      await frontend.keyboard.down('Control');
+      await devToolsPage.page.keyboard.down('Control');
   }
 
-  await frontend.keyboard.press('f');
+  await devToolsPage.page.keyboard.press('f');
 
   switch (platform) {
     case 'mac':
-      await frontend.keyboard.up('Meta');
+      await devToolsPage.page.keyboard.up('Meta');
       break;
 
     default:
-      await frontend.keyboard.up('Control');
+      await devToolsPage.page.keyboard.up('Control');
   }
 }
 
-export async function setSearchFilter(text: string) {
-  const {frontend} = getBrowserAndPages();
-  const grid = await waitFor('#profile-views table.data');
+export async function setSearchFilter(text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const grid = await devToolsPage.waitFor('#profile-views table.data');
   await grid.focus();
-  await triggerLocalFindDialog(frontend);
+  await triggerLocalFindDialog(devToolsPage);
   const SEARCH_QUERY = '[aria-label="Find"]';
-  const inputElement = await waitFor(SEARCH_QUERY);
+  const inputElement = await devToolsPage.waitFor(SEARCH_QUERY);
   if (!inputElement) {
     assert.fail('Unable to find search input field');
   }
@@ -142,42 +133,43 @@
   await inputElement.type(text);
 }
 
-export async function waitForSearchResultNumber(results: number) {
+export async function waitForSearchResultNumber(
+    results: number, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
   const findMatch = async () => {
-    const currentMatch = await waitFor('.search-results-matches');
+    const currentMatch = await devToolsPage.waitFor('.search-results-matches');
     const currentTextContent = currentMatch && await currentMatch.evaluate(el => el.textContent);
     if (currentTextContent?.endsWith(` ${results}`)) {
       return currentMatch;
     }
     return undefined;
   };
-  return await waitForFunction(findMatch);
+  return await devToolsPage.waitForFunction(findMatch);
 }
 
-export async function findSearchResult(searchResult: string, pollIntrerval = 500) {
-  const {frontend} = getBrowserAndPages();
-  const match = await waitFor('#profile-views table.data');
-  const matches = await waitFor(' .search-results-matches');
+export async function findSearchResult(
+    searchResult: string, pollIntrerval = 500, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const match = await devToolsPage.waitFor('#profile-views table.data');
+  const matches = await devToolsPage.waitFor(' .search-results-matches');
   const matchesText = await matches.evaluate(async element => {
     return element.textContent;
   });
   if (matchesText === '1 of 1') {
-    await waitForElementWithTextContent(searchResult, match);
+    await devToolsPage.waitForElementWithTextContent(searchResult, match);
   } else {
-    await waitForFunction(async () => {
-      const selectedBefore = await waitFor(SELECTED_RESULT);
-      await click('[aria-label="Show next result"]');
+    await devToolsPage.waitForFunction(async () => {
+      const selectedBefore = await devToolsPage.waitFor(SELECTED_RESULT);
+      await devToolsPage.click('[aria-label="Show next result"]');
       // Wait until the click has taken effect by checking that the selected
       // result has changed. This is done to prevent the assertion afterwards
       // from happening before the next result is fully loaded.
-      await waitForFunction(async () => {
-        const selectedAfter = await waitFor(SELECTED_RESULT);
-        return await frontend.evaluate((b, a) => {
+      await devToolsPage.waitForFunction(async () => {
+        const selectedAfter = await devToolsPage.waitFor(SELECTED_RESULT);
+        return await devToolsPage.page.evaluate((b, a) => {
           return b !== a;
         }, selectedBefore, selectedAfter);
       });
       const result = Promise.race([
-        waitForElementWithTextContent(searchResult, match),
+        devToolsPage.waitForElementWithTextContent(searchResult, match),
         new Promise(resolve => {
           setTimeout(resolve, pollIntrerval, false);
         }),
@@ -211,9 +203,10 @@
   retainerClassName: string;
 }
 
-export async function assertRetainerChainSatisfies(p: (retainerChain: RetainerChainEntry[]) => boolean) {
+export async function assertRetainerChainSatisfies(
+    p: (retainerChain: RetainerChainEntry[]) => boolean, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
   // Give some time for the expansion to finish.
-  const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data');
+  const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data', devToolsPage);
   const retainerChain = [];
   for (let i = 0; i < retainerGridElements.length; ++i) {
     const retainer = retainerGridElements[i];
@@ -237,8 +230,9 @@
   return p(retainerChain);
 }
 
-export async function waitUntilRetainerChainSatisfies(p: (retainerChain: RetainerChainEntry[]) => boolean) {
-  await waitForFunction(assertRetainerChainSatisfies.bind(null, p));
+export async function waitUntilRetainerChainSatisfies(
+    p: (retainerChain: RetainerChainEntry[]) => boolean, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  await devToolsPage.waitForFunction(assertRetainerChainSatisfies.bind(null, p, devToolsPage));
 }
 
 export function appearsInOrder(targetArray: string[], inputArray: string[]) {
@@ -266,18 +260,20 @@
   return false;
 }
 
-export async function waitForRetainerChain(expectedRetainers: string[]) {
-  await waitForFunction(assertRetainerChainSatisfies.bind(null, retainerChain => {
+export async function waitForRetainerChain(
+    expectedRetainers: string[], devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  await devToolsPage.waitForFunction(assertRetainerChainSatisfies.bind(null, retainerChain => {
     const actual = retainerChain.map(e => e.retainerClassName);
     return appearsInOrder(actual, expectedRetainers);
-  }));
+  }, devToolsPage));
 }
 
-export async function changeViewViaDropdown(newPerspective: string) {
+export async function changeViewViaDropdown(
+    newPerspective: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
   const perspectiveDropdownSelector = 'select[aria-label="Perspective"]';
-  const dropdown = await waitFor(perspectiveDropdownSelector);
+  const dropdown = await devToolsPage.waitFor(perspectiveDropdownSelector);
 
-  const optionToSelect = await waitForElementWithTextContent(newPerspective, dropdown);
+  const optionToSelect = await devToolsPage.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}`);
@@ -285,10 +281,13 @@
   await dropdown.select(optionValue);
 }
 
-export async function changeAllocationSampleViewViaDropdown(newPerspective: string) {
+export async function changeAllocationSampleViewViaDropdown(
+    newPerspective: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
   const perspectiveDropdownSelector = 'select[aria-label="Profile view mode"]';
-  const dropdown = await waitFor(perspectiveDropdownSelector);
-  const optionToSelect = await waitForElementWithTextContent(newPerspective, dropdown);
+  const dropdown = await devToolsPage.waitFor(
+      perspectiveDropdownSelector,
+  );
+  const optionToSelect = await devToolsPage.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}`);
@@ -296,21 +295,21 @@
   await dropdown.select(optionValue);
 }
 
-export async function focusTableRowWithName(text: string) {
-  const row = await waitFor(`//span[text()="${text}"]/ancestor::tr`, undefined, undefined, 'xpath');
-  await focusTableRow(row);
+export async function focusTableRowWithName(text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const row = await devToolsPage.waitFor(`//span[text()="${text}"]/ancestor::tr`, undefined, undefined, 'xpath');
+  await focusTableRow(row, devToolsPage);
 }
 
-export async function focusTableRow(row: puppeteer.ElementHandle<Element>) {
+export async function focusTableRow(
+    row: puppeteer.ElementHandle<Element>, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
   // Click in a numeric cell, to avoid accidentally clicking a link.
-  const cell = await waitFor('.numeric-column', row);
-  await clickElement(cell);
+  const cell = await devToolsPage.waitFor('.numeric-column', row);
+  await devToolsPage.clickElement(cell);
 }
 
-export async function expandFocusedRow() {
-  const {frontend} = getBrowserAndPages();
-  await frontend.keyboard.press('ArrowRight');
-  await waitFor('.selected.data-grid-data-grid-node.expanded');
+export async function expandFocusedRow(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  await devToolsPage.page.keyboard.press('ArrowRight');
+  await devToolsPage.waitFor('.selected.data-grid-data-grid-node.expanded');
 }
 
 function parseByteString(str: string): number {
@@ -327,8 +326,9 @@
   return number;
 }
 
-async function getSizesFromRow(row: puppeteer.ElementHandle<Element>) {
-  const numericData = await $$('.numeric-column>.profile-multiple-values>span', row);
+async function getSizesFromRow(
+    row: puppeteer.ElementHandle<Element>, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const numericData = await devToolsPage.$$('.numeric-column>.profile-multiple-values>span', row);
   assert.lengthOf(numericData, 4);
   function readNumber(e: Element): string {
     return e.textContent as string;
@@ -339,74 +339,83 @@
   return {shallowSize, retainedSize};
 }
 
-export async function getSizesFromSelectedRow() {
-  const row = await waitFor('.selected.data-grid-data-grid-node');
-  return await getSizesFromRow(row);
+export async function getSizesFromSelectedRow(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const row = await devToolsPage.waitFor('.selected.data-grid-data-grid-node');
+  return await getSizesFromRow(row, devToolsPage);
 }
 
-export async function getCategoryRow(text: string, wait = true) {
+export async function getCategoryRow(
+    text: string, wait = true, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
   const selector = `//td[text()="${text}"]/ancestor::tr`;
-  return await (wait ? waitFor(selector, undefined, undefined, 'xpath') : $(selector, undefined, 'xpath'));
+  return await (wait ? devToolsPage.waitFor(selector, undefined, undefined, 'xpath') :
+                       devToolsPage.$(selector, undefined, 'xpath'));
 }
 
-export async function getSizesFromCategoryRow(text: string) {
-  const row = await getCategoryRow(text);
-  return await getSizesFromRow(row);
+export async function getSizesFromCategoryRow(text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const row = await getCategoryRow(text, undefined, devToolsPage);
+  return await getSizesFromRow(row, devToolsPage);
 }
 
-export async function getDistanceFromCategoryRow(text: string) {
-  const row = await getCategoryRow(text);
-  const numericColumns = await $$('.numeric-column', row);
+export async function getDistanceFromCategoryRow(
+    text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const row = await getCategoryRow(text, undefined, devToolsPage);
+  const numericColumns = await devToolsPage.$$('.numeric-column', row);
   return await numericColumns[0].evaluate(e => parseInt(e.textContent as string, 10));
 }
 
-export async function getCountFromCategoryRowWithName(text: string) {
-  const row = await getCategoryRow(text);
-  return await getCountFromCategoryRow(row);
+export async function getCountFromCategoryRowWithName(
+    text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const row = await getCategoryRow(text, undefined, devToolsPage);
+  return await getCountFromCategoryRow(row, devToolsPage);
 }
 
-export async function getCountFromCategoryRow(row: puppeteer.ElementHandle<Element>) {
-  const countSpan = await waitFor('.objects-count', row);
+export async function getCountFromCategoryRow(
+    row: puppeteer.ElementHandle<Element>, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const countSpan = await devToolsPage.waitFor('.objects-count', row);
   return await countSpan.evaluate(e => parseInt((e.textContent ?? '').substring(1), 10));
 }
 
-export async function getAddedCountFromComparisonRowWithName(text: string) {
-  const row = await getCategoryRow(text);
-  return await getAddedCountFromComparisonRow(row);
+export async function getAddedCountFromComparisonRowWithName(
+    text: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const row = await getCategoryRow(text, undefined, devToolsPage);
+  return await getAddedCountFromComparisonRow(row, devToolsPage);
 }
 
-export async function getAddedCountFromComparisonRow(row: puppeteer.ElementHandle<Element>) {
-  const addedCountCell = await waitFor('.addedCount-column', row);
+export async function getAddedCountFromComparisonRow(
+    row: puppeteer.ElementHandle<Element>, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const addedCountCell = await devToolsPage.waitFor('.addedCount-column', row);
   const countText = await addedCountCell.evaluate(e => e.textContent ?? '');
   return parseByteString(countText);
 }
 
-export async function getRemovedCountFromComparisonRow(row: puppeteer.ElementHandle<Element>) {
-  const addedCountCell = await waitFor('.removedCount-column', row);
+export async function getRemovedCountFromComparisonRow(
+    row: puppeteer.ElementHandle<Element>, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const addedCountCell = await devToolsPage.waitFor('.removedCount-column', row);
   const countText = await addedCountCell.evaluate(e => e.textContent ?? '');
   return parseByteString(countText);
 }
 
-export async function clickOnContextMenuForRetainer(retainerName: string, menuItem: string) {
-  const retainersPane = await waitFor('.retaining-paths-view');
-  const element = await waitFor(`//span[text()="${retainerName}"]`, retainersPane, undefined, 'xpath');
+export async function clickOnContextMenuForRetainer(
+    retainerName: string, menuItem: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const retainersPane = await devToolsPage.waitFor('.retaining-paths-view');
+  const element = await devToolsPage.waitFor(`//span[text()="${retainerName}"]`, retainersPane, undefined, 'xpath');
   // Push the click right a bit further to avoid the disclosure triangle.
-  await clickElement(element, {clickOptions: {button: 'right', offset: {x: 35, y: 0}}});
-  const button = await waitForAria(menuItem);
-  await clickElement(button);
+  await devToolsPage.clickElement(element, {clickOptions: {button: 'right', offset: {x: 35, y: 0}}});
+  const button = await devToolsPage.waitForAria(menuItem);
+  await devToolsPage.clickElement(button);
 }
 
-export async function restoreIgnoredRetainers() {
-  const element = await waitFor('devtools-button[aria-label="Restore ignored retainers"]');
-  await clickElement(element);
+export async function restoreIgnoredRetainers(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const element = await devToolsPage.waitFor('devtools-button[aria-label="Restore ignored retainers"]');
+  await devToolsPage.clickElement(element);
 }
 
-export async function setFilterDropdown(filter: string) {
-  const select = await waitFor('devtools-toolbar select[aria-label="Filter"]');
+export async function setFilterDropdown(filter: string, devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const select = await devToolsPage.waitFor('devtools-toolbar select[aria-label="Filter"]');
   await select.select(filter);
 }
 
-export async function checkExposeInternals() {
-  const element = await waitForElementWithTextContent('Internals with implementation details');
-  await clickElement(element);
+export async function checkExposeInternals(devToolsPage = getBrowserAndPagesWrappers().devToolsPage) {
+  const element = await devToolsPage.waitForElementWithTextContent('Internals with implementation details');
+  await devToolsPage.clickElement(element);
 }
diff --git a/test/e2e/memory/memory_test.ts b/test/e2e/memory/memory_test.ts
deleted file mode 100644
index f9dd276..0000000
--- a/test/e2e/memory/memory_test.ts
+++ /dev/null
@@ -1,615 +0,0 @@
-// 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-core';
-
-import {
-  $,
-  $$,
-  assertNotNullOrUndefined,
-  clickElement,
-  drainFrontendTaskQueue,
-  enableExperiment,
-  getBrowserAndPages,
-  goToResource,
-  step,
-  waitFor,
-  waitForElementsWithTextContent,
-  waitForElementWithTextContent,
-  waitForFunction,
-  waitForMany,
-  waitForNoElementsWithTextContent,
-} from '../../shared/helper.js';
-import {
-  changeAllocationSampleViewViaDropdown,
-  changeViewViaDropdown,
-  checkExposeInternals,
-  clickOnContextMenuForRetainer,
-  expandFocusedRow,
-  findSearchResult,
-  focusTableRow,
-  focusTableRowWithName,
-  getAddedCountFromComparisonRow,
-  getAddedCountFromComparisonRowWithName,
-  getCategoryRow,
-  getCountFromCategoryRow,
-  getCountFromCategoryRowWithName,
-  getDataGridRows,
-  getDistanceFromCategoryRow,
-  getRemovedCountFromComparisonRow,
-  getSizesFromCategoryRow,
-  getSizesFromSelectedRow,
-  navigateToMemoryTab,
-  restoreIgnoredRetainers,
-  setClassFilter,
-  setFilterDropdown,
-  setSearchFilter,
-  takeAllocationProfile,
-  takeAllocationTimelineProfile,
-  takeDetachedElementsProfile,
-  takeHeapSnapshot,
-  waitForNonEmptyHeapSnapshotData,
-  waitForRetainerChain,
-  waitForSearchResultNumber,
-  waitUntilRetainerChainSatisfies,
-} from '../helpers/memory-helpers.js';
-
-describe('The Memory Panel', 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('Snapshot 2');
-    await waitForNonEmptyHeapSnapshotData();
-    const heapSnapShots = await $$('.heap-snapshot-sidebar-tree-item');
-    assert.lengthOf(heapSnapShots, 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('leaking()');
-    await waitForRetainerChain([
-      'Detached V8EventListener',
-      'Detached EventListener',
-      'Detached InternalNode',
-      'Detached InternalNode',
-      'Detached <div>',
-      'Retainer',
-      'Window',
-    ]);
-  });
-
-  it('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('myEventListener()');
-    });
-
-    await step('waiting for retainer chain', async () => {
-      await waitForRetainerChain([
-        'V8EventListener',
-        'EventListener',
-        'InternalNode',
-        'InternalNode',
-        '<body>',
-      ]);
-    });
-  });
-
-  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.
-    await waitForFunction(async () => {
-      const pendingActivitiesSpan = await waitFor('//span[text()="Pending activities"]', undefined, undefined, 'xpath');
-      const pendingActiviesRow = await waitFor('ancestor-or-self::tr', pendingActivitiesSpan, undefined, 'xpath');
-      try {
-        await clickElement(pendingActivitiesSpan);
-      } catch {
-        return false;
-      }
-      const res = await pendingActiviesRow.evaluate(x => x.classList.toString());
-      return res.includes('selected');
-    });
-    await frontend.keyboard.press('ArrowRight');
-    const internalNodeSpan = await waitFor(
-        '//span[text()="InternalNode"][ancestor-or-self::tr[preceding-sibling::*[1][//span[text()="Pending activities"]]]]',
-        undefined, undefined, 'xpath');
-    const internalNodeRow = await $('ancestor-or-self::tr', internalNodeSpan, 'xpath');
-    await waitForFunction(async () => {
-      await clickElement(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 <div>');
-    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('searchable string');
-    await waitForSearchResultNumber(1);
-    // 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'));
-  });
-
-  // Flaky on win and linux
-  it.skip('[crbug.com/40238574] 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('\"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?.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 clickElement(sharedInLeakingElementRow /* TODO(crbug.com/1363150): {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(e => e.nextSibling)).asElement() as
-        puppeteer.ElementHandle<HTMLElement>;
-    if (!nextRow) {
-      assert.fail('Could not find row below "shared in leaking()" row');
-    }
-    const nextNextRow =
-        (await nextRow.evaluateHandle(e => e.nextSibling)).asElement() as puppeteer.ElementHandle<HTMLElement>;
-    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/40193901] 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 a tooltip', async () => {
-    await goToResource('memory/detached-dom-tree.html');
-    await navigateToMemoryTab();
-    await takeHeapSnapshot();
-    await waitForNonEmptyHeapSnapshotData();
-    await setSearchFilter('Detached <div>');
-    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');
-    await propertyNameElement!.hover();
-    const el = await waitFor('div.vbox.flex-auto.no-pointer-events');
-    await waitFor('.source-code', el);
-
-    await setSearchFilter('system / descriptorarray');
-    await findSearchResult('system / DescriptorArray');
-    const searchResultElement = await waitFor('.selected.data-grid-data-grid-node span.object-value-null');
-    await searchResultElement!.hover();
-    await waitFor('.widget .object-popover-footer');
-  });
-
-  it('shows the list of a detached node', async () => {
-    await goToResource('memory/detached-node.html');
-    await navigateToMemoryTab();
-    void takeDetachedElementsProfile();
-    await waitFor('.detached-elements-view');
-  });
-
-  it('shows the flamechart for an allocation sample', async () => {
-    await goToResource('memory/allocations.html');
-    await navigateToMemoryTab();
-    void takeAllocationProfile();
-    void changeAllocationSampleViewViaDropdown('Chart');
-    await waitFor('canvas.flame-chart-canvas');
-  });
-
-  it('shows allocations for an allocation timeline', async () => {
-    await goToResource('memory/allocations.html');
-    await navigateToMemoryTab();
-    void takeAllocationTimelineProfile({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 () => {
-    await goToResource('memory/allocations.html');
-    await navigateToMemoryTab();
-    void takeAllocationTimelineProfile({recordStacks: false});
-    const dropdown = await waitFor('select[aria-label="Perspective"]');
-    await waitForNoElementsWithTextContent('Allocation', dropdown);
-  });
-
-  it('shows object source links in snapshot', async () => {
-    const {target, frontend} = getBrowserAndPages();
-    await target.evaluate(`
-        class MyTestClass {
-          constructor() {
-            this.z = new Uint32Array(1e6);  // Pull the class to top.
-            this.myFunction = () => 42;
-          }
-        };
-        function* MyTestGenerator() {
-          yield 1;
-        }
-        class MyTestClass2 {}
-        window.myTestClass = new MyTestClass();
-        window.myTestGenerator = MyTestGenerator();
-        window.myTestClass2 = new MyTestClass2();
-        //# sourceURL=my-test-script.js`);
-    await navigateToMemoryTab();
-    await takeHeapSnapshot();
-    await setClassFilter('MyTest');
-    await waitForNonEmptyHeapSnapshotData();
-
-    const expectedEntries = [
-      {constructor: 'MyTestClass', link: 'my-test-script.js:3'},
-      {constructor: 'MyTestClass', prop: 'myFunction', link: 'my-test-script.js:5'},
-      {constructor: 'MyTestGenerator', link: 'my-test-script.js:8'},
-      {constructor: 'MyTestClass2', link: 'my-test-script.js:11'},
-    ];
-
-    const rows = await getDataGridRows('.data-grid');
-    for (const entry of expectedEntries) {
-      let row: puppeteer.ElementHandle<Element>|null = null;
-      // Find the row with the desired constructor.
-      for (const r of rows) {
-        const constructorName = await waitForFunction(() => r.evaluate(e => e.firstChild?.textContent));
-        if (entry.constructor === constructorName) {
-          row = r;
-          break;
-        }
-      }
-      assertNotNullOrUndefined(row);
-      // Expand the constructor sub-tree.
-      await clickElement(row);
-      await frontend.keyboard.press('ArrowRight');
-      // Get the object subtree/child.
-      const {objectElement, objectName} = await waitForFunction(async () => {
-        const objectElement =
-            await row?.evaluateHandle(e => e.nextSibling) as puppeteer.ElementHandle<HTMLElement>| null;
-        const objectName = await objectElement?.evaluate(e => e.querySelector('.object-value-object')?.textContent);
-        if (!objectName) {
-          return undefined;
-        }
-        return {objectElement, objectName};
-      });
-      let element = objectElement;
-      assertNotNullOrUndefined(element);
-      // Verify we have the object with the matching name.
-      assert.strictEqual(objectName, entry.constructor);
-      // Get the right property of the object if required.
-      if (entry.prop) {
-        // Expand the object.
-        await clickElement(element);
-        await frontend.keyboard.press('ArrowRight');
-        // Try to find the property.
-        element = await waitForFunction(async () => {
-          let row = element;
-          while (row) {
-            const nextRow = await row.evaluateHandle(e => e.nextSibling) as puppeteer.ElementHandle<HTMLElement>| null;
-            if (!nextRow) {
-              return undefined;
-            }
-            row = nextRow;
-            const text = await row.evaluate(e => e.querySelector('.property-name')?.textContent);
-            // If we did not find any text at all, then we saw all properties. Let us fail/retry here.
-            if (!text) {
-              return undefined;
-            }
-            // If we found the property, we are done.
-            if (text === entry.prop) {
-              return row;
-            }
-            // Continue looking for the property on the next row.
-          }
-          return undefined;
-        });
-        assertNotNullOrUndefined(element);
-      }
-
-      // Verify the link to the source code.
-      const linkText = await waitForFunction(
-          async () => await element?.evaluate(e => e.querySelector('.devtools-link')?.textContent));
-      assert.strictEqual(linkText, entry.link);
-    }
-  });
-
-  async function runJSSetTest() {
-    await navigateToMemoryTab();
-    await takeHeapSnapshot();
-    await waitForNonEmptyHeapSnapshotData();
-    await setSearchFilter('Retainer');
-    await waitForSearchResultNumber(4);
-    await findSearchResult('Retainer()');
-    await focusTableRowWithName('Retainer()');
-    await expandFocusedRow();
-    await focusTableRowWithName('customProperty');
-    const sizesForSet = await getSizesFromSelectedRow();
-    await expandFocusedRow();
-    await focusTableRowWithName('(internal array)[]');
-    const sizesForBackingStorage = await getSizesFromSelectedRow();
-    return {sizesForSet, sizesForBackingStorage};
-  }
-
-  it('Does not include backing store size in the shallow size of a JS Set', async () => {
-    await goToResource('memory/set.html');
-    await enableExperiment('show-option-tp-expose-internals-in-heap-snapshot');
-    await navigateToMemoryTab();
-    await checkExposeInternals();
-    const sizes = await runJSSetTest();
-
-    // The Set object is small, regardless of the contained content.
-    assert.isTrue(sizes.sizesForSet.shallowSize <= 100);
-    // The Set retains its backing storage.
-    // Note: 16 bytes is added to retainedSize to account for rounding present in the UI layer.
-    assert.isTrue(
-        sizes.sizesForSet.retainedSize + 16 >=
-        sizes.sizesForSet.shallowSize + sizes.sizesForBackingStorage.retainedSize);
-    // The backing storage contains 100 items, which occupy at least one pointer per item.
-    assert.isTrue(sizes.sizesForBackingStorage.shallowSize >= 400);
-    // The backing storage retains 100 strings, which occupy at least 16 bytes each.
-    assert.isTrue(sizes.sizesForBackingStorage.retainedSize >= sizes.sizesForBackingStorage.shallowSize + 1600);
-  });
-
-  it('Includes backing store size in the shallow size of a JS Set', async () => {
-    await goToResource('memory/set.html');
-    const sizes = await runJSSetTest();
-
-    // The Set is reported as containing at least 100 pointers.
-    assert.isTrue(sizes.sizesForSet.shallowSize >= 400);
-    // The Set retains its backing storage.
-    assert.isTrue(
-        sizes.sizesForSet.retainedSize >= sizes.sizesForSet.shallowSize + sizes.sizesForBackingStorage.retainedSize);
-    // The backing storage is reported as zero size.
-    assert.strictEqual(sizes.sizesForBackingStorage.shallowSize, 0);
-    // The backing storage retains 100 strings, which occupy at least 16 bytes each.
-    assert.isTrue(sizes.sizesForBackingStorage.retainedSize >= 1600);
-  });
-
-  it('Computes distances and sizes for WeakMap values correctly', async () => {
-    await goToResource('memory/weakmap.html');
-    await navigateToMemoryTab();
-    await takeHeapSnapshot();
-    await waitForNonEmptyHeapSnapshotData();
-    await setClassFilter('CustomClass');
-    assert.strictEqual(5, await getDistanceFromCategoryRow('CustomClass1'));
-    assert.strictEqual(6, await getDistanceFromCategoryRow('CustomClass2'));
-    assert.strictEqual(2, await getDistanceFromCategoryRow('CustomClass3'));
-    assert.strictEqual(8, await getDistanceFromCategoryRow('CustomClass4'));
-    assert.isTrue((await getSizesFromCategoryRow('CustomClass1Key')).retainedSize >= 2 ** 15);
-    assert.isTrue((await getSizesFromCategoryRow('CustomClass2Key')).retainedSize >= 2 ** 15);
-    assert.isTrue((await getSizesFromCategoryRow('CustomClass3Key')).retainedSize < 2 ** 15);
-    assert.isTrue((await getSizesFromCategoryRow('CustomClass4Key')).retainedSize < 2 ** 15);
-    assert.isTrue((await getSizesFromCategoryRow('CustomClass4Retainer')).retainedSize >= 2 ** 15);
-  });
-
-  it('Allows ignoring retainers', async () => {
-    await goToResource('memory/ignoring-retainers.html');
-    await navigateToMemoryTab();
-    await takeHeapSnapshot();
-    await waitForNonEmptyHeapSnapshotData();
-    await setSearchFilter('searchable_string');
-    await waitForSearchResultNumber(2);
-    await findSearchResult('"searchable_string"');
-    await waitForRetainerChain(['{y}', 'KeyType', 'Window']);
-    await clickOnContextMenuForRetainer('KeyType', 'Ignore this retainer');
-    await waitForRetainerChain(['{y}', '{x}', 'Window']);
-    await clickOnContextMenuForRetainer('x', 'Ignore this retainer');
-    await waitForRetainerChain(['{y}', '(internal array)[]', 'WeakMap', 'Window']);
-    await clickOnContextMenuForRetainer('(internal array)[]', 'Ignore this retainer');
-    await waitForRetainerChain([
-      '{y}',
-      '{d}',
-      `{${'#'.repeat(130)}, …}`,
-      '{b, irrelevantProperty, <symbol also irrelevant>, "}"}',
-      '{a, extraProp0, extraProp1, extraProp2, extraProp3, …, extraProp6, extraProp7, extraProp8, extraProp9}',
-      'Window',
-    ]);
-    await clickOnContextMenuForRetainer('b', 'Ignore this retainer');
-    await waitForRetainerChain(['(Internalized strings)', '(GC roots)']);
-    await restoreIgnoredRetainers();
-    await waitForRetainerChain(['{y}', 'KeyType', 'Window']);
-  });
-
-  it('Can filter the summary view', async () => {
-    await goToResource('memory/filtering.html');
-    await navigateToMemoryTab();
-    await takeHeapSnapshot();
-    await waitForNonEmptyHeapSnapshotData();
-    await setFilterDropdown('Duplicated strings');
-    await setSearchFilter('"duplicatedKey":"duplicatedValue"');
-    await waitForSearchResultNumber(2);
-    await setFilterDropdown('Objects retained by detached DOM nodes');
-    await getCategoryRow('ObjectRetainedByDetachedDom');
-    assert.isNotOk(await getCategoryRow('ObjectRetainedByBothDetachedDomAndConsole', false));
-    await setFilterDropdown('Objects retained by DevTools Console');
-    await getCategoryRow('ObjectRetainedByConsole');
-    assert.isNotOk(await getCategoryRow('ObjectRetainedByBothDetachedDomAndConsole', false));
-  });
-
-  it('Groups HTML elements by tag name', async () => {
-    await goToResource('memory/dom-details.html');
-    await navigateToMemoryTab();
-    await takeHeapSnapshot();
-    await waitForNonEmptyHeapSnapshotData();
-    await setClassFilter('<div>');
-    assert.strictEqual(3, await getCountFromCategoryRowWithName('<div>'));
-    assert.strictEqual(3, await getCountFromCategoryRowWithName('Detached <div>'));
-    await setSearchFilter('Detached <div data-x="p" data-y="q">');
-    await waitForSearchResultNumber(1);
-  });
-
-  it('Groups plain JS objects by interface', async () => {
-    await goToResource('memory/diff.html');
-    await navigateToMemoryTab();
-    await takeHeapSnapshot();
-    await waitForNonEmptyHeapSnapshotData();
-    await setClassFilter('{a, b, c, d, ');
-    // Objects should be grouped by interface if there are at least two matching instances.
-    assert.strictEqual(2, await getCountFromCategoryRowWithName('{a, b, c, d, p, q, r}'));
-    assert.isNotOk(await getCategoryRow('{a, b, c, d, e}', /* wait:*/ false));
-    const {frontend, target} = getBrowserAndPages();
-    await target.bringToFront();
-    await target.click('button#update');
-    await frontend.bringToFront();
-    await takeHeapSnapshot('Snapshot 2');
-    await waitForNonEmptyHeapSnapshotData();
-    await changeViewViaDropdown('Comparison');
-    await setClassFilter('{a, b, c, d, ');
-    // When comparing, the old snapshot is categorized according to the new one's interfaces,
-    // so the comparison should report only one new object of the following type, not two.
-    assert.strictEqual(1, await getAddedCountFromComparisonRowWithName('{a, b, c, d, e}'));
-    // Only one of these objects remains, so it's no longer a category.
-    assert.isNotOk(await getCategoryRow('{a, b, c, d, p, q, r}', /* wait:*/ false));
-  });
-
-  it('Groups objects by constructor location', async () => {
-    await goToResource('memory/duplicated-names.html');
-    await navigateToMemoryTab();
-    await takeHeapSnapshot();
-    await waitForNonEmptyHeapSnapshotData();
-    // TODO: filtering does not work while UI is rendering snapshot.
-    await drainFrontendTaskQueue();
-    await setClassFilter('DuplicatedClassName');
-    let rows = await waitForMany('tr.data-grid-data-grid-node', 3);
-    assert.strictEqual(30, await getCountFromCategoryRow(rows[0]));
-    assert.strictEqual(3, await getCountFromCategoryRow(rows[1]));
-    assert.strictEqual(2, await getCountFromCategoryRow(rows[2]));
-    await focusTableRow(rows[0]);
-    await expandFocusedRow();
-    // TODO: pressing arrowDown does not work while UI is rendering.
-    await drainFrontendTaskQueue();
-    const {frontend, target} = getBrowserAndPages();
-    await frontend.keyboard.press('ArrowDown');
-    await clickOnContextMenuForRetainer('x', 'Reveal in Summary view');
-    await waitUntilRetainerChainSatisfies(
-        retainerChain => retainerChain.length > 0 && retainerChain[0].propertyName === 'a');
-    await target.bringToFront();
-    await target.click('button#update');
-    await frontend.bringToFront();
-    await takeHeapSnapshot('Snapshot 2');
-    await waitForNonEmptyHeapSnapshotData();
-    await changeViewViaDropdown('Comparison');
-    await setClassFilter('DuplicatedClassName');
-    rows = await waitForMany('tr.data-grid-data-grid-node', 3);
-    assert.strictEqual(5, await getAddedCountFromComparisonRow(rows[0]));
-    assert.strictEqual(1, await getRemovedCountFromComparisonRow(rows[0]));
-    assert.strictEqual(1, await getAddedCountFromComparisonRow(rows[1]));
-    assert.strictEqual(10, await getRemovedCountFromComparisonRow(rows[1]));
-    assert.strictEqual(0, await getAddedCountFromComparisonRow(rows[2]));
-    assert.strictEqual(2, await getRemovedCountFromComparisonRow(rows[2]));
-  });
-});
diff --git a/test/e2e/network/network-request-view_test.ts b/test/e2e/network/network-request-view_test.ts
index 22b545c..85b7dcb 100644
--- a/test/e2e/network/network-request-view_test.ts
+++ b/test/e2e/network/network-request-view_test.ts
@@ -660,7 +660,7 @@
     const SEARCH_RESULT = '.search-result';
     const {frontend} = getBrowserAndPages();
 
-    await triggerLocalFindDialog(frontend);
+    await triggerLocalFindDialog();
     await waitFor(SEARCH_QUERY);
     const inputElement = await $(SEARCH_QUERY);
     if (!inputElement) {
diff --git a/test/e2e_non_hosted/BUILD.gn b/test/e2e_non_hosted/BUILD.gn
index 97ff69c..82269be 100644
--- a/test/e2e_non_hosted/BUILD.gn
+++ b/test/e2e_non_hosted/BUILD.gn
@@ -30,6 +30,7 @@
     "layers",
     "lighthouse",
     "media",
+    "memory",
     "network",
     "performance",
     "puppeteer",
diff --git a/test/e2e/memory/BUILD.gn b/test/e2e_non_hosted/memory/BUILD.gn
similarity index 79%
rename from test/e2e/memory/BUILD.gn
rename to test/e2e_non_hosted/memory/BUILD.gn
index d798524..3917f25 100644
--- a/test/e2e/memory/BUILD.gn
+++ b/test/e2e_non_hosted/memory/BUILD.gn
@@ -4,11 +4,11 @@
 
 import("../../../scripts/build/typescript/typescript.gni")
 
-node_ts_library("memory") {
+ts_e2e_library("memory") {
   sources = [ "memory_test.ts" ]
 
   deps = [
-    "../../shared",
-    "../helpers",
+    "../../e2e/helpers",
+    "../shared",
   ]
 }
diff --git a/test/e2e/memory/DIR_METADATA b/test/e2e_non_hosted/memory/DIR_METADATA
similarity index 100%
rename from test/e2e/memory/DIR_METADATA
rename to test/e2e_non_hosted/memory/DIR_METADATA
diff --git a/test/e2e_non_hosted/memory/memory_test.ts b/test/e2e_non_hosted/memory/memory_test.ts
new file mode 100644
index 0000000..cea0d64
--- /dev/null
+++ b/test/e2e_non_hosted/memory/memory_test.ts
@@ -0,0 +1,619 @@
+// 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-core';
+
+import {
+  changeAllocationSampleViewViaDropdown,
+  changeViewViaDropdown,
+  checkExposeInternals,
+  clickOnContextMenuForRetainer,
+  expandFocusedRow,
+  findSearchResult,
+  focusTableRow,
+  focusTableRowWithName,
+  getAddedCountFromComparisonRow,
+  getAddedCountFromComparisonRowWithName,
+  getCategoryRow,
+  getCountFromCategoryRow,
+  getCountFromCategoryRowWithName,
+  getDataGridRows,
+  getDistanceFromCategoryRow,
+  getRemovedCountFromComparisonRow,
+  getSizesFromCategoryRow,
+  getSizesFromSelectedRow,
+  navigateToMemoryTab,
+  restoreIgnoredRetainers,
+  setClassFilter,
+  setFilterDropdown,
+  setSearchFilter,
+  takeAllocationProfile,
+  takeAllocationTimelineProfile,
+  takeDetachedElementsProfile,
+  takeHeapSnapshot,
+  waitForNonEmptyHeapSnapshotData,
+  waitForRetainerChain,
+  waitForSearchResultNumber,
+  waitUntilRetainerChainSatisfies,
+} from '../../e2e/helpers/memory-helpers.js';
+import {
+  assertNotNullOrUndefined,
+  step,
+} from '../../shared/helper.js';
+import type {DevToolsPage} from '../shared/frontend-helper.js';
+
+async function runJSSetTest(devToolsPage: DevToolsPage) {
+  await navigateToMemoryTab(devToolsPage);
+  await takeHeapSnapshot(undefined, devToolsPage);
+  await waitForNonEmptyHeapSnapshotData(devToolsPage);
+  await setSearchFilter('Retainer', devToolsPage);
+  await waitForSearchResultNumber(4, devToolsPage);
+  await findSearchResult('Retainer()', undefined, devToolsPage);
+  await focusTableRowWithName('Retainer()', devToolsPage);
+  await expandFocusedRow(devToolsPage);
+  await focusTableRowWithName('customProperty', devToolsPage);
+  const sizesForSet = await getSizesFromSelectedRow(devToolsPage);
+  await expandFocusedRow(devToolsPage);
+  await focusTableRowWithName('(internal array)[]', devToolsPage);
+  const sizesForBackingStorage = await getSizesFromSelectedRow(devToolsPage);
+  return {sizesForSet, sizesForBackingStorage};
+}
+
+describe('The Memory Panel', 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(20_000);
+  }
+
+  setup({dockingMode: 'undocked'});
+
+  it('Loads content', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/default.html');
+    await navigateToMemoryTab(devToolsPage);
+  });
+
+  it('Can take several heap snapshots ', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/default.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await takeHeapSnapshot('Snapshot 2', devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    const heapSnapShots = await devToolsPage.$$('.heap-snapshot-sidebar-tree-item');
+    assert.lengthOf(heapSnapShots, 2);
+  });
+
+  it('Shows a DOM node and its JS wrapper as a single node', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/detached-node.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await setSearchFilter('leaking', devToolsPage);
+    await waitForSearchResultNumber(4, devToolsPage);
+    await findSearchResult('leaking()', undefined, devToolsPage);
+    await waitForRetainerChain(
+        [
+          'Detached V8EventListener',
+          'Detached EventListener',
+          'Detached InternalNode',
+          'Detached InternalNode',
+          'Detached <div>',
+          'Retainer',
+          'Window',
+        ],
+        devToolsPage);
+  });
+
+  it('Correctly retains the path for event listeners', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/event-listeners.html');
+    await step('taking a heap snapshot', async () => {
+      await navigateToMemoryTab(devToolsPage);
+      await takeHeapSnapshot(undefined, devToolsPage);
+      await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    });
+    await step('searching for the event listener', async () => {
+      await setSearchFilter('myEventListener', devToolsPage);
+      await waitForSearchResultNumber(4, devToolsPage);
+    });
+
+    await step('selecting the search result that we need', async () => {
+      await findSearchResult('myEventListener()', undefined, devToolsPage);
+    });
+
+    await step('waiting for retainer chain', async () => {
+      await waitForRetainerChain(
+          [
+            'V8EventListener',
+            'EventListener',
+            'InternalNode',
+            'InternalNode',
+            '<body>',
+          ],
+          devToolsPage);
+    });
+  });
+
+  it('Puts all ActiveDOMObjects with pending activities into one group', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/dom-objects.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    // The test ensures that the following structure is present:
+    // Pending activities
+    // -> Pending activities
+    //    -> InternalNode
+    //       -> MediaQueryList
+    //       -> MediaQueryList
+    await setSearchFilter('Pending activities', devToolsPage);
+    // Here and below we have to wait until the elements are actually created
+    // and visible.
+    await devToolsPage.waitForFunction(async () => {
+      const pendingActivitiesSpan =
+          await devToolsPage.waitFor('//span[text()="Pending activities"]', undefined, undefined, 'xpath');
+      const pendingActiviesRow =
+          await devToolsPage.waitFor('ancestor-or-self::tr', pendingActivitiesSpan, undefined, 'xpath');
+      try {
+        await devToolsPage.clickElement(pendingActivitiesSpan);
+      } catch {
+        return false;
+      }
+      const res = await pendingActiviesRow.evaluate(x => x.classList.toString());
+      return res.includes('selected');
+    });
+    await devToolsPage.page.keyboard.press('ArrowRight');
+    const internalNodeSpan = await devToolsPage.waitFor(
+        '//span[text()="InternalNode"][ancestor-or-self::tr[preceding-sibling::*[1][//span[text()="Pending activities"]]]]',
+        undefined, undefined, 'xpath');
+    const internalNodeRow = await devToolsPage.$('ancestor-or-self::tr', internalNodeSpan, 'xpath');
+    await devToolsPage.waitForFunction(async () => {
+      await devToolsPage.clickElement(internalNodeSpan);
+      const res = await internalNodeRow.evaluate(x => x.classList.toString());
+      return res.includes('selected');
+    });
+    await devToolsPage.page.keyboard.press('ArrowRight');
+    await devToolsPage.waitForFunction(async () => {
+      const pendingActiviesChildren = await devToolsPage.waitForElementsWithTextContent('MediaQueryList');
+      return pendingActiviesChildren.length === 2;
+    });
+  });
+
+  it('Shows the correct number of divs for a detached DOM tree correctly', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/detached-dom-tree.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await setSearchFilter('Detached <div>', devToolsPage);
+    await waitForSearchResultNumber(3, devToolsPage);
+  });
+
+  it('Shows the correct output for an attached iframe', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/attached-iframe.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await setSearchFilter('searchable string', devToolsPage);
+    await waitForSearchResultNumber(1, devToolsPage);
+    // 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'),
+        devToolsPage);
+  });
+
+  // Flaky on win and linux
+  it.skip(
+      '[crbug.com/40238574] Correctly shows multiple retainer paths for an object',
+      async ({devToolsPage, inspectedPage}) => {
+        await inspectedPage.goToResource('memory/multiple-retainers.html');
+        await navigateToMemoryTab(devToolsPage);
+        await takeHeapSnapshot(undefined, devToolsPage);
+        await waitForNonEmptyHeapSnapshotData(devToolsPage);
+        await setSearchFilter('leaking', devToolsPage);
+        await waitForSearchResultNumber(4, devToolsPage);
+        await findSearchResult('\"leaking\"', undefined, devToolsPage);
+
+        await devToolsPage.waitForFunction(async () => {
+          // Wait for all the rows of the data-grid to load.
+          const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data', devToolsPage);
+          return retainerGridElements.length === 9;
+        });
+
+        const sharedInLeakingElementRow = await devToolsPage.waitForFunction(async () => {
+          const results = await getDataGridRows('.retaining-paths-view table.data', devToolsPage);
+          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?.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 devToolsPage.clickElement(
+            sharedInLeakingElementRow /* TODO(crbug.com/1363150): {maxPixelsFromLeft: 10} */);
+        // Expand the data-grid for the shared list
+        await devToolsPage.page.keyboard.press('ArrowRight');
+
+        // check that we found two V8EventListener objects
+        await devToolsPage.waitForFunction(async () => {
+          const pendingActiviesChildren = await devToolsPage.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(e => e.nextSibling)).asElement() as
+            puppeteer.ElementHandle<HTMLElement>;
+        if (!nextRow) {
+          assert.fail('Could not find row below "shared in leaking()" row');
+        }
+        const nextNextRow =
+            (await nextRow.evaluateHandle(e => e.nextSibling)).asElement() as puppeteer.ElementHandle<HTMLElement>;
+        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/40193901] Shows the correct output for a detached iframe', async ({devToolsPage, inspectedPage}) => {
+        await inspectedPage.goToResource('memory/detached-iframe.html');
+        await navigateToMemoryTab(devToolsPage);
+        await takeHeapSnapshot(undefined, devToolsPage);
+        await waitForNonEmptyHeapSnapshotData(devToolsPage);
+        await setSearchFilter('Leak', devToolsPage);
+        await waitForSearchResultNumber(8, devToolsPage);
+        await waitUntilRetainerChainSatisfies(
+            retainerChain => retainerChain.some(({retainerClassName}) => retainerClassName === 'Detached Window'),
+            devToolsPage,
+        );
+      });
+
+  it('Shows a tooltip', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/detached-dom-tree.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await setSearchFilter('Detached <div>', devToolsPage);
+    await waitForSearchResultNumber(3, devToolsPage);
+    await waitUntilRetainerChainSatisfies(retainerChain => {
+      return retainerChain.length > 0 && retainerChain[0].propertyName === 'retaining_wrapper';
+    }, devToolsPage);
+    const rows = await getDataGridRows('.retaining-paths-view table.data', devToolsPage);
+    const propertyNameElement = await rows[0].$('span.property-name');
+    await propertyNameElement!.hover();
+    const el = await devToolsPage.waitFor('div.vbox.flex-auto.no-pointer-events');
+    await devToolsPage.waitFor('.source-code', el);
+
+    await setSearchFilter('system / descriptorarray', devToolsPage);
+    await findSearchResult('system / DescriptorArray', undefined, devToolsPage);
+    const searchResultElement = await devToolsPage.waitFor('.selected.data-grid-data-grid-node span.object-value-null');
+    await searchResultElement!.hover();
+    await devToolsPage.waitFor('.widget .object-popover-footer');
+  });
+
+  it('shows the list of a detached node', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/detached-node.html');
+    await navigateToMemoryTab(devToolsPage);
+    void takeDetachedElementsProfile(devToolsPage);
+    await devToolsPage.waitFor('.detached-elements-view');
+  });
+
+  it('shows the flamechart for an allocation sample', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/allocations.html');
+    await navigateToMemoryTab(devToolsPage);
+    void takeAllocationProfile(devToolsPage);
+    void changeAllocationSampleViewViaDropdown('Chart', devToolsPage);
+    await devToolsPage.waitFor('canvas.flame-chart-canvas');
+  });
+
+  it('shows allocations for an allocation timeline', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/allocations.html');
+    await navigateToMemoryTab(devToolsPage);
+    void takeAllocationTimelineProfile({recordStacks: true}, devToolsPage);
+    await changeViewViaDropdown('Allocation', devToolsPage);
+
+    const header = await devToolsPage.waitForElementWithTextContent('Live Count');
+    const table = await header.evaluateHandle(node => {
+      return node.closest('.data-grid')!;
+    });
+    await devToolsPage.waitFor('.data-grid-data-grid-node', table);
+  });
+
+  it('does not show allocations perspective when stacks not recorded', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/allocations.html');
+    await navigateToMemoryTab(devToolsPage);
+    void takeAllocationTimelineProfile({recordStacks: false}, devToolsPage);
+    const dropdown = await devToolsPage.waitFor('select[aria-label="Perspective"]');
+    await devToolsPage.waitForNoElementsWithTextContent('Allocation', dropdown);
+  });
+
+  it('shows object source links in snapshot', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.evaluate(`
+        class MyTestClass {
+          constructor() {
+            this.z = new Uint32Array(1e6);  // Pull the class to top.
+            this.myFunction = () => 42;
+          }
+        };
+        function* MyTestGenerator() {
+          yield 1;
+        }
+        class MyTestClass2 {}
+        window.myTestClass = new MyTestClass();
+        window.myTestGenerator = MyTestGenerator();
+        window.myTestClass2 = new MyTestClass2();
+        //# sourceURL=my-test-script.js`);
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await setClassFilter('MyTest', devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+
+    const expectedEntries = [
+      {constructor: 'MyTestClass', link: 'my-test-script.js:3'},
+      {constructor: 'MyTestClass', prop: 'myFunction', link: 'my-test-script.js:5'},
+      {constructor: 'MyTestGenerator', link: 'my-test-script.js:8'},
+      {constructor: 'MyTestClass2', link: 'my-test-script.js:11'},
+    ];
+
+    const rows = await getDataGridRows('.data-grid', devToolsPage);
+    for (const entry of expectedEntries) {
+      let row: puppeteer.ElementHandle<Element>|null = null;
+      // Find the row with the desired constructor.
+      for (const r of rows) {
+        const constructorName = await devToolsPage.waitForFunction(() => r.evaluate(e => e.firstChild?.textContent));
+        if (entry.constructor === constructorName) {
+          row = r;
+          break;
+        }
+      }
+      assertNotNullOrUndefined(row);
+      // Expand the constructor sub-tree.
+      await devToolsPage.clickElement(row);
+      await devToolsPage.page.keyboard.press('ArrowRight');
+      // Get the object subtree/child.
+      const {objectElement, objectName} = await devToolsPage.waitForFunction(async () => {
+        const objectElement =
+            await row?.evaluateHandle(e => e.nextSibling) as puppeteer.ElementHandle<HTMLElement>| null;
+        const objectName = await objectElement?.evaluate(e => e.querySelector('.object-value-object')?.textContent);
+        if (!objectName) {
+          return undefined;
+        }
+        return {objectElement, objectName};
+      });
+      let element = objectElement;
+      assertNotNullOrUndefined(element);
+      // Verify we have the object with the matching name.
+      assert.strictEqual(objectName, entry.constructor);
+      // Get the right property of the object if required.
+      if (entry.prop) {
+        // Expand the object.
+        await devToolsPage.clickElement(element);
+        await devToolsPage.page.keyboard.press('ArrowRight');
+        // Try to find the property.
+        element = await devToolsPage.waitForFunction(async () => {
+          let row = element;
+          while (row) {
+            const nextRow = await row.evaluateHandle(e => e.nextSibling) as puppeteer.ElementHandle<HTMLElement>| null;
+            if (!nextRow) {
+              return undefined;
+            }
+            row = nextRow;
+            const text = await row.evaluate(e => e.querySelector('.property-name')?.textContent);
+            // If we did not find any text at all, then we saw all properties. Let us fail/retry here.
+            if (!text) {
+              return undefined;
+            }
+            // If we found the property, we are done.
+            if (text === entry.prop) {
+              return row;
+            }
+            // Continue looking for the property on the next row.
+          }
+          return undefined;
+        });
+        assertNotNullOrUndefined(element);
+      }
+
+      // Verify the link to the source code.
+      const linkText = await devToolsPage.waitForFunction(
+          async () => await element?.evaluate(e => e.querySelector('.devtools-link')?.textContent));
+      assert.strictEqual(linkText, entry.link);
+    }
+  });
+
+  it('Includes backing store size in the shallow size of a JS Set', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/set.html');
+    const sizes = await runJSSetTest(devToolsPage);
+
+    // The Set is reported as containing at least 100 pointers.
+    assert.isTrue(sizes.sizesForSet.shallowSize >= 400);
+    // The Set retains its backing storage.
+    assert.isTrue(
+        sizes.sizesForSet.retainedSize >= sizes.sizesForSet.shallowSize + sizes.sizesForBackingStorage.retainedSize);
+    // The backing storage is reported as zero size.
+    assert.strictEqual(sizes.sizesForBackingStorage.shallowSize, 0);
+    // The backing storage retains 100 strings, which occupy at least 16 bytes each.
+    assert.isTrue(sizes.sizesForBackingStorage.retainedSize >= 1600);
+  });
+
+  it('Computes distances and sizes for WeakMap values correctly', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/weakmap.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await setClassFilter('CustomClass', devToolsPage);
+    assert.strictEqual(5, await getDistanceFromCategoryRow('CustomClass1', devToolsPage));
+    assert.strictEqual(6, await getDistanceFromCategoryRow('CustomClass2', devToolsPage));
+    assert.strictEqual(2, await getDistanceFromCategoryRow('CustomClass3', devToolsPage));
+    assert.strictEqual(8, await getDistanceFromCategoryRow('CustomClass4', devToolsPage));
+    assert.isTrue((await getSizesFromCategoryRow('CustomClass1Key', devToolsPage)).retainedSize >= 2 ** 15);
+    assert.isTrue((await getSizesFromCategoryRow('CustomClass2Key', devToolsPage)).retainedSize >= 2 ** 15);
+    assert.isTrue((await getSizesFromCategoryRow('CustomClass3Key', devToolsPage)).retainedSize < 2 ** 15);
+    assert.isTrue((await getSizesFromCategoryRow('CustomClass4Key', devToolsPage)).retainedSize < 2 ** 15);
+    assert.isTrue((await getSizesFromCategoryRow('CustomClass4Retainer', devToolsPage)).retainedSize >= 2 ** 15);
+  });
+
+  it('Allows ignoring retainers', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/ignoring-retainers.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await setSearchFilter('searchable_string', devToolsPage);
+    await waitForSearchResultNumber(2, devToolsPage);
+    await findSearchResult('"searchable_string"', undefined, devToolsPage);
+    await waitForRetainerChain(['{y}', 'KeyType', 'Window'], devToolsPage);
+    await clickOnContextMenuForRetainer('KeyType', 'Ignore this retainer', devToolsPage);
+    await waitForRetainerChain(['{y}', '{x}', 'Window'], devToolsPage);
+    await clickOnContextMenuForRetainer('x', 'Ignore this retainer', devToolsPage);
+    await waitForRetainerChain(['{y}', '(internal array)[]', 'WeakMap', 'Window'], devToolsPage);
+    await clickOnContextMenuForRetainer('(internal array)[]', 'Ignore this retainer', devToolsPage);
+    await waitForRetainerChain(
+        [
+          '{y}',
+          '{d}',
+          `{${'#'.repeat(130)}, …}`,
+          '{b, irrelevantProperty, <symbol also irrelevant>, "}"}',
+          '{a, extraProp0, extraProp1, extraProp2, extraProp3, …, extraProp6, extraProp7, extraProp8, extraProp9}',
+          'Window',
+        ],
+        devToolsPage);
+    await clickOnContextMenuForRetainer('b', 'Ignore this retainer', devToolsPage);
+    await waitForRetainerChain(['(Internalized strings)', '(GC roots)'], devToolsPage);
+    await restoreIgnoredRetainers(devToolsPage);
+    await waitForRetainerChain(['{y}', 'KeyType', 'Window'], devToolsPage);
+  });
+
+  it('Can filter the summary view', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/filtering.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await setFilterDropdown('Duplicated strings', devToolsPage);
+    await setSearchFilter('"duplicatedKey":"duplicatedValue"', devToolsPage);
+    await waitForSearchResultNumber(2, devToolsPage);
+    await setFilterDropdown('Objects retained by detached DOM nodes', devToolsPage);
+    await getCategoryRow('ObjectRetainedByDetachedDom', undefined, devToolsPage);
+    assert.isNotOk(await getCategoryRow('ObjectRetainedByBothDetachedDomAndConsole', false, devToolsPage));
+    await setFilterDropdown('Objects retained by DevTools Console', devToolsPage);
+    await getCategoryRow('ObjectRetainedByConsole', undefined, devToolsPage);
+    assert.isNotOk(await getCategoryRow('ObjectRetainedByBothDetachedDomAndConsole', false, devToolsPage));
+  });
+
+  it('Groups HTML elements by tag name', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/dom-details.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await setClassFilter('<div>', devToolsPage);
+    assert.strictEqual(3, await getCountFromCategoryRowWithName('<div>', devToolsPage));
+    assert.strictEqual(3, await getCountFromCategoryRowWithName('Detached <div>', devToolsPage));
+    await setSearchFilter('Detached <div data-x="p" data-y="q">', devToolsPage);
+    await waitForSearchResultNumber(1, devToolsPage);
+  });
+
+  it('Groups plain JS objects by interface', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/diff.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await setClassFilter('{a, b, c, d, ', devToolsPage);
+    // Objects should be grouped by interface if there are at least two matching instances.
+    assert.strictEqual(2, await getCountFromCategoryRowWithName('{a, b, c, d, p, q, r}', devToolsPage));
+    assert.isNotOk(await getCategoryRow('{a, b, c, d, e}', /* wait:*/ false, devToolsPage));
+    await inspectedPage.bringToFront();
+    await inspectedPage.page.click('button#update');
+    await devToolsPage.bringToFront();
+    await takeHeapSnapshot('Snapshot 2', devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await changeViewViaDropdown('Comparison', devToolsPage);
+    await setClassFilter('{a, b, c, d, ', devToolsPage);
+    // When comparing, the old snapshot is categorized according to the new one's interfaces,
+    // so the comparison should report only one new object of the following type, not two.
+    assert.strictEqual(1, await getAddedCountFromComparisonRowWithName('{a, b, c, d, e}', devToolsPage));
+    // Only one of these objects remains, so it's no longer a category.
+    assert.isNotOk(await getCategoryRow('{a, b, c, d, p, q, r}', /* wait:*/ false, devToolsPage));
+  });
+
+  it('Groups objects by constructor location', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/duplicated-names.html');
+    await navigateToMemoryTab(devToolsPage);
+    await takeHeapSnapshot(undefined, devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    // TODO: filtering does not work while UI is rendering snapshot.
+    await devToolsPage.drainTaskQueue();
+    await setClassFilter('DuplicatedClassName', devToolsPage);
+    let rows = await devToolsPage.waitForMany('tr.data-grid-data-grid-node', 3);
+    assert.strictEqual(30, await getCountFromCategoryRow(rows[0], devToolsPage));
+    assert.strictEqual(3, await getCountFromCategoryRow(rows[1], devToolsPage));
+    assert.strictEqual(2, await getCountFromCategoryRow(rows[2], devToolsPage));
+    await focusTableRow(rows[0], devToolsPage);
+    await expandFocusedRow(devToolsPage);
+    // TODO: pressing arrowDown does not work while UI is rendering.
+    await devToolsPage.drainTaskQueue();
+    await devToolsPage.page.keyboard.press('ArrowDown');
+    await clickOnContextMenuForRetainer('x', 'Reveal in Summary view', devToolsPage);
+    await waitUntilRetainerChainSatisfies(
+        retainerChain => retainerChain.length > 0 && retainerChain[0].propertyName === 'a', devToolsPage);
+    await inspectedPage.bringToFront();
+    await inspectedPage.page.click('button#update');
+    await devToolsPage.bringToFront();
+    await takeHeapSnapshot('Snapshot 2', devToolsPage);
+    await waitForNonEmptyHeapSnapshotData(devToolsPage);
+    await changeViewViaDropdown('Comparison', devToolsPage);
+    await setClassFilter('DuplicatedClassName', devToolsPage);
+    rows = await devToolsPage.waitForMany('tr.data-grid-data-grid-node', 3);
+    assert.strictEqual(5, await getAddedCountFromComparisonRow(rows[0], devToolsPage));
+    assert.strictEqual(1, await getRemovedCountFromComparisonRow(rows[0], devToolsPage));
+    assert.strictEqual(1, await getAddedCountFromComparisonRow(rows[1], devToolsPage));
+    assert.strictEqual(10, await getRemovedCountFromComparisonRow(rows[1], devToolsPage));
+    assert.strictEqual(0, await getAddedCountFromComparisonRow(rows[2], devToolsPage));
+    assert.strictEqual(2, await getRemovedCountFromComparisonRow(rows[2], devToolsPage));
+  });
+});
+
+describe('The Memory Panel with show-option-tp-expose-internals-in-heap-snapshot experiment', () => {
+  setup({dockingMode: 'undocked', enabledDevToolsExperiments: ['show-option-tp-expose-internals-in-heap-snapshot']});
+
+  it('Does not include backing store size in the shallow size of a JS Set', async ({devToolsPage, inspectedPage}) => {
+    await inspectedPage.goToResource('memory/set.html');
+    await navigateToMemoryTab(devToolsPage);
+    await checkExposeInternals(devToolsPage);
+    const sizes = await runJSSetTest(devToolsPage);
+
+    // The Set object is small, regardless of the contained content.
+    assert.isTrue(sizes.sizesForSet.shallowSize <= 100);
+    // The Set retains its backing storage.
+    // Note: 16 bytes is added to retainedSize to account for rounding present in the UI layer.
+    assert.isTrue(
+        sizes.sizesForSet.retainedSize + 16 >=
+        sizes.sizesForSet.shallowSize + sizes.sizesForBackingStorage.retainedSize);
+    // The backing storage contains 100 items, which occupy at least one pointer per item.
+    assert.isTrue(sizes.sizesForBackingStorage.shallowSize >= 400);
+    // The backing storage retains 100 strings, which occupy at least 16 bytes each.
+    assert.isTrue(sizes.sizesForBackingStorage.retainedSize >= sizes.sizesForBackingStorage.shallowSize + 1600);
+  });
+});