@memlab/e2e
Version:
memlab browser E2E interaction libraries
289 lines (288 loc) • 10.7 kB
JavaScript
;
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @oncall memory_lab
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@memlab/core");
const fs_1 = __importDefault(require("fs"));
const core_2 = require("@memlab/core");
const BaseOperation_1 = __importDefault(require("./operations/BaseOperation"));
const InteractionUtils_1 = __importDefault(require("./operations/InteractionUtils"));
const core_3 = require("@memlab/core");
function checkLastSnapshotChunk(chunk) {
const regex = /\}\s*$/;
if (!regex.test(chunk)) {
core_2.utils.throwError(new Error('resolved `HeapProfiler.takeHeapSnapshot` before writing the last chunk'));
}
}
// get URL parameter for a specific step
function getURLParameter(tab, visitPlan) {
var _a, _b;
const params = visitPlan.URLParameters;
const stepParams = (_b = (_a = tab.urlParams) === null || _a === void 0 ? void 0 : _a.map(({ name, value }) => [name, value].map(encodeURIComponent).join('=')).join('&')) !== null && _b !== void 0 ? _b : '';
let ret = params +
(params.length > 0 && stepParams.length > 0 ? '&' : '') +
stepParams;
if (ret.length > 0 && !ret.startsWith('?')) {
ret = '?' + ret;
}
return ret;
}
function compareURL(page, url) {
if (!core_2.config.verbose) {
return;
}
const actual = unescape(page.url());
url = unescape(url);
if (!core_2.utils.isURLEqual(url, actual)) {
core_2.info.warning('URL changed:');
core_2.info.lowLevel(` Expected: ${url}`);
core_2.info.lowLevel('----');
core_2.info.lowLevel(` Actual: ${actual}`);
core_2.info.lowLevel('');
}
}
function logTabProgress(i, visitPlan) {
const len = visitPlan.tabsOrder.length;
const tab = visitPlan.tabsOrder[i];
const progress = `[${i + 1}/${len}]`;
const tabType = tab.type ? `(${tab.type})` : '';
const msg = `${progress} visiting ${tab.name} ${tabType}`;
if (core_2.config.verbose) {
core_2.info.topLevel(msg);
}
core_2.info.topLevel(core_2.serializer.summarizeTabsOrder(visitPlan.tabsOrder, {
color: true,
progress: i,
}));
core_2.browserInfo.addMarker(`[memlab]: ${msg}`);
}
function serializeVisitPlan(visitPlan) {
fs_1.default.writeFileSync(core_2.config.snapshotSequenceFile, JSON.stringify(visitPlan.tabsOrder, null, 2), 'UTF-8');
}
function logMetaData(visitPlan, opt = {}) {
// save the visiting info to disk
serializeVisitPlan(visitPlan);
// save the run meta info to disk
const runMeta = {
app: core_2.config.targetApp,
type: visitPlan.type,
interaction: core_2.config.targetTab,
browserInfo: core_2.browserInfo,
};
core_1.runInfoUtils.runMetaInfoManager.saveRunMetaInfo(runMeta);
// additional post processing of collected data
if (opt.final) {
core_2.config.runningMode.postProcessData(visitPlan);
}
}
function setPermissions(page, origin) {
return __awaiter(this, void 0, void 0, function* () {
if (origin === '' || origin == null) {
return;
}
const browser = page.browser();
const context = browser.defaultBrowserContext();
yield context.overridePermissions(origin, core_2.config.grantedPermissions);
});
}
// check if the URL is correct
function checkURL(page, intendedURL) {
compareURL(page, intendedURL);
}
function injectPageReloadChecker(page) {
return __awaiter(this, void 0, void 0, function* () {
yield page.evaluate(() => {
// @ts-expect-error TODO: add Window shim type
window.__memlab_check_reload = 1;
});
});
}
function checkPageReload(page) {
return __awaiter(this, void 0, void 0, function* () {
// @ts-expect-error TODO: add Window shim type
const flag = yield page.evaluate(() => window.__memlab_check_reload);
if (flag !== 1) {
core_2.utils.haltOrThrow('The page is reloaded. MemLab cannot analyze heap across page reloads. ' +
'Please remove window.reload() calls, page.goto() calls, ' +
'or any reload logic.');
}
});
}
function maybeWaitForConsoleInput(stepId) {
return __awaiter(this, void 0, void 0, function* () {
if (core_2.config.isManualDebug) {
yield core_2.info.waitForConsole(`Press Enter (or Return) to continue step-${stepId}:`);
}
});
}
function applyAsyncWithGuard(f, self, args,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
exceptionHandler = (_ex) => {
/*noop*/
}) {
return __awaiter(this, void 0, void 0, function* () {
let ret;
try {
ret = yield f.apply(self, args);
}
catch (ex) {
yield exceptionHandler(core_2.utils.getError(ex));
}
return ret;
});
}
function applyAsyncWithRetry(f, self, args, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
options.retry = options.retry || 0;
const retry = options.retry;
const exceptionHandler = (ex) => __awaiter(this, void 0, void 0, function* () {
if (retry <= 0 || core_2.config.verbose) {
// if the browser UI is enabled, MemLab should wait for a while
// so we can have a chance to manually inspect the page
if (core_2.config.openDevtoolsConsole) {
yield InteractionUtils_1.default.waitFor(core_2.config.delayBeforeExitUponException);
}
core_2.utils.haltOrThrow(core_2.utils.getError(ex), {
printCallback: () => {
core_2.info.warning('interaction fail');
},
});
}
core_2.info.warning(`interaction fail, making ${retry} more attempt(s)...`);
if (!!options.delayBeforeRetry && options.delayBeforeRetry > 0) {
yield InteractionUtils_1.default.waitFor(options.delayBeforeRetry);
}
if (options.retry) {
options.retry--;
}
yield applyAsyncWithRetry(f, self, args, options);
});
yield applyAsyncWithGuard(f, self, args, exceptionHandler);
});
}
function clearConsole(page) {
return __awaiter(this, void 0, void 0, function* () {
if (core_2.config.clearConsole) {
yield page.evaluate(() => {
try {
console.clear();
}
catch (_a) {
// do nothing
}
});
}
});
}
function dispatchOperation(page, operation, opArgs) {
return __awaiter(this, void 0, void 0, function* () {
if (!(operation instanceof BaseOperation_1.default)) {
throw core_2.utils.haltOrThrow(`unknown operation: ${operation}`);
}
yield operation.do(page, opArgs);
});
}
function waitExtraForTab(tabInfo) {
return __awaiter(this, void 0, void 0, function* () {
let delay = 0;
const mode = core_2.config.runningMode;
if (tabInfo.type === 'target') {
if (!mode.shouldExtraWaitForTarget(tabInfo)) {
return;
}
delay += core_2.config.extraWaitingForTarget;
}
else if (tabInfo.type === 'final') {
if (!mode.shouldExtraWaitForFinal(tabInfo)) {
return;
}
delay += core_2.config.extraWaitingForFinal;
}
// wait for extra time
if (delay > 0) {
core_2.info.overwrite(`wait extra ${delay / 1000}s for ${tabInfo.type} page...`);
yield InteractionUtils_1.default.waitFor(delay);
}
});
}
function getNavigationHistoryLength(page) {
return __awaiter(this, void 0, void 0, function* () {
return yield page.evaluate(() => {
let ret = -1;
try {
ret = history.length;
}
catch (e) {
// do nothing
}
return ret;
});
});
}
// const allocFile = path.join(config.curDataDir, 'Heap-alloc.json');
// await startTrackingHeapAllocation(page, allocFile);
function startTrackingHeapAllocation(page, file) {
return __awaiter(this, void 0, void 0, function* () {
const heap = '';
fs_1.default.writeFileSync(file, heap, 'UTF-8');
core_2.info.lowLevel('tracking heap allocation...');
const cdpSession = yield page.target().createCDPSession();
cdpSession.on('HeapProfiler.addHeapSnapshotChunk', data => {
fs_1.default.appendFileSync(file, data.chunk, 'UTF-8');
});
yield cdpSession.send('HeapProfiler.startTrackingHeapObjects', {
trackAllocations: true,
});
yield InteractionUtils_1.default.waitFor(30000);
return cdpSession;
});
}
function getScenarioAppName(scenario = null) {
if (!scenario || !scenario.app) {
return getScenarioDefaultAppName();
}
return scenario.app();
}
function getScenarioDefaultAppName() {
return 'default-app-for-scenario';
}
exports.default = (0, core_3.setInternalValue)({
applyAsyncWithGuard,
applyAsyncWithRetry,
checkLastSnapshotChunk,
checkPageReload,
checkURL,
clearConsole,
compareURL,
dispatchOperation,
getNavigationHistoryLength,
getScenarioAppName,
getURLParameter,
injectPageReloadChecker,
logMetaData,
logTabProgress,
maybeWaitForConsoleInput,
serializeVisitPlan,
setPermissions,
startTrackingHeapAllocation,
waitExtraForTab,
}, __filename, core_3.constant.internalDir);