UNPKG

@wdio/reporter

Version:
742 lines (730 loc) 21.8 kB
const __importMetaUrl = require('url').pathToFileURL(__filename).href; "use strict"; 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 index_exports = {}; __export(index_exports, { HookStats: () => HookStats, RunnerStats: () => RunnerStats, SuiteStats: () => SuiteStats, TestStats: () => TestStats, default: () => WDIOReporter, getBrowserName: () => getBrowserName }); module.exports = __toCommonJS(index_exports); var import_node_fs = __toESM(require("node:fs"), 1); var import_node_path = __toESM(require("node:path"), 1); var import_node_events = require("node:events"); // src/supportsColor.ts var import_node_process = __toESM(require("node:process"), 1); var import_node_os = __toESM(require("node:os"), 1); var import_node_tty = __toESM(require("node:tty"), 1); function hasFlag(flag, argv = import_node_process.default.argv) { const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--"; const position = argv.indexOf(prefix + flag); const terminatorPosition = argv.indexOf("--"); return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition); } var { env } = import_node_process.default; var flagForceColor; if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) { flagForceColor = 0; } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) { flagForceColor = 1; } function envForceColor() { if ("FORCE_COLOR" in env) { if (env.FORCE_COLOR === "true") { return 1; } if (env.FORCE_COLOR === "false") { return 0; } return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3); } } function translateLevel(level) { if (level === 0) { return false; } return { level, hasBasic: true, has256: level >= 2, has16m: level >= 3 }; } function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) { const noFlagForceColor = envForceColor(); if (noFlagForceColor !== void 0) { flagForceColor = noFlagForceColor; } const forceColor = sniffFlags ? flagForceColor : noFlagForceColor; if (forceColor === 0) { return 0; } if (sniffFlags) { if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) { return 3; } if (hasFlag("color=256")) { return 2; } } if ("TF_BUILD" in env && "AGENT_NAME" in env) { return 1; } if (haveStream && !streamIsTTY && forceColor === void 0) { return 0; } const min = forceColor || 0; if (env.TERM === "dumb") { return min; } if (import_node_process.default.platform === "win32") { const osRelease = import_node_os.default.release().split("."); if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) { return Number(osRelease[2]) >= 14931 ? 3 : 2; } return 1; } if ("CI" in env) { if ("GITHUB_ACTIONS" in env || "GITEA_ACTIONS" in env) { return 3; } if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "BUILDKITE", "DRONE"].some((sign) => sign in env) || env.CI_NAME === "codeship") { return 1; } return min; } if ("TEAMCITY_VERSION" in env) { return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0; } if (env.COLORTERM === "truecolor") { return 3; } if (env.TERM === "xterm-kitty") { return 3; } if ("TERM_PROGRAM" in env) { const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10); switch (env.TERM_PROGRAM) { case "iTerm.app": { return version >= 3 ? 3 : 2; } case "Apple_Terminal": { return 2; } } } if (env.TERM && /-256(color)?$/i.test(env.TERM)) { return 2; } if (env.TERM && /^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) { return 1; } if ("COLORTERM" in env) { return 1; } return min; } function createSupportsColor(stream, options = {}) { const level = _supportsColor(stream, { streamIsTTY: stream && stream.isTTY, ...options }); return translateLevel(level); } var supportsColor = { stdout: createSupportsColor({ isTTY: import_node_tty.default.isatty(1) }), stderr: createSupportsColor({ isTTY: import_node_tty.default.isatty(2) }) }; var supportsColor_default = supportsColor; // src/constants.ts var COLORS = { pass: 90, fail: 31, "bright pass": 92, "bright fail": 91, "bright yellow": 93, pending: 36, suite: 0, "error title": 0, "error message": 31, "error stack": 90, checkmark: 32, fast: 90, medium: 33, slow: 31, green: 32, light: 90, "diff gutter": 90, "diff added": 32, "diff removed": 31, "diff added inline": "30;42", "diff removed inline": "30;41" }; // src/utils.ts var FIRST_FUNCTION_REGEX = /function (\w+)/; function sanitizeString(str) { if (!str) { return ""; } return String(str).replace(/^.*\/([^/]+)\/?$/, "$1").replace(/\./g, "_").replace(/\s/g, "").toLowerCase(); } function sanitizeCaps(caps) { if (!caps) { return ""; } let result; result = caps["appium:deviceName"] || caps.deviceName ? [ sanitizeString(caps.platformName), // @ts-expect-error outdated JSONWP capabilities sanitizeString(caps["appium:deviceName"] || caps.deviceName), sanitizeString(caps["appium:platformVersion"]), sanitizeString(caps["appium:app"]) ] : [ sanitizeString(caps.browserName), // @ts-expect-error outdated JSONWP capabilities sanitizeString(caps.version || caps.browserVersion), // @ts-expect-error outdated JSONWP capabilities sanitizeString(caps.platform || caps.platformName), sanitizeString(caps["appium:app"]) ]; result = result.filter((n) => n !== void 0 && n !== ""); return result.join("."); } function getErrorsFromEvent(e) { if (e.errors) { return e.errors; } if (e.error) { return [e.error]; } return []; } function pad(str, len) { return Array(len - str.length + 1).join(" ") + str; } function color(type, content) { if (!supportsColor_default.stdout) { return String(content); } return `\x1B[${COLORS[type]}m${content}\x1B[0m`; } function colorLines(name, str) { return str.split("\n").map((str2) => color(name, str2)).join("\n"); } function transformCommandScript(script) { if (!script) { return script; } let name = void 0; if (typeof script === "string") { name = FIRST_FUNCTION_REGEX.exec(script); FIRST_FUNCTION_REGEX.exec(""); } else if (typeof script === "function") { name = script.name; script = script.toString(); } else { return `<${typeof script}>`; } if (typeof name === "string" && name) { return `<script fn ${name}(...)> [${Buffer.byteLength(script, "utf-8")} bytes]`; } return `<script> [${Buffer.byteLength(script, "utf-8")} bytes]`; } // src/stats/runnable.ts var RunnableStats = class { constructor(type) { this.type = type; } start = /* @__PURE__ */ new Date(); end; _duration = 0; complete() { this.end = /* @__PURE__ */ new Date(); this._duration = this.end.getTime() - this.start.getTime(); } get duration() { if (this.end) { return this._duration; } return (/* @__PURE__ */ new Date()).getTime() - this.start.getTime(); } /** * ToDo: we should always rely on uid */ static getIdentifier(runner) { return runner.uid || runner.title; } }; // src/stats/suite.ts var SuiteStats = class extends RunnableStats { uid; cid; file; title; fullTitle; tags; tests = []; hooks = []; suites = []; parent; retries = 0; /** * an array of hooks and tests stored in order as they happen */ hooksAndTests = []; description; rule; constructor(suite) { super(suite.type || "suite"); this.uid = RunnableStats.getIdentifier(suite); this.cid = suite.cid; this.file = suite.file; this.title = suite.title; this.fullTitle = suite.fullTitle; this.tags = suite.tags; this.parent = suite.parent; this.description = suite.description; this.rule = suite.rule; } /** * Mark suite as retried and remove previous history. */ retry() { this.retries++; this.tests = []; this.hooks = []; this.hooksAndTests = []; } }; // src/stats/hook.ts var HookStats = class extends RunnableStats { uid; cid; title; parent; // Mocha only body; errors; error; state; currentTest; constructor(runner) { super("hook"); this.uid = RunnableStats.getIdentifier(runner); this.cid = runner.cid; this.title = runner.title; this.parent = runner.parent; this.currentTest = runner.currentTest; this.body = runner.body; } complete(errors) { this.errors = errors; if (errors && errors.length) { this.error = errors[0]; this.state = "failed"; } super.complete(); } }; // src/stats/test.ts var import_node_util = require("node:util"); var import_diff = require("diff"); var import_object_inspect = __toESM(require("object-inspect"), 1); var maxStringLength = 2048; var TestStats = class extends RunnableStats { uid; cid; title; currentTest; fullTitle; output; argument; retries; parent; /** * initial test state is pending * the state can change to the following: passed, skipped, failed */ state; pendingReason; errors; error; body; constructor(test) { super("test"); this.uid = RunnableStats.getIdentifier(test); this.cid = test.cid; this.title = test.title; this.fullTitle = test.fullTitle; this.output = []; this.argument = test.argument; this.retries = test.retries; this.parent = test.parent; this.body = test.body; this.state = "pending"; } pass() { this.complete(); this.state = "passed"; } skip(reason) { this.pendingReason = reason; this.state = "skipped"; } fail(errors) { this.complete(); this.state = "failed"; const formattedErrors = errors?.map((err) => ( /** * only format if error object has either an "expected" or "actual" property set */ (err.expected || err.actual) && !import_node_util.types.isProxy(err.actual) && /** * and if they aren't already formated, e.g. in Jasmine */ (err.message && !err.message.includes("Expected: ") && !err.message.includes("Received: ")) ? this._stringifyDiffObjs(err) : err )); this.errors = formattedErrors; if (formattedErrors && formattedErrors.length) { this.error = formattedErrors[0]; } } _stringifyDiffObjs(err) { const inspectOpts = { maxStringLength }; const expected = (0, import_object_inspect.default)(err.expected, inspectOpts); const actual = (0, import_object_inspect.default)(err.actual, inspectOpts); let msg = (0, import_diff.diffWordsWithSpace)(actual, expected).map((str) => str.added ? colorLines("diff added inline", str.value) : str.removed ? colorLines("diff removed inline", str.value) : str.value).join(""); const lines = msg.split("\n"); if (lines.length > 4) { const width = String(lines.length).length; msg = lines.map(function(str, i) { return pad(String(++i), width) + " | " + str; }).join("\n"); } msg = ` ${color("diff removed inline", "actual")} ${color("diff added inline", "expected")} ${msg} `; msg = msg.replace(/^/gm, " "); const newError = new Error(err.message + msg); newError.stack = err.stack; return newError; } }; // src/stats/runner.ts var RunnerStats = class extends RunnableStats { cid; capabilities; sanitizedCapabilities; config; specs; sessionId; isMultiremote; instanceOptions; retry; failures; retries; error; constructor(runner) { super("runner"); this.cid = runner.cid; this.capabilities = runner.capabilities; this.sanitizedCapabilities = sanitizeCaps(runner.capabilities); this.config = runner.config; this.specs = runner.specs; this.sessionId = runner.sessionId; this.isMultiremote = runner.isMultiremote; this.instanceOptions = runner.instanceOptions; this.retry = runner.retry; } }; // src/index.ts var WDIOReporter = class extends import_node_events.EventEmitter { constructor(options) { super(); this.options = options; if (this.options.outputDir) { try { import_node_fs.default.mkdirSync(this.options.outputDir, { recursive: true }); } catch (err) { throw new Error(`Couldn't create output directory at "${this.options.outputDir}": ${err.stack}`); } } this.outputStream = (this.options.stdout || !this.options.logFile) && this.options.writeStream ? this.options.writeStream : import_node_fs.default.createWriteStream(this.options.logFile); let currentTest; const rootSuite = new SuiteStats({ title: "(root)", fullTitle: "(root)", file: "" }); this.currentSuites.push(rootSuite); this.on("client:beforeCommand", this.onBeforeCommand.bind(this)); this.on("client:afterCommand", this.onAfterCommand.bind(this)); this.on("client:beforeAssertion", this.onBeforeAssertion.bind(this)); this.on("client:afterAssertion", this.onAfterAssertion.bind(this)); this.on("runner:start", (runner) => { rootSuite.cid = runner.cid; this.specs.push(...runner.specs); this.runnerStat = new RunnerStats(runner); this.onRunnerStart(this.runnerStat); }); this.on("suite:start", (params) => { if (!params.file) { params.file = !params.parent ? this.specs.shift() || "unknown spec file" : this.currentSpec; this.currentSpec = params.file; } const suite = new SuiteStats(params); const currentSuite = this.currentSuites[this.currentSuites.length - 1]; currentSuite.suites.push(suite); this.currentSuites.push(suite); this.suites[suite.uid] = suite; this.onSuiteStart(suite); }); this.on("suite:retry", (suite) => { const suiteStat = this.suites[suite.uid]; suiteStat.retry(); this.onSuiteRetry(suiteStat); }); this.on("hook:start", (hook) => { const hookStats = new HookStats(hook); const currentSuite = this.currentSuites[this.currentSuites.length - 1]; currentSuite.hooks.push(hookStats); currentSuite.hooksAndTests.push(hookStats); this.hooks[hook.uid] = hookStats; this.onHookStart(hookStats); }); this.on("hook:end", (hook) => { const hookStats = this.hooks[hook.uid]; hookStats.complete(getErrorsFromEvent(hook)); this.counts.hooks++; this.onHookEnd(hookStats); }); this.on("test:start", (test) => { test.retries = this.retries; currentTest = new TestStats(test); const currentSuite = this.currentSuites[this.currentSuites.length - 1]; currentSuite.tests.push(currentTest); currentSuite.hooksAndTests.push(currentTest); this.tests[test.uid] = currentTest; this.onTestStart(currentTest); }); this.on("test:pass", (test) => { const testStat = this.tests[test.uid]; testStat.pass(); this.counts.passes++; this.counts.tests++; this.onTestPass(testStat); }); this.on("test:skip", (test) => { const testStat = this.tests[test.uid]; currentTest.skip(test.pendingReason); this.counts.skipping++; this.counts.tests++; this.onTestSkip(testStat); }); this.on("test:fail", (test) => { const testStat = this.tests[test.uid]; testStat.fail(getErrorsFromEvent(test)); this.counts.failures++; this.counts.tests++; this.onTestFail(testStat); }); this.on("test:retry", (test) => { const testStat = this.tests[test.uid]; testStat.fail(getErrorsFromEvent(test)); this.onTestRetry(testStat); this.retries++; }); this.on("test:pending", (test) => { test.retries = this.retries; const currentSuite = this.currentSuites[this.currentSuites.length - 1]; currentTest = new TestStats(test); if (test.uid in this.tests && this.tests[test.uid].state !== "pending") { currentTest.uid = test.uid in this.tests ? "skipped-" + this.counts.skipping : currentTest.uid; } const suiteTests = currentSuite.tests; if (!suiteTests.length || currentTest.uid !== suiteTests[suiteTests.length - 1].uid) { currentSuite.tests.push(currentTest); currentSuite.hooksAndTests.push(currentTest); } else { suiteTests[suiteTests.length - 1] = currentTest; currentSuite.hooksAndTests[currentSuite.hooksAndTests.length - 1] = currentTest; } this.tests[currentTest.uid] = currentTest; if (test.state === "pending") { currentTest.state = "pending"; this.counts.pending++; this.counts.tests++; this.onTestPending(currentTest); } else { currentTest.skip(test.pendingReason); this.counts.skipping++; this.counts.tests++; this.onTestSkip(currentTest); } }); this.on("test:end", (test) => { const testStat = this.tests[test.uid]; this.retries = 0; this.onTestEnd(testStat); }); this.on("suite:end", (suite) => { const suiteStat = this.suites[suite.uid]; suiteStat.complete(); this.currentSuites.pop(); this.onSuiteEnd(suiteStat); }); this.on("runner:end", (runner) => { rootSuite.complete(); if (this.runnerStat) { this.runnerStat.failures = runner.failures; this.runnerStat.retries = runner.retries; this.runnerStat.error = runner.error; this.runnerStat.complete(); this.onRunnerEnd(this.runnerStat); } const logFile = this.options.logFile; if (!this.isContentPresent && logFile && import_node_fs.default.existsSync(logFile)) { import_node_fs.default.unlinkSync(logFile); } }); this.on("client:beforeCommand", (payload) => { if (!currentTest) { return; } if (payload.body?.script) { payload.body = { ...payload.body, script: transformCommandScript(payload.body.script) }; } currentTest.output.push(Object.assign(payload, { type: "command" })); }); this.on("client:afterCommand", (payload) => { if (!currentTest) { return; } if (payload.body?.script) { payload.body = { ...payload.body, script: transformCommandScript(payload.body.script) }; } currentTest.output.push(Object.assign(payload, { type: "result" })); }); } outputStream; failures = 0; suites = {}; hooks = {}; tests = {}; currentSuites = []; counts = { suites: 0, tests: 0, hooks: 0, passes: 0, skipping: 0, failures: 0, pending: 0 }; retries = 0; runnerStat; isContentPresent = false; specs = []; currentSpec; /** * allows reporter to stale process shutdown process until required sync work * is done (e.g. when having to send data to some server or any other async work) */ get isSynchronised() { return true; } /** * function to write to reporters output stream */ write(content) { if (content) { this.isContentPresent = true; } this.outputStream.write(content); } onRunnerStart(_runnerStats) { } onBeforeCommand(_commandArgs) { } onAfterCommand(_commandArgs) { } onBeforeAssertion(_assertionArgs) { } onAfterAssertion(_assertionArgs) { } onSuiteStart(_suiteStats) { } onHookStart(_hookStat) { } onHookEnd(_hookStats) { } onTestStart(_testStats) { } onTestPass(_testStats) { } onTestFail(_testStats) { } onTestRetry(_testStats) { } onTestSkip(_testStats) { } onTestPending(_testStats) { } onTestEnd(_testStats) { } onSuiteRetry(_suiteStats) { } onSuiteEnd(_suiteStats) { } onRunnerEnd(_runnerStats) { } }; function getBrowserName(caps) { const app = (caps["appium:app"] || caps.app || "").replace("sauce-storage:", ""); const appName = caps["appium:bundleId"] || caps["appium:appPackage"] || caps["appium:appActivity"] || (import_node_path.default.isAbsolute(app) ? import_node_path.default.basename(app) : app); return caps.browserName || caps.browser || appName; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { HookStats, RunnerStats, SuiteStats, TestStats, getBrowserName });