UNPKG

@wdio/mocha-framework

Version:
343 lines (337 loc) 11.6 kB
// 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 };