UNPKG

ui-coverage-scenario-tool-js

Version:

**UI Coverage Scenario Tool** is an innovative, no-overhead solution for tracking and visualizing UI test coverage — directly on your actual application, not static snapshots. The tool collects coverage during UI test execution and generates an interactiv

608 lines (587 loc) 22.4 kB
#!/usr/bin/env node var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // src/cli.ts var import_commander = require("commander"); // src/tools/logger.ts var getLogger = (name) => ({ info: (msg) => console.info(`[${name}] ${msg}`), debug: (msg) => console.debug(`[${name}] ${msg}`), error: (msg) => console.error(`[${name}] ${msg}`), warning: (msg) => console.warn(`[${name}] ${msg}`) }); // src/tracker/storage.ts var import_promises2 = __toESM(require("fs/promises"), 1); var import_path = __toESM(require("path"), 1); var import_uuid = require("uuid"); // src/tools/files.ts var import_fs = __toESM(require("fs"), 1); var import_js_yaml = __toESM(require("js-yaml"), 1); var import_promises = __toESM(require("fs/promises"), 1); var logger = getLogger("FILES"); var isPathExists = async (path5) => { try { await import_promises.default.access(path5); return true; } catch (error) { return false; } }; var loadFromJson = (file) => { try { if (!import_fs.default.existsSync(file)) return {}; const raw = import_fs.default.readFileSync(file, "utf-8"); return JSON.parse(raw); } catch (error) { logger.warning(`Failed to load JSON config ${file}: ${error}`); return {}; } }; var loadFromYaml = (file) => { try { if (!import_fs.default.existsSync(file)) return {}; const raw = import_fs.default.readFileSync(file, "utf-8"); return import_js_yaml.default.load(raw); } catch (error) { logger.warning(`Failed to load YAML config ${file}: ${error}`); return {}; } }; // src/tracker/models/pages.ts var CoveragePageResultList = class _CoveragePageResultList { constructor({ results }) { this.results = results; } filter({ app }) { const filtered = this.results.filter((result) => !app || result.app.toLowerCase() === app.toLowerCase()); return new _CoveragePageResultList({ results: filtered }); } unique() { const map = /* @__PURE__ */ new Map(); for (const result of this.results) { const key = result.page; if (!map.has(key)) { map.set(key, result); } } return new _CoveragePageResultList({ results: Array.from(map.values()) }); } findScenarios({ page }) { const scenarios = this.results.filter((result) => result.page === page).map((result) => result.scenario); return Array.from(new Set(scenarios)); } }; // src/tracker/models/elements.ts var CoverageElementResultList = class _CoverageElementResultList { constructor({ results }) { this.results = results; } filter({ app, scenario }) { const filtered = this.results.filter( (result) => (!app || result.app.toLowerCase() === app.toLowerCase()) && (!scenario || result.scenario.toLowerCase() === scenario.toLowerCase()) ); return new _CoverageElementResultList({ results: filtered }); } get groupedByAction() { return this.groupBy((r) => r.actionType); } get groupedBySelector() { return this.groupBy((r) => `${encodeURIComponent(r.selector)}|${r.selectorType}`); } get totalActions() { return this.results.length; } get totalSelectors() { return this.groupedBySelector.size; } countAction(actionType) { return this.results.filter((r) => r.actionType === actionType).length; } groupBy(keyGetter) { const map = /* @__PURE__ */ new Map(); for (const result of this.results) { const key = keyGetter(result); const results = map.get(key) || []; results.push(result); map.set(key, results); } const resultMap = /* @__PURE__ */ new Map(); for (const [key, results] of map.entries()) { resultMap.set(key, new _CoverageElementResultList({ results })); } return resultMap; } }; // src/tracker/models/scenarios.ts var CoverageScenarioResultList = class _CoverageScenarioResultList { constructor({ results }) { this.results = results; } filter({ app }) { const filtered = this.results.filter((result) => !app || result.app.toLowerCase() === app.toLowerCase()); return new _CoverageScenarioResultList({ results: filtered }); } }; // src/tracker/models/transitions.ts var CoverageTransitionResultList = class _CoverageTransitionResultList { constructor({ results }) { this.results = results; } filter({ app }) { const filtered = this.results.filter((result) => !app || result.app.toLowerCase() === app.toLowerCase()); return new _CoverageTransitionResultList({ results: filtered }); } unique() { const map = /* @__PURE__ */ new Map(); for (const result of this.results) { const key = `${result.fromPage}\u2192${result.toPage}`; if (!map.has(key)) { map.set(key, result); } } return new _CoverageTransitionResultList({ results: Array.from(map.values()) }); } findScenarios({ toPage, fromPage }) { const scenarios = this.results.filter((result) => result.toPage === toPage && result.fromPage === fromPage).map((result) => result.scenario); return Array.from(new Set(scenarios)); } countTransitions({ toPage, fromPage }) { return this.results.filter((result) => result.toPage === toPage && result.fromPage === fromPage).length; } }; // src/tracker/storage.ts var logger2 = getLogger("UI_COVERAGE_TRACKER_STORAGE"); var UICoverageTrackerStorage = class { constructor({ settings }) { this.settings = settings; } async load(props) { const { context, resultList } = props; const resultsDir = this.settings.resultsDir; logger2.info(`Loading coverage results from directory: ${resultsDir}`); if (!await isPathExists(resultsDir)) { logger2.warning(`Results directory does not exist: ${resultsDir}`); return new resultList({ results: [] }); } const results = []; for (const fileName of await import_promises2.default.readdir(resultsDir)) { const file = import_path.default.join(resultsDir, fileName); const fileStats = await import_promises2.default.stat(file); if (fileStats.isFile() && fileName.endsWith(`-${context}.json`)) { try { const json = await import_promises2.default.readFile(file, "utf-8"); results.push(JSON.parse(json)); } catch (error) { logger2.warning(`Failed to parse file ${fileName}: ${error}`); } } } logger2.info(`Loaded ${results.length} coverage files from directory: ${resultsDir}`); return new resultList({ results }); } async save({ result, context }) { const resultsDir = this.settings.resultsDir; if (!await isPathExists(resultsDir)) { logger2.info(`Results directory does not exist, creating: ${resultsDir}`); await import_promises2.default.mkdir(resultsDir, { recursive: true }); } const file = import_path.default.join(resultsDir, `${(0, import_uuid.v4)()}-${context}.json`); try { await import_promises2.default.writeFile(file, JSON.stringify(result), "utf-8"); } catch (error) { logger2.error(`Error saving coverage data to file ${file}: ${error}`); } } async savePageResult(result) { await this.save({ context: "page", result }); } async saveElementResult(result) { await this.save({ context: "element", result }); } async saveScenarioResult(result) { await this.save({ context: "scenario", result }); } async saveTransitionResult(result) { await this.save({ context: "transition", result }); } async loadPageResults() { return await this.load({ context: "page", resultList: CoveragePageResultList }); } async loadElementResults() { return await this.load({ context: "element", resultList: CoverageElementResultList }); } async loadScenarioResults() { return await this.load({ context: "scenario", resultList: CoverageScenarioResultList }); } async loadTransitionResults() { return await this.load({ context: "transition", resultList: CoverageTransitionResultList }); } }; // src/history/storage.ts var import_promises3 = __toESM(require("fs/promises"), 1); var import_path2 = __toESM(require("path"), 1); // src/tools/json.ts var logger3 = getLogger("JSON"); var loadJson = ({ content, fallback }) => { try { return JSON.parse(content, (key, value) => { switch (key) { case "createdAt": return new Date(value); default: return value; } }); } catch (error) { logger3.warning(`Failed to parse JSON: ${error}`); return fallback; } }; // src/history/storage.ts var logger4 = getLogger("UI_COVERAGE_HISTORY_STORAGE"); var UICoverageHistoryStorage = class { constructor({ settings }) { this.settings = settings; } async load() { const historyFile = this.settings.historyFile; if (!historyFile) { logger4.debug("No history file path provided, returning empty history state"); return { apps: {} }; } if (!await isPathExists(historyFile)) { logger4.error(`History file not found: ${historyFile}, returning empty history state`); return { apps: {} }; } try { logger4.info(`Loading history from file: ${historyFile}`); const content = await import_promises3.default.readFile(historyFile, "utf-8"); return loadJson({ content, fallback: { apps: {} } }); } catch (error) { logger4.error(`Error loading history from file ${historyFile}: ${error}`); return { apps: {} }; } } async save(state) { const historyFile = this.settings.historyFile; if (!historyFile) { logger4.debug("History file path is not defined, skipping history save"); return; } try { await import_promises3.default.mkdir(import_path2.default.dirname(historyFile), { recursive: true }); await import_promises3.default.writeFile(historyFile, JSON.stringify(state), "utf-8"); logger4.info(`History state saved to file: ${historyFile}`); } catch (error) { logger4.error(`Error saving history to file ${historyFile}: ${error}`); } } async saveFromReport(report) { const state = { apps: {} }; for (const app of this.settings.apps) { const coverage = report.appsCoverage[app.key]; if (!coverage) continue; const appState = { total: coverage.history, scenarios: {} }; for (const scenario of coverage.scenarios) { appState.scenarios[scenario.name] = scenario.history; } state.apps[app.key] = appState; } await this.save(state); } }; // src/tools/actions.ts var ActionType = /* @__PURE__ */ ((ActionType2) => { ActionType2["Fill"] = "FILL"; ActionType2["Type"] = "TYPE"; ActionType2["Select"] = "SELECT"; ActionType2["Click"] = "CLICK"; ActionType2["Hover"] = "HOVER"; ActionType2["Text"] = "TEXT"; ActionType2["Value"] = "VALUE"; ActionType2["Hidden"] = "HIDDEN"; ActionType2["Visible"] = "VISIBLE"; ActionType2["Checked"] = "CHECKED"; ActionType2["Enabled"] = "ENABLED"; ActionType2["Disabled"] = "DISABLED"; ActionType2["Unchecked"] = "UNCHECKED"; return ActionType2; })(ActionType || {}); // src/coverage/builder.ts var UICoverageBuilder = class { constructor({ historyBuilder, pageResultList, elementResultList, scenarioResultList, transitionResultList }) { this.historyBuilder = historyBuilder; this.pageResultList = pageResultList; this.elementResultList = elementResultList; this.scenarioResultList = scenarioResultList; this.transitionResultList = transitionResultList; } buildPagesCoverage() { const nodes = this.pageResultList.unique().results.map((result) => ({ url: result.url, page: result.page, priority: result.priority, scenarios: this.pageResultList.findScenarios({ page: result.page }) })); const edges = this.transitionResultList.unique().results.map((result) => ({ count: this.transitionResultList.countTransitions(result), toPage: result.toPage, fromPage: result.fromPage, scenarios: this.transitionResultList.findScenarios(result) })); return { nodes, edges }; } buildScenarioCoverage({ scenario }) { const elements = this.elementResultList.filter({ scenario: scenario.name }); const steps = elements.results.map((element) => ({ selector: element.selector, timestamp: element.timestamp, actionType: element.actionType, selectorType: element.selectorType })); const actions = Object.values(ActionType).map((actionType) => ({ actionType, count: elements.countAction(actionType) })).filter((a) => a.count > 0); const history = this.historyBuilder.getScenarioHistory({ name: scenario.name, actions }); return { url: scenario.url, name: scenario.name, steps, actions, history }; } build() { const pages = this.buildPagesCoverage(); const actions = []; for (const [action, results] of this.elementResultList.groupedByAction.entries()) { if (results.totalActions > 0) { actions.push({ actionType: action, count: results.totalActions }); } } const scenarios = this.scenarioResultList.results.map( (scenario) => this.buildScenarioCoverage({ scenario }) ); const history = this.historyBuilder.getAppHistory({ actions, totalActions: this.elementResultList.totalActions, totalElements: this.elementResultList.totalSelectors }); return { pages, history, scenarios }; } }; // src/history/builder.ts var UICoverageHistoryBuilder = class { constructor({ history, settings }) { this.history = history; this.settings = settings; this.createdAt = /* @__PURE__ */ new Date(); } buildAppHistory({ actions, totalActions, totalElements }) { return { actions, createdAt: this.createdAt, totalActions, totalElements }; } buildScenarioHistory({ actions }) { return { actions, createdAt: this.createdAt }; } appendHistory({ history, buildFunc }) { if (!this.settings.historyFile) { return []; } const newItem = buildFunc(); if (!newItem.actions || newItem.actions.length === 0) { return history; } const combined = [...history, newItem].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); return combined.slice(-this.settings.historyRetentionLimit); } getAppHistory(props) { return this.appendHistory({ history: this.history.total, buildFunc: () => this.buildAppHistory(props) }); } getScenarioHistory({ name, actions }) { const history = this.history.scenarios[name] || []; return this.appendHistory({ history, buildFunc: () => this.buildScenarioHistory({ actions }) }); } }; // src/reports/storage.ts var import_promises4 = __toESM(require("fs/promises"), 1); var import_path3 = __toESM(require("path"), 1); var logger5 = getLogger("UI_REPORTS_STORAGE"); var UIReportsStorage = class { constructor({ settings }) { this.settings = settings; } async injectStateIntoHtml(state) { const stateJson = JSON.stringify(state); const templateFile = this.settings.htmlReportTemplateFile; if (!templateFile || !await isPathExists(templateFile)) { logger5.error("Template HTML report file not found."); return ""; } const html = await import_promises4.default.readFile(templateFile, "utf-8"); const scriptRegex = /<script id="state" type="application\/json">[\s\S]*?<\/script>/gi; const scriptTag = `<script id="state" type="application/json">${stateJson}</script>`; return html.replace(scriptRegex, scriptTag); } async saveJsonReport(state) { const file = this.settings.jsonReportFile; if (!file) { logger5.info("JSON report file is not configured \u2014 skipping JSON report generation."); return; } try { await import_promises4.default.mkdir(import_path3.default.dirname(file), { recursive: true }); await import_promises4.default.writeFile(file, JSON.stringify(state, null, 2)); logger5.info(`JSON report saved to ${file}`); } catch (error) { logger5.error(`Failed to write JSON report: ${error}`); } } async saveHtmlReport(state) { const file = this.settings.htmlReportFile; if (!file) { logger5.info("HTML report file is not configured \u2014 skipping HTML report generation."); return; } try { const content = await this.injectStateIntoHtml(state); await import_promises4.default.mkdir(import_path3.default.dirname(file), { recursive: true }); await import_promises4.default.writeFile(file, content, "utf-8"); logger5.info(`HTML report saved to ${file}`); } catch (error) { logger5.error(`Failed to write HTML report: ${error}`); } } }; // src/config/builders.ts var import_path4 = __toESM(require("path"), 1); var import_url = __toESM(require("url"), 1); var import_dotenv = __toESM(require("dotenv"), 1); var import_meta = {}; import_dotenv.default.config(); var cwd = process.cwd(); var cleanUndefined = (input) => { return Object.fromEntries(Object.entries(input).filter(([_, v]) => v !== void 0)); }; var buildEnvSettings = () => { return cleanUndefined({ apps: loadJson({ content: process.env.UI_COVERAGE_SCENARIO_APPS || "", fallback: [] }), resultsDir: process.env.UI_COVERAGE_SCENARIO_RESULTS_DIR || void 0, historyFile: process.env.UI_COVERAGE_SCENARIO_HISTORY_FILE || void 0, historyRetentionLimit: parseInt(process.env.UI_COVERAGE_SCENARIO_HISTORY_RETENTION_LIMIT || "", 10) || void 0, htmlReportFile: process.env.UI_COVERAGE_SCENARIO_HTML_REPORT_FILE || void 0, jsonReportFile: process.env.UI_COVERAGE_SCENARIO_JSON_REPORT_FILE || void 0 }); }; var buildJsonSettings = () => { return cleanUndefined(loadFromJson(import_path4.default.join(cwd, "ui-coverage-scenario.config.json"))); }; var buildYamlSettings = () => { return cleanUndefined(loadFromYaml(import_path4.default.join(cwd, "ui-coverage-scenario.config.yaml"))); }; var buildDefaultSettings = () => { let htmlReportTemplateFile; try { htmlReportTemplateFile = import_path4.default.join( import_path4.default.dirname(import_url.default.fileURLToPath(import_meta.url)), "reports/templates/index.html" ); } catch { htmlReportTemplateFile = import_path4.default.join(cwd, "src/reports/templates/index.html"); } return { apps: [], resultsDir: import_path4.default.join(cwd, "coverage-results"), historyFile: import_path4.default.join(cwd, "coverage-history.json"), historyRetentionLimit: 30, htmlReportFile: import_path4.default.join(cwd, "index.html"), jsonReportFile: import_path4.default.join(cwd, "coverage-report.json"), htmlReportTemplateFile }; }; // src/config/core.ts var getSettings = () => { const defaultSettings = buildDefaultSettings(); return { ...defaultSettings, ...buildYamlSettings(), ...buildJsonSettings(), ...buildEnvSettings(), htmlReportTemplateFile: defaultSettings.htmlReportTemplateFile }; }; // src/commands/save-report.ts var logger6 = getLogger("SAVE_REPORT"); var saveReport = async () => { logger6.info("Starting to save the report"); const settings = getSettings(); const reportsStorage = new UIReportsStorage({ settings }); const trackerStorage = new UICoverageTrackerStorage({ settings }); const historyStorage = new UICoverageHistoryStorage({ settings }); const reportState = { config: { apps: settings.apps }, createdAt: /* @__PURE__ */ new Date(), appsCoverage: {} }; const historyState = await historyStorage.load(); const pageResults = await trackerStorage.loadPageResults(); const elementResults = await trackerStorage.loadElementResults(); const scenarioResults = await trackerStorage.loadScenarioResults(); const transitionResults = await trackerStorage.loadTransitionResults(); for (const app of settings.apps) { const pageResultList = pageResults.filter({ app: app.key }); const elementResultList = elementResults.filter({ app: app.key }); const scenarioResultList = scenarioResults.filter({ app: app.key }); const transitionResultList = transitionResults.filter({ app: app.key }); const history = historyState.apps[app.key] || { total: [], scenarios: {} }; const historyBuilder = new UICoverageHistoryBuilder({ history, settings }); const coverageBuilder = new UICoverageBuilder({ historyBuilder, pageResultList, elementResultList, scenarioResultList, transitionResultList }); reportState.appsCoverage[app.key] = coverageBuilder.build(); } await historyStorage.saveFromReport(reportState); await reportsStorage.saveJsonReport(reportState); await reportsStorage.saveHtmlReport(reportState); logger6.info("Report saving process completed"); }; // src/commands/print-config.ts var logger7 = getLogger("PRINT_CONFIG"); var printConfig = () => { const settings = getSettings(); logger7.info(JSON.stringify(settings, null, 2)); }; // src/cli.ts var program = new import_commander.Command(); program.name("ui-coverage-scenario-tool").description("UI Coverage Scenario CLI Tool").version("0.13.0"); program.command("save-report").description("Generate a coverage report based on collected result files.").action(saveReport); program.command("print-config").description("Print the resolved configuration to the console.").action(printConfig); program.parse(process.argv); //# sourceMappingURL=cli.cjs.map