@wdio/reporter
Version:
A WebdriverIO utility to help reporting all events
742 lines (730 loc) • 21.8 kB
JavaScript
const __importMetaUrl = require('url').pathToFileURL(__filename).href;
;
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
});