@wdio/mocha-framework
Version:
A WebdriverIO plugin. Adapter for Mocha testing framework.
343 lines (337 loc) • 11.6 kB
JavaScript
// src/index.ts
import url from "node:url";
import path from "node:path";
import Mocha from "mocha";
import { handleRequires } from "mocha/lib/cli/run-helpers.js";
import logger from "@wdio/logger";
import { executeHooksWithArgs } from "@wdio/utils";
// src/common.ts
import { wrapGlobalTestMethod } from "@wdio/utils";
// src/constants.ts
var INTERFACES = {
bdd: ["it", "specify", "before", "beforeEach", "after", "afterEach"],
tdd: ["test", "suiteSetup", "setup", "suiteTeardown", "teardown"],
qunit: ["test", "before", "beforeEach", "after", "afterEach"]
};
var TEST_INTERFACES = {
bdd: ["it", "specify"],
tdd: ["test"],
qunit: ["test"]
};
var EVENTS = {
"suite": "suite:start",
"suite end": "suite:end",
"test": "test:start",
"test end": "test:end",
"hook": "hook:start",
"hook end": "hook:end",
"pass": "test:pass",
"fail": "test:fail",
"retry": "test:retry",
"pending": "test:pending"
};
var NOOP = (
/* istanbul ignore next */
function() {
}
);
var MOCHA_TIMEOUT_MESSAGE = 'For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.';
// src/common.ts
var MOCHA_UI_TYPE_EXTRACTOR = /^(?:.*-)?([^-.]+)(?:.js)?$/;
var DEFAULT_INTERFACE_TYPE = "bdd";
function formatMessage(params) {
const message = {
type: params.type
};
const payload = params.payload;
const mochaAllHooksIfPresent = payload?.title?.match(/^"(before|after)( all| each)?" hook/);
if (params.err) {
if (params.err && params.err.message && params.err.message.includes(MOCHA_TIMEOUT_MESSAGE)) {
const testName = (payload?.parent?.title ? `${payload.parent.title} ${payload.title}` : payload?.title) || "unknown test";
const replacement = `The execution in the test "${testName}" took too long. Try to reduce the run time or increase your timeout for test specs (https://webdriver.io/docs/timeouts).`;
params.err.message = params.err.message.replace(MOCHA_TIMEOUT_MESSAGE, replacement);
params.err.stack = params.err.stack.replace(MOCHA_TIMEOUT_MESSAGE, replacement);
}
message.error = {
name: params.err.name,
message: params.err.message,
stack: params.err.stack,
type: params.err.type || params.err.name,
expected: params.err.expected,
actual: params.err.actual
};
if (mochaAllHooksIfPresent) {
message.type = "hook:end";
}
}
if (payload) {
message.title = payload.title;
message.parent = payload.parent ? payload.parent.title : void 0;
let fullTitle = message.title;
if (payload.parent) {
let parent = payload.parent;
while (parent && parent.title) {
fullTitle = parent.title + "." + fullTitle;
parent = parent.parent;
}
}
message.fullTitle = fullTitle;
message.pending = payload.pending || false;
message.file = payload.file;
message.duration = payload.duration;
message.body = payload.body;
if (payload.ctx && payload.ctx.currentTest) {
message.currentTest = payload.ctx.currentTest.title;
}
if (params.type.match(/Test/)) {
message.passed = payload.state === "passed";
}
if (payload.parent?.title && mochaAllHooksIfPresent) {
const hookName = mochaAllHooksIfPresent[0];
message.title = `${hookName} for ${payload.parent.title}`;
}
if (payload.context) {
message.context = payload.context;
}
}
return message;
}
function requireExternalModules(mods, loader = loadModule) {
return mods.map((mod) => {
if (!mod) {
return Promise.resolve();
}
mod = mod.includes(":") ? mod.substring(mod.lastIndexOf(":") + 1) : mod;
if (mod.startsWith("./") && globalThis.process) {
mod = `${globalThis.process.cwd()}/${mod.slice(2)}`;
}
return loader(mod);
});
}
function setupEnv(cid, options, beforeTest, beforeHook, afterTest, afterHook) {
const match = MOCHA_UI_TYPE_EXTRACTOR.exec(options.ui);
const type = match && INTERFACES[match[1]] && match[1] || DEFAULT_INTERFACE_TYPE;
const hookArgsFn = (context) => {
return [{ ...context.test, parent: context.test?.parent?.title }, context];
};
INTERFACES[type].forEach((fnName) => {
const isTest = TEST_INTERFACES[type].flatMap((testCommand) => [testCommand, testCommand + ".only"]).includes(fnName);
wrapGlobalTestMethod(
isTest,
isTest ? beforeTest : beforeHook,
// @ts-ignore
hookArgsFn,
isTest ? afterTest : afterHook,
hookArgsFn,
fnName,
cid
);
});
const { compilers = [] } = options;
return requireExternalModules([...compilers]);
}
async function loadModule(name) {
try {
return await import(
/* @vite-ignore */
name
);
} catch {
throw new Error(`Module ${name} can't get loaded. Are you sure you have installed it?
Note: if you've installed WebdriverIO globally you need to install these external modules globally too!`);
}
}
// src/types.ts
import { default as default2 } from "mocha";
// src/index.ts
var log = logger("@wdio/mocha-framework");
var FILE_PROTOCOL = "file://";
var MochaAdapter = class {
constructor(_cid, _config, _specs, _capabilities, _reporter) {
this._cid = _cid;
this._config = _config;
this._specs = _specs;
this._capabilities = _capabilities;
this._reporter = _reporter;
this._config = Object.assign({
mochaOpts: {}
}, _config);
}
_mocha;
_runner;
_specLoadError;
_level = 0;
_hasTests = true;
_suiteIds = ["0"];
_suiteCnt = /* @__PURE__ */ new Map();
_hookCnt = /* @__PURE__ */ new Map();
_testCnt = /* @__PURE__ */ new Map();
_suiteStartDate = Date.now();
async init() {
const { mochaOpts } = this._config;
if (Array.isArray(mochaOpts.require)) {
const plugins = await handleRequires(
mochaOpts.require.filter((p) => typeof p === "string").map((p) => path.resolve(this._config.rootDir, p))
);
Object.assign(mochaOpts, plugins);
}
const mocha = this._mocha = new Mocha(mochaOpts);
await mocha.loadFilesAsync({
esmDecorator: (file) => `${file}?invalidateCache=${Math.random()}`
});
mocha.reporter(NOOP);
mocha.fullTrace();
this._specs.forEach((spec) => mocha.addFile(
spec.startsWith(FILE_PROTOCOL) ? url.fileURLToPath(spec) : spec
));
const { beforeTest, beforeHook, afterTest, afterHook } = this._config;
mocha.suite.on("pre-require", () => setupEnv(this._cid, this._config.mochaOpts, beforeTest, beforeHook, afterTest, afterHook));
mocha.suite.on("require", () => mocha.unloadFiles());
await this._loadFiles(mochaOpts);
return this;
}
async _loadFiles(mochaOpts) {
try {
await this._mocha.loadFilesAsync({
esmDecorator: (file) => `${file}?invalidateCache=${Math.random()}`
});
const mochaRunner = new Mocha.Runner(this._mocha.suite, { delay: false });
if (mochaOpts.grep) {
mochaRunner.grep(this._mocha.options.grep, mochaOpts.invert);
}
this._hasTests = mochaRunner.total > 0;
} catch (err) {
const error = `Unable to load spec files quite likely because they rely on \`browser\` object that is not fully initialized.
\`browser\` object has only \`capabilities\` and some flags like \`isMobile\`.
Helper files that use other \`browser\` commands have to be moved to \`before\` hook.
Spec file(s): ${this._specs.join(",")}
Error: ${err.stack}`;
this._specLoadError = new Error(error);
log.warn(error);
}
}
hasTests() {
return this._hasTests;
}
async run() {
const mocha = this._mocha;
let runtimeError;
const result = await new Promise((resolve) => {
try {
this._runner = mocha.run(resolve);
} catch (err) {
runtimeError = err;
return resolve(1);
}
Object.keys(EVENTS).forEach((e) => this._runner.on(e, this.emit.bind(this, EVENTS[e])));
this._runner.suite.beforeAll(this.wrapHook("beforeSuite"));
this._runner.suite.afterAll(this.wrapHook("afterSuite"));
});
await executeHooksWithArgs("after", this._config.after, [runtimeError || this._specLoadError || result, this._capabilities, this._specs]);
if (runtimeError || this._specLoadError) {
throw runtimeError || this._specLoadError;
}
return result;
}
/**
* Hooks which are added as true Mocha hooks need to call done() to notify async
*/
wrapHook(hookName) {
return () => executeHooksWithArgs(
hookName,
this._config[hookName],
[this.prepareMessage(hookName)]
).catch((e) => {
log.error(`Error in ${hookName} hook: ${e.stack.slice(7)}`);
});
}
prepareMessage(hookName) {
const params = { type: hookName };
switch (hookName) {
case "beforeSuite":
this._suiteStartDate = Date.now();
params.payload = this._runner?.suite.suites[0];
break;
case "afterSuite":
params.payload = this._runner?.suite.suites[0];
if (params.payload) {
params.payload.duration = params.payload.duration || Date.now() - this._suiteStartDate;
}
break;
case "beforeTest":
case "afterTest":
params.payload = this._runner?.test;
break;
}
return formatMessage(params);
}
emit(event, payload, err) {
if (payload.root) {
return;
}
const message = formatMessage({ type: event, payload, err });
message.cid = this._cid;
message.specs = this._specs;
message.uid = this.getUID(message);
this._reporter.emit(message.type, message);
}
getSyncEventIdStart(type) {
const prop = `_${type}Cnt`;
const suiteId = this._suiteIds[this._suiteIds.length - 1];
const cnt = this[prop].has(suiteId) ? this[prop].get(suiteId) || 0 : 0;
this[prop].set(suiteId, cnt + 1);
return `${type}-${suiteId}-${cnt}`;
}
getSyncEventIdEnd(type) {
const prop = `_${type}Cnt`;
const suiteId = this._suiteIds[this._suiteIds.length - 1];
const cnt = this[prop].get(suiteId) - 1;
return `${type}-${suiteId}-${cnt}`;
}
getUID(message) {
if (message.type === "suite:start") {
const suiteCnt = this._suiteCnt.has(this._level.toString()) ? this._suiteCnt.get(this._level.toString()) : 0;
const suiteId = `suite-${this._level}-${suiteCnt}`;
if (this._suiteCnt.has(this._level.toString())) {
this._suiteCnt.set(this._level.toString(), this._suiteCnt.get(this._level.toString()) + 1);
} else {
this._suiteCnt.set(this._level.toString(), 1);
}
this._suiteIds.push(`${this._level}${suiteCnt}`);
this._level++;
return suiteId;
}
if (message.type === "suite:end") {
this._level--;
const suiteCnt = this._suiteCnt.get(this._level.toString()) - 1;
const suiteId = `suite-${this._level}-${suiteCnt}`;
this._suiteIds.pop();
return suiteId;
}
if (message.type === "hook:start") {
return this.getSyncEventIdStart("hook");
}
if (message.type === "hook:end") {
return this.getSyncEventIdEnd("hook");
}
if (["test:start", "test:pending"].includes(message.type)) {
return this.getSyncEventIdStart("test");
}
if (["test:end", "test:pass", "test:fail", "test:retry"].includes(message.type)) {
return this.getSyncEventIdEnd("test");
}
throw new Error(`Unknown message type : ${message.type}`);
}
};
var adapterFactory = {};
adapterFactory.init = async function(...args) {
const adapter = new MochaAdapter(...args);
const instance = await adapter.init();
return instance;
};
var index_default = adapterFactory;
export {
MochaAdapter,
adapterFactory,
index_default as default
};