@alithanar/react-automation-profiler
Version:
Automated React profiling and data visualization using React's Profiler API, Puppeteer, and D3.
360 lines • 14.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const express_1 = __importDefault(require("express"));
const promises_1 = __importDefault(require("fs/promises"));
const html_minifier_terser_1 = require("html-minifier-terser");
const jsdom_1 = __importDefault(require("jsdom"));
const js_yaml_1 = __importDefault(require("js-yaml"));
const puppeteer_1 = __importDefault(require("puppeteer"));
const util_1 = require("../utils/util");
const interfaces_1 = require("../interfaces");
var Actions;
(function (Actions) {
Actions["CLICK"] = "click";
Actions["FOCUS"] = "focus";
Actions["HOVER"] = "hover";
Actions["GOTO"] = "goto";
Actions["WAIT"] = "wait";
})(Actions || (Actions = {}));
async function automate(props, resultsStorage) {
const { automationCount, averageOf, cwd, includeMount, isServerReady, packagePath, serverPort, url, headless, browserArgs, output, preloadFilePath, cookies, scenarios, } = props;
const MOUNT = 'Mount';
const browser = await puppeteer_1.default.launch({ headless, args: browserArgs });
async function initPage() {
const newPage = await browser.newPage();
if (preloadFilePath) {
const preloadFile = await promises_1.default.readFile(preloadFilePath, 'utf8');
await newPage.evaluateOnNewDocument(preloadFile);
}
if (cookies) {
newPage.setCookie(...cookies);
}
return newPage;
}
async function launchWithUrl() {
await page.goto(url);
await page.setViewport({
deviceScaleFactor: 1,
height: 1080,
width: 1920,
});
}
let page = await initPage();
let errorMessage = '';
async function exportResults() {
switch (output) {
case interfaces_1.OutputType.CHART: {
await appendJsonToHTML();
return null;
}
case interfaces_1.OutputType.JSON: {
return exportJsonBundle();
}
}
}
async function appendJsonToHTML() {
var _a;
const { JSDOM } = jsdom_1.default;
try {
const contents = await promises_1.default.readFile(`${packagePath}/index.html`, 'utf8');
const { document } = new JSDOM(`${contents}`).window;
(_a = document.querySelectorAll('.json')) === null || _a === void 0 ? void 0 : _a.forEach((item) => item.remove());
const results = resultsStorage.getAllResults();
Object.keys(results).forEach((flowKey) => {
const jsonScript = document.createElement('script');
const result = results[flowKey][0];
const idArr = result.id.split('-');
jsonScript.id = `${idArr[0]}-${idArr[1]}`;
jsonScript.classList.add('json');
jsonScript.type = 'application/json';
jsonScript.innerHTML = JSON.stringify(result);
document.body.appendChild(jsonScript);
});
await promises_1.default.writeFile(`${packagePath}/index.html`, document.documentElement.outerHTML);
await generateExport(document);
}
catch (e) {
errorMessage = 'Could not append JSON data to HTML file.';
(0, util_1.printMessage)("ERROR" /* ERROR */, { e: e, log: errorMessage });
}
}
function exportJsonBundle() {
return resultsStorage.getAllResults();
}
async function calculateAverage() {
try {
const results = resultsStorage.getAllResults();
const flows = Object.keys(results);
flows.forEach(async (flowKey, i) => {
const sumLogs = [];
let sumNumberOfInteractions = 0;
const flowResults = results[flowKey];
flowResults.forEach((result) => {
const { logs, numberOfInteractions } = result;
for (const [index, log] of logs.entries()) {
if (typeof sumLogs[index] !== 'object' || sumLogs[index] === null)
sumLogs[index] = {
actualDuration: 0,
baseDuration: 0,
commitTime: 0,
id: '',
interactions: new Set(),
phase: '',
startTime: 0,
};
sumLogs[index].actualDuration += log.actualDuration;
sumLogs[index].baseDuration += log.baseDuration;
sumLogs[index].commitTime += log.commitTime;
sumLogs[index].startTime += log.startTime;
if (!sumLogs[index].id)
sumLogs[index].id = log.id;
if (!sumLogs[index].phase)
sumLogs[index].phase = log.phase;
}
sumNumberOfInteractions += numberOfInteractions;
});
const averageData = {
logs: sumLogs.map((log) => ({
actualDuration: log.actualDuration / averageOf,
baseDuration: log.baseDuration / averageOf,
commitTime: log.commitTime / averageOf,
id: log.id,
interactions: log.interactions,
phase: log.phase,
startTime: log.startTime / averageOf,
})),
numberOfInteractions: sumNumberOfInteractions / averageOf,
id: (0, util_1.getFileName)(flowKey),
};
resultsStorage.removeResultsByKey(flowKey);
resultsStorage.appendResult(`average-${flowKey}`, averageData);
if (averageOf === automationCount && i === flows.length - 1) {
return await exportResults();
}
return averageData;
});
return null;
}
catch (e) {
errorMessage = 'An error occurred while calculating averages.';
(0, util_1.printMessage)("ERROR" /* ERROR */, { e: e, log: errorMessage });
return null;
}
}
async function setIsLoggingEnabled(isLoggingEnabled) {
await page.evaluate((isEnabled) => {
window.isProfilingEnabled = isEnabled;
}, isLoggingEnabled);
}
async function collectLogs({ label, numberOfInteractions = 0, }) {
if (label !== MOUNT || (label === MOUNT && includeMount)) {
const logs = await page.evaluate(() => window.profiler);
if ((logs === null || logs === void 0 ? void 0 : logs.length) === 0)
return false;
resultsStorage.appendResult(label, {
logs,
numberOfInteractions,
id: (0, util_1.getFileName)(label),
});
}
await page.evaluate(() => {
window.profiler = [];
window.isProfilingEnabled = true;
});
return true;
}
async function generateExport(document) {
var _a;
(_a = document.querySelector('#export')) === null || _a === void 0 ? void 0 : _a.remove();
try {
await promises_1.default.writeFile(`${packagePath}/export.html`, (0, html_minifier_terser_1.minify)(document.documentElement.outerHTML, {
collapseBooleanAttributes: true,
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
removeAttributeQuotes: true,
}));
}
catch (e) {
errorMessage = 'An error occurred while generating a new export file.';
(0, util_1.printMessage)("ERROR" /* ERROR */, { e: e, log: errorMessage });
}
}
async function handleActions(actions) {
for (const action of actions) {
const [actionType, ...selector] = action.split(' ');
if (Object.values(Actions).includes(actionType)) {
const selectorStr = selector.join(' ');
(0, util_1.printMessage)("NOTICE" /* NOTICE */, {
log: `${actionType}: ${selectorStr}`,
});
switch (actionType) {
case Actions.CLICK:
await page.click(selectorStr);
break;
case Actions.FOCUS:
await page.focus(selectorStr);
break;
case Actions.HOVER:
await page.hover(selectorStr);
break;
case Actions.GOTO:
await page.goto(selectorStr);
break;
case Actions.WAIT:
await page.waitForTimeout(parseInt(selectorStr));
break;
}
}
else {
errorMessage = 'One or more action types provided was not valid.';
throw (0, util_1.printMessage)("ERROR" /* ERROR */, { log: errorMessage });
}
}
}
async function readAutomationFile() {
let flows;
try {
flows = (js_yaml_1.default.load(await promises_1.default.readFile(`${cwd}/react.automation.yml`, 'utf8')));
return flows;
}
catch (_a) {
flows = (js_yaml_1.default.load(await promises_1.default.readFile(`${cwd}/react.automation.yaml`, 'utf8')));
return flows;
}
}
async function runFlows() {
if (scenarios === null || scenarios === void 0 ? void 0 : scenarios.length) {
(0, util_1.printMessage)("NOTICE" /* NOTICE */, {
log: `Running programmatic flows.\n`,
});
await runScenarios(scenarios);
}
else {
(0, util_1.printMessage)("NOTICE" /* NOTICE */, {
log: `Running YAML flows.\n`,
});
await runYAMLFlows();
}
}
async function runScenarios(scenarios) {
var _a;
try {
let attempts = 0;
for (let i = 0; i < scenarios.length; i++) {
const scenario = scenarios[i];
if (scenario.shouldSkip) {
(0, util_1.printMessage)("NOTICE" /* NOTICE */, {
log: `Scenario "${scenario.id}": skipped`,
});
continue;
}
if (i > 0) {
page = await initPage();
}
await launchWithUrl();
(0, util_1.printMessage)("NOTICE" /* NOTICE */, {
log: `Scenario "${scenario.id}": attempt #${attempts + 1}`,
});
await setIsLoggingEnabled(false);
await ((_a = scenario.onBefore) === null || _a === void 0 ? void 0 : _a.call(scenario, page));
await setIsLoggingEnabled(true);
await scenario.onProfile(page);
const success = await collectLogs({
label: scenario.id,
numberOfInteractions: 0, // TODO
});
if (!success) {
if (attempts++ < 3)
i -= 1;
else
(0, util_1.printMessage)("NOTICE" /* NOTICE */, {
log: `Automation flow "${scenario.id}" did not produce any renders.\n`,
});
}
if (!page.isClosed()) {
page.close();
}
}
}
catch (error) {
handleRunFlowsError(error);
}
}
async function runYAMLFlows() {
try {
const flows = await readAutomationFile();
if (!flows) {
return;
}
const keys = Object.keys(flows);
let attempts = 0;
for (let i = 0; i < keys.length; i++) {
const actions = flows[keys[i]];
await handleActions(actions);
const success = await collectLogs({
label: keys[i],
numberOfInteractions: actions.length,
});
if (!success) {
if (attempts++ < 3)
i -= 1;
else
(0, util_1.printMessage)("NOTICE" /* NOTICE */, {
log: `Automation flow "${keys[i]}" did not produce any renders.\n`,
});
}
}
}
catch (error) {
handleRunFlowsError(error);
}
}
function handleRunFlowsError(error) {
const isErrorObjectEmpty = Object.keys(error).length === 0;
const description = isErrorObjectEmpty &&
` This was likely caused by one of these issues:
- The react.automation YAML file could not be found at the root of your repo.
- The react.automation file is using a selector that does not exist.`;
errorMessage = `An error occurred while trying to run automation flows.${description ? description : ''}`;
(0, util_1.printMessage)("ERROR" /* ERROR */, {
e: error ? error : new Error('Unspecified error'),
log: errorMessage,
});
}
async function startServer() {
const app = (0, express_1.default)();
app.use(express_1.default.static(packagePath));
app.get('/', (_, res) => res.sendFile(`${packagePath}/index.html`));
app.listen(serverPort);
}
if (!scenarios) {
await launchWithUrl();
}
await collectLogs({ label: MOUNT });
await runFlows();
await browser.close();
let results = {};
if (averageOf > 1 && automationCount === averageOf) {
results = await calculateAverage();
}
else if (averageOf === 1) {
results = await exportResults();
}
if (!isServerReady &&
automationCount === averageOf &&
output === interfaces_1.OutputType.CHART)
await startServer();
if (errorMessage) {
(0, util_1.printMessage)("ERROR" /* ERROR */, {
log: 'Automation could not complete because of the above errors.',
});
throw new Error(errorMessage);
}
return results;
}
exports.default = automate;
//# sourceMappingURL=automation.js.map