UNPKG

@alithanar/react-automation-profiler

Version:

Automated React profiling and data visualization using React's Profiler API, Puppeteer, and D3.

360 lines 14.9 kB
"use strict"; 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