"use strict";
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
Object.defineProperty(exports, "__esModule", { value: true });
exports.SELECTED_RESULT = exports.MEMORY_TAB_ID = void 0;
exports.navigateToMemoryTab = navigateToMemoryTab;
exports.takeDetachedElementsProfile = takeDetachedElementsProfile;
exports.takeAllocationProfile = takeAllocationProfile;
exports.takeAllocationTimelineProfile = takeAllocationTimelineProfile;
exports.takeHeapSnapshot = takeHeapSnapshot;
exports.waitForHeapSnapshotData = waitForHeapSnapshotData;
exports.waitForNonEmptyHeapSnapshotData = waitForNonEmptyHeapSnapshotData;
exports.getDataGridRows = getDataGridRows;
exports.setClassFilter = setClassFilter;
exports.setSearchFilter = setSearchFilter;
exports.waitForSearchResultNumber = waitForSearchResultNumber;
exports.findSearchResult = findSearchResult;
exports.checkRetainerChainSatisfies = checkRetainerChainSatisfies;
exports.waitUntilRetainerChainSatisfies = waitUntilRetainerChainSatisfies;
exports.appearsInOrder = appearsInOrder;
exports.waitForRetainerChain = waitForRetainerChain;
exports.changeViewViaDropdown = changeViewViaDropdown;
exports.changeAllocationSampleViewViaDropdown = changeAllocationSampleViewViaDropdown;
exports.focusTableRowWithName = focusTableRowWithName;
exports.focusTableRow = focusTableRow;
exports.expandFocusedRow = expandFocusedRow;
exports.getSizesFromSelectedRow = getSizesFromSelectedRow;
exports.getCategoryRow = getCategoryRow;
exports.getSizesFromCategoryRow = getSizesFromCategoryRow;
exports.getDistanceFromCategoryRow = getDistanceFromCategoryRow;
exports.getCountFromCategoryRowWithName = getCountFromCategoryRowWithName;
exports.getCountFromCategoryRow = getCountFromCategoryRow;
exports.getAddedCountFromComparisonRowWithName = getAddedCountFromComparisonRowWithName;
exports.getAddedCountFromComparisonRow = getAddedCountFromComparisonRow;
exports.getRemovedCountFromComparisonRow = getRemovedCountFromComparisonRow;
exports.clickOnContextMenuForRetainer = clickOnContextMenuForRetainer;
exports.restoreIgnoredRetainers = restoreIgnoredRetainers;
exports.setFilterDropdown = setFilterDropdown;
const chai_1 = require("chai");
const NEW_HEAP_SNAPSHOT_BUTTON = 'devtools-button[aria-label="Take heap snapshot"]';
const MEMORY_PANEL_CONTENT = 'div[aria-label="Memory panel"]';
const PROFILE_TREE_SIDEBAR = 'div.profiles-tree-sidebar';
exports.MEMORY_TAB_ID = '#tab-heap-profiler';
const CLASS_FILTER_INPUT = 'div[aria-placeholder="Filter by class"]';
exports.SELECTED_RESULT = '#profile-views table.data tr.data-grid-data-grid-node.revealed.parent.selected';
async function navigateToMemoryTab(devToolsPage) {
    await devToolsPage.click(exports.MEMORY_TAB_ID);
    await devToolsPage.waitFor(MEMORY_PANEL_CONTENT);
    await devToolsPage.waitFor(PROFILE_TREE_SIDEBAR);
}
async function takeDetachedElementsProfile(devToolsPage) {
    await devToolsPage.click('xpath///label[text()="Detached elements"]');
    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');
}
async function takeAllocationProfile(devToolsPage) {
    await devToolsPage.click('xpath///label[text()="Allocation sampling"]');
    await devToolsPage.click('devtools-button[aria-label="Start heap profiling"]');
    await new Promise(r => setTimeout(r, 200));
    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');
}
async function takeAllocationTimelineProfile({ recordStacks } = {
    recordStacks: false,
}, devToolsPage) {
    await devToolsPage.click('xpath///label[text()="Allocations on timeline"]');
    if (recordStacks) {
        await devToolsPage.click('[title="Allocation stack traces (more overhead)"]');
    }
    await devToolsPage.click('devtools-button[aria-label="Start recording heap profile"]');
    await new Promise(r => setTimeout(r, 200));
    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');
}
async function takeHeapSnapshot(name = 'Snapshot 1', 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;
    });
}
async function waitForHeapSnapshotData(devToolsPage) {
    await devToolsPage.waitFor('#profile-views');
    await devToolsPage.waitFor('#profile-views .data-grid');
    const rowCountMatches = async () => {
        const rows = await getDataGridRows('#profile-views table.data', devToolsPage);
        if (rows.length > 0) {
            return rows;
        }
        return undefined;
    };
    return await devToolsPage.waitForFunction(rowCountMatches);
}
async function waitForNonEmptyHeapSnapshotData(devToolsPage) {
    const rows = await waitForHeapSnapshotData(devToolsPage);
    chai_1.assert.isTrue(rows.length > 0);
}
async function getDataGridRows(selector, devToolsPage) {
    // The grid in Memory Tab contains a tree
    const grid = await devToolsPage.waitFor(selector);
    return await devToolsPage.$$('.data-grid-data-grid-node', grid);
}
async function setClassFilter(text, devToolsPage) {
    const classFilter = await devToolsPage.waitFor(CLASS_FILTER_INPUT);
    await classFilter.focus();
    void devToolsPage.pasteText(text);
}
async function setSearchFilter(text, devToolsPage) {
    const grid = await devToolsPage.waitFor('#profile-views table.data');
    await grid.focus();
    await devToolsPage.pressKey('f', { control: true });
    const SEARCH_QUERY = '[aria-label="Find"]';
    const inputElement = await devToolsPage.waitFor(SEARCH_QUERY);
    chai_1.assert.isOk(inputElement, 'Unable to find search input field');
    await inputElement.focus();
    await inputElement.type(text);
}
async function waitForSearchResultNumber(results, devToolsPage) {
    const findMatch = async () => {
        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 devToolsPage.waitForFunction(findMatch);
}
/**
 *
 * @param searchResult
 * @param searchMatch Leave undefined if you want to go over all instances
 * @param devToolsPage
 */
async function findSearchResult(searchResult, searchMatch, devToolsPage) {
    await devToolsPage.waitForFunction(async () => {
        if (!searchMatch) {
            const match = await devToolsPage.waitFor('#profile-views table.data');
            const result = await devToolsPage.$textContent(searchResult, match);
            if (result) {
                return true;
            }
        }
        else {
            const matches = await devToolsPage.waitFor('.search-results-matches');
            const matchesText = await matches.evaluate(element => {
                return element.textContent;
            });
            if (typeof searchMatch === 'string' && matchesText === searchMatch) {
                return true;
            }
            if (typeof searchMatch !== 'string' && searchMatch.test(matchesText)) {
                return true;
            }
        }
        await devToolsPage.click('[aria-label="Show next result"]');
        return;
    });
    const match = await devToolsPage.waitFor('#profile-views');
    await devToolsPage.waitForElementWithTextContent(searchResult, match);
}
const normalizRetainerName = (retainerName) => {
    // Retainers starting with `Window /` might have host information in their
    // name, including the port, so we need to strip that. We can't distinguish
    // Window from Window / either, because on Mac it is often just Window.
    if (retainerName.startsWith('Window /')) {
        return 'Window';
    }
    // Retainers including double-colons :: are names from the C++ implementation
    // exposed through V8's gn arg `cppgc_enable_object_names`; these should be
    // considered implementation details, so we normalize them.
    if (retainerName.includes('::')) {
        if (retainerName.startsWith('Detached')) {
            return 'Detached InternalNode';
        }
        return 'InternalNode';
    }
    return retainerName;
};
async function checkRetainerChainSatisfies(p, devToolsPage) {
    // Give some time for the expansion to finish.
    const retainerGridElements = await getDataGridRows('.retaining-paths-view table.data', devToolsPage);
    const retainerChain = [];
    for (let i = 0; i < retainerGridElements.length; ++i) {
        const retainer = retainerGridElements[i];
        const propertyName = await retainer.$eval('span.property-name', el => el.textContent);
        const rawRetainerClassName = await retainer.$eval('span.value', el => el.textContent);
        chai_1.assert.isOk(propertyName, 'Could not get retainer name');
        chai_1.assert.isOk(rawRetainerClassName, 'Could not get retainer value');
        const retainerClassName = normalizRetainerName(rawRetainerClassName);
        retainerChain.push({ propertyName, retainerClassName });
        if (await retainer.evaluate(el => !el.classList.contains('expanded'))) {
            // Only follow the shortest retainer chain to the end. This relies on
            // the retainer view behavior that auto-expands the shortest retaining
            // chain.
            break;
        }
    }
    return p(retainerChain);
}
async function waitUntilRetainerChainSatisfies(p, devToolsPage) {
    await devToolsPage.waitForFunction(checkRetainerChainSatisfies.bind(null, p, devToolsPage));
}
function appearsInOrder(targetArray, inputArray) {
    let i = 0;
    let j = 0;
    if (inputArray.length > targetArray.length) {
        return false;
    }
    if (inputArray === targetArray) {
        return true;
    }
    while (i < targetArray.length && j < inputArray.length) {
        if (inputArray[j] === targetArray[i]) {
            j++;
        }
        i++;
    }
    if (j === inputArray.length) {
        return true;
    }
    return false;
}
async function waitForRetainerChain(expectedRetainers, devToolsPage) {
    await devToolsPage.waitForFunction(checkRetainerChainSatisfies.bind(null, retainerChain => {
        const actual = retainerChain.map(e => e.retainerClassName);
        return appearsInOrder(actual, expectedRetainers);
    }, devToolsPage));
}
async function changeViewViaDropdown(newPerspective, devToolsPage) {
    const perspectiveDropdownSelector = 'select[aria-label="Perspective"]';
    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}`);
    }
    await dropdown.select(optionValue);
}
async function changeAllocationSampleViewViaDropdown(newPerspective, devToolsPage) {
    const perspectiveDropdownSelector = 'select[aria-label="Profile view mode"]';
    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}`);
    }
    await dropdown.select(optionValue);
}
async function focusTableRowWithName(text, devToolsPage) {
    const row = await devToolsPage.waitFor(`//span[text()="${text}"]/ancestor::tr`, undefined, undefined, 'xpath');
    await focusTableRow(row, devToolsPage);
}
async function focusTableRow(row, devToolsPage) {
    // Click in a numeric cell, to avoid accidentally clicking a link.
    await devToolsPage.click('.numeric-column', {
        root: row,
    });
}
async function expandFocusedRow(devToolsPage) {
    await devToolsPage.pressKey('ArrowRight');
    await devToolsPage.waitFor('.selected.data-grid-data-grid-node.expanded');
}
function parseByteString(str) {
    const number = parseFloat(str);
    if (str.endsWith('kB')) {
        return number * 1000;
    }
    if (str.endsWith('MB')) {
        return number * 1000 * 1000;
    }
    if (str.endsWith('GB')) {
        return number * 1000 * 1000 * 1000;
    }
    return number;
}
async function getSizesFromRow(row, devToolsPage) {
    const numericData = await devToolsPage.$$('.numeric-column>.profile-multiple-values>span', row);
    chai_1.assert.lengthOf(numericData, 4);
    function readNumber(e) {
        return e.textContent;
    }
    const shallowSize = parseByteString(await numericData[0].evaluate(readNumber));
    const retainedSize = parseByteString(await numericData[2].evaluate(readNumber));
    chai_1.assert.isTrue(retainedSize >= shallowSize);
    return { shallowSize, retainedSize };
}
async function getSizesFromSelectedRow(devToolsPage) {
    const row = await devToolsPage.waitFor('.selected.data-grid-data-grid-node');
    return await getSizesFromRow(row, devToolsPage);
}
async function getCategoryRow(text, wait = true, devToolsPage) {
    const selector = `//td[text()="${text}"]/ancestor::tr`;
    const row = await (wait ? devToolsPage.waitFor(selector, undefined, undefined, 'xpath') :
        devToolsPage.$(selector, undefined, 'xpath'));
    return row;
}
async function getSizesFromCategoryRow(text, devToolsPage) {
    const row = await getCategoryRow(text, undefined, devToolsPage);
    return await getSizesFromRow(row, devToolsPage);
}
async function getDistanceFromCategoryRow(text, devToolsPage) {
    const row = await getCategoryRow(text, undefined, devToolsPage);
    const numericColumns = await devToolsPage.$$('.numeric-column', row);
    return await numericColumns[0].evaluate(e => parseInt(e.textContent, 10));
}
async function getCountFromCategoryRowWithName(text, devToolsPage) {
    const row = await getCategoryRow(text, undefined, devToolsPage);
    return await getCountFromCategoryRow(row, devToolsPage);
}
async function getCountFromCategoryRow(row, devToolsPage) {
    const countSpan = await devToolsPage.waitFor('.objects-count', row);
    return await countSpan.evaluate(e => parseInt(e.textContent.substring(1), 10));
}
async function getAddedCountFromComparisonRowWithName(text, devToolsPage) {
    const row = await getCategoryRow(text, undefined, devToolsPage);
    return await getAddedCountFromComparisonRow(row, devToolsPage);
}
async function getAddedCountFromComparisonRow(row, devToolsPage) {
    const addedCountCell = await devToolsPage.waitFor('.addedCount-column', row);
    const countText = await addedCountCell.evaluate(e => e.textContent);
    return parseByteString(countText);
}
async function getRemovedCountFromComparisonRow(row, devToolsPage) {
    const addedCountCell = await devToolsPage.waitFor('.removedCount-column', row);
    const countText = await addedCountCell.evaluate(e => e.textContent);
    return parseByteString(countText);
}
async function clickOnContextMenuForRetainer(retainerName, menuItem, devToolsPage) {
    const retainersPane = await devToolsPage.waitFor('.retaining-paths-view');
    await devToolsPage.click(`xpath///span[text()="${retainerName}"]`, {
        root: retainersPane,
        clickOptions: {
            button: 'right',
            // Push the click right a bit further to avoid the disclosure triangle.
            offset: { x: 35, y: 0 },
        },
    });
    await devToolsPage.click(`aria/${menuItem}`);
}
async function restoreIgnoredRetainers(devToolsPage) {
    await devToolsPage.click('devtools-button[aria-label="Restore ignored retainers"]');
}
async function setFilterDropdown(filter, devToolsPage) {
    const select = await devToolsPage.waitFor('devtools-toolbar select[aria-label="Filter"]');
    await select.select(filter);
}
//# sourceMappingURL=memory-helpers.js.map