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

434 lines (419 loc) 14.9 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 __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; 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 )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { ActionType: () => ActionType, SelectorType: () => SelectorType, UICoverageTracker: () => UICoverageTracker }); module.exports = __toCommonJS(src_exports); // 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/tools/selector.ts var SelectorType = /* @__PURE__ */ ((SelectorType2) => { SelectorType2["CSS"] = "CSS"; SelectorType2["XPath"] = "XPATH"; return SelectorType2; })(SelectorType || {}); // 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/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/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 (path3) => { try { await import_promises.default.access(path3); 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/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/config/builders.ts var import_path2 = __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_path2.default.join(cwd, "ui-coverage-scenario.config.json"))); }; var buildYamlSettings = () => { return cleanUndefined(loadFromYaml(import_path2.default.join(cwd, "ui-coverage-scenario.config.yaml"))); }; var buildDefaultSettings = () => { let htmlReportTemplateFile; try { htmlReportTemplateFile = import_path2.default.join( import_path2.default.dirname(import_url.default.fileURLToPath(import_meta.url)), "reports/templates/index.html" ); } catch { htmlReportTemplateFile = import_path2.default.join(cwd, "src/reports/templates/index.html"); } return { apps: [], resultsDir: import_path2.default.join(cwd, "coverage-results"), historyFile: import_path2.default.join(cwd, "coverage-history.json"), historyRetentionLimit: 30, htmlReportFile: import_path2.default.join(cwd, "index.html"), jsonReportFile: import_path2.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/tracker/core.ts var logger4 = getLogger("UI_COVERAGE_TRACKER"); var UICoverageTracker = class { constructor({ app, settings }) { this.app = app; this.settings = settings || getSettings(); this.storage = new UICoverageTrackerStorage({ settings: this.settings }); this.scenario = null; } startScenario({ url: url2, name }) { this.scenario = { url: url2, app: this.app, name }; } async endScenario() { if (this.scenario) { await this.storage.saveScenarioResult(this.scenario); } this.scenario = null; } /** * @deprecated Method is deprecated, use `trackElement` instead. */ async trackCoverage(props) { logger4.warning("Method trackCoverage() is deprecated. Use trackElement() instead."); await this.trackElement(props); } async trackPage({ url: url2, page, priority }) { if (!this.scenario) { logger4.warning("No active scenario. Did you forget to call startScenario? Calling: trackPage"); return; } await this.storage.savePageResult({ app: this.app, url: url2, page, priority, scenario: this.scenario.name }); } async trackElement({ selector, actionType, selectorType }) { if (!this.scenario) { logger4.warning("No active scenario. Did you forget to call startScenario? Calling: trackElement"); return; } await this.storage.saveElementResult({ app: this.app, scenario: this.scenario.name, selector, timestamp: Date.now(), actionType, selectorType }); } async trackTransition({ toPage, fromPage }) { if (!this.scenario) { logger4.warning("No active scenario. Did you forget to call startScenario? Calling: trackTransition"); return; } await this.storage.saveTransitionResult({ app: this.app, toPage, scenario: this.scenario.name, fromPage }); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ActionType, SelectorType, UICoverageTracker }); //# sourceMappingURL=index.cjs.map