UNPKG

@wdio/jasmine-framework

Version:
521 lines (517 loc) 17.5 kB
// src/index.ts import url from "node:url"; import Jasmine from "jasmine"; import logger2 from "@wdio/logger"; import { wrapGlobalTestMethod, executeHooksWithArgs } from "@wdio/utils"; import { expect as expectImport, matchers, getConfig } from "expect-webdriverio"; import { _setGlobal } from "@wdio/globals"; // src/reporter.ts import logger from "@wdio/logger"; var log = logger("@wdio/jasmine-framework"); var STACKTRACE_FILTER = /(node_modules(\/|\\)(\w+)*|@wdio\/sync\/(build|src)|- - - - -)/g; var JasmineReporter = class { constructor(_reporter, params) { this._reporter = _reporter; this._cid = params.cid; this._specs = params.specs; this._jasmineOpts = params.jasmineOpts; this._shouldCleanStack = typeof params.cleanStack === "boolean" ? params.cleanStack : true; } startedSuite; _cid; _specs; _jasmineOpts; _shouldCleanStack; _parent = []; _failedCount = 0; _suiteStart = /* @__PURE__ */ new Date(); _testStart = /* @__PURE__ */ new Date(); suiteStarted(suite) { this._suiteStart = /* @__PURE__ */ new Date(); const newSuite = { type: "suite", start: this._suiteStart, ...suite }; this.startedSuite = newSuite; let fullName = suite.description; for (const parent of this._parent.reverse()) { fullName = parent.description + "." + fullName; } newSuite.fullName = fullName; this.emit("suite:start", newSuite); this._parent.push({ description: suite.description, id: suite.id, tests: 0 }); } specStarted(test) { this._testStart = /* @__PURE__ */ new Date(); const newTest = { type: "test", start: this._testStart, ...test }; const parentSuite = this._parent[this._parent.length - 1]; if (!parentSuite) { log.warn( 'No root suite was defined! This can cause reporters to malfunction. Please always start a spec file with describe("...", () => { ... }).' ); } else { parentSuite.tests++; } this.emit("test:start", newTest); } specDone(test) { const newTest = { ...test, start: this._testStart, type: "test", duration: Date.now() - this._testStart.getTime() }; if (test.status === "excluded") { newTest.status = "pending"; } if (test.status === "failed" && this._jasmineOpts.failSpecWithNoExpectations && test.failedExpectations.length === 0 && test.passedExpectations.length === 0) { test.failedExpectations.push({ matcherName: "toHaveAssertion", expected: " >1 assertions", actual: "0 assertions", passed: false, message: 'No assertions found in test! This test is failing because no assertions were found but "jasmineOpts" had a "failSpecWithNoExpectations" flag set to "true".\n\nRead more on this Jasmine option at: https://jasmine.github.io/api/5.0/Configuration#failSpecWithNoExpectations', stack: "" }); } if (test.failedExpectations && test.failedExpectations.length) { let errors = test.failedExpectations; if (this._shouldCleanStack) { errors = test.failedExpectations.map((x) => this.cleanStack(x)); } newTest.errors = errors; newTest.error = errors[0]; } const e = "test:" + (newTest.status ? newTest.status.replace(/ed/, "") : "unknown"); this.emit(e, newTest); this._failedCount += test.status === "failed" ? 1 : 0; this.emit("test:end", newTest); } suiteDone(suite) { const parentSuite = this._parent[this._parent.length - 1]; const newSuite = { ...suite, type: "suite", start: this._suiteStart, duration: Date.now() - this._suiteStart.getTime() }; if (parentSuite.tests === 0 && suite.failedExpectations && suite.failedExpectations.length) { const id = "spec" + Math.random(); this.specStarted({ id, description: "<unknown test>", fullName: "<unknown test>", duration: null, properties: {}, failedExpectations: [], deprecationWarnings: [], passedExpectations: [], status: "unknown", pendingReason: "", debugLogs: null, filename: suite.filename }); this.specDone({ id, description: "<unknown test>", fullName: "<unknown test>", failedExpectations: suite.failedExpectations, deprecationWarnings: [], passedExpectations: [], status: "failed", duration: null, properties: {}, pendingReason: "", debugLogs: null, filename: suite.filename }); } this._parent.pop(); this.emit("suite:end", newSuite); delete this.startedSuite; } emit(event, payload) { const message = { cid: this._cid, uid: this.getUniqueIdentifier(payload), event, title: payload.description, fullTitle: payload.fullName, pending: payload.status === "pending", pendingReason: payload.pendingReason, parent: this._parent.length ? this.getUniqueIdentifier(this._parent[this._parent.length - 1]) : null, type: payload.type, // We maintain the single error property for backwards compatibility with reporters // However, any reporter wanting to make use of Jasmine's 'soft assertion' type expects // should default to looking at 'errors' if they are available error: payload.error, errors: payload.errors, duration: payload.duration || 0, specs: this._specs, start: payload.start, file: payload.filename }; this._reporter.emit(event, message); } getFailedCount() { return this._failedCount; } getUniqueIdentifier(target) { return target.description + target.id; } cleanStack(error) { if (!error.stack) { return error; } let stack = error.stack.split("\n"); stack = stack.filter((line) => !line.match(STACKTRACE_FILTER)); error.stack = stack.join("\n"); return error; } }; // src/utils.ts var buildJasmineFromJestResult = (result, isNot) => { return { pass: result.pass !== isNot, message: result.message() }; }; var jestResultToJasmine = (result, isNot) => { if (result instanceof Promise) { return result.then((jestStyleResult) => buildJasmineFromJestResult(jestStyleResult, isNot)); } return buildJasmineFromJestResult(result, isNot); }; // src/index.ts var INTERFACES = { bdd: ["beforeAll", "beforeEach", "it", "xit", "fit", "afterEach", "afterAll"] }; var EXPECT_ASYMMETRIC_MATCHERS = [ "any", "anything", "arrayContaining", "objectContaining", "stringContaining", "stringMatching", "not" ]; var TEST_INTERFACES = ["it", "fit", "xit"]; var NOOP = function noop() { }; var DEFAULT_TIMEOUT_INTERVAL = 6e4; var FILE_PROTOCOL = "file://"; var log2 = logger2("@wdio/jasmine-framework"); var JasmineAdapter = class { constructor(_cid, _config, _specs, _capabilities, reporter) { this._cid = _cid; this._config = _config; this._specs = _specs; this._capabilities = _capabilities; this._jasmineOpts = Object.assign({ cleanStack: true }, this._config.jasmineOpts || // @ts-expect-error legacy option this._config.jasmineNodeOpts); this._reporter = new JasmineReporter(reporter, { cid: this._cid, specs: this._specs, cleanStack: this._jasmineOpts.cleanStack, jasmineOpts: this._jasmineOpts }); this._hasTests = true; this._jrunner.exitOnCompletion = false; } _jasmineOpts; _reporter; _totalTests = 0; _hasTests = true; _lastTest; _lastSpec; _jrunner = new Jasmine({}); async init() { const self = this; const { jasmine } = this._jrunner; const jasmineEnv = jasmine.getEnv(); this._specs.forEach((spec) => this._jrunner.addSpecFile( /** * as Jasmine doesn't support file:// formats yet we have to * remove it before adding it to Jasmine */ spec.startsWith(FILE_PROTOCOL) ? url.fileURLToPath(spec) : spec )); jasmine.DEFAULT_TIMEOUT_INTERVAL = this._jasmineOpts.defaultTimeoutInterval || DEFAULT_TIMEOUT_INTERVAL; jasmineEnv.addReporter(this._reporter); jasmineEnv.configure({ specFilter: this._jasmineOpts.specFilter || this.customSpecFilter.bind(this), stopOnSpecFailure: Boolean(this._jasmineOpts.stopOnSpecFailure), failSpecWithNoExpectations: Boolean(this._jasmineOpts.failSpecWithNoExpectations), failFast: this._jasmineOpts.failFast, random: Boolean(this._jasmineOpts.random), seed: Boolean(this._jasmineOpts.seed), oneFailurePerSpec: Boolean( // depcrecated old property this._jasmineOpts.stopSpecOnExpectationFailure || this._jasmineOpts.oneFailurePerSpec ) }); jasmine.Spec.prototype.addExpectationResult = this.getExpectationResultHandler(jasmine); const hookArgsFn = (context) => [{ ...self._lastTest || {} }, context]; const emitHookEvent = (fnName, eventType) => (_test, _context, { error } = {}) => { const title = `"${fnName === "beforeAll" ? "before" : "after"} all" hook`; const hook = { id: "", start: /* @__PURE__ */ new Date(), type: "hook", description: title, fullName: title, duration: null, properties: {}, passedExpectations: [], pendingReason: "", failedExpectations: [], deprecationWarnings: [], status: "", debugLogs: null, filename: "", ...error ? { error } : {} }; this._reporter.emit("hook:" + eventType, hook); }; INTERFACES.bdd.forEach((fnName) => { const isTest = TEST_INTERFACES.includes(fnName); const beforeHook = [...this._config.beforeHook]; const afterHook = [...this._config.afterHook]; if (fnName.includes("All")) { beforeHook.push(emitHookEvent(fnName, "start")); afterHook.push(emitHookEvent(fnName, "end")); } wrapGlobalTestMethod( isTest, isTest ? this._config.beforeTest : beforeHook, hookArgsFn, isTest ? this._config.afterTest : afterHook, hookArgsFn, fnName, this._cid ); }); Jasmine.prototype.configureDefaultReporter = NOOP; const beforeAllMock = jasmine.Suite.prototype.beforeAll; jasmine.Suite.prototype.beforeAll = function(...args) { self._lastSpec = this.result; beforeAllMock.apply(this, args); }; const executeMock = jasmine.Spec.prototype.execute; jasmine.Spec.prototype.execute = function(...args) { self._lastTest = this.result; self._lastTest.start = (/* @__PURE__ */ new Date()).getTime(); globalThis._wdioDynamicJasmineResultErrorList = this.result.failedExpectations; globalThis._jasmineTestResult = this.result; executeMock.apply(this, args); }; const expect = jasmineEnv.expectAsync; const matchers2 = this.#setupMatchers(jasmine); jasmineEnv.beforeAll(() => jasmineEnv.addAsyncMatchers(matchers2)); const wdioExpect = expectImport; for (const matcher of EXPECT_ASYMMETRIC_MATCHERS) { expect[matcher] = wdioExpect[matcher]; } expect.not = wdioExpect.not; _setGlobal("expect", expect, this._config.injectGlobals); await this._loadFiles(); _setGlobal("expect", expect, this._config.injectGlobals); return this; } async _loadFiles() { try { if (Array.isArray(this._jasmineOpts.requires)) { this._jrunner.addRequires(this._jasmineOpts.requires); } if (Array.isArray(this._jasmineOpts.helpers)) { this._jrunner.addMatchingHelperFiles(this._jasmineOpts.helpers); } await this._jrunner.loadRequires(); await this._jrunner.loadHelpers(); await this._jrunner.loadSpecs(); this._grep(this._jrunner.env.topSuite()); this._hasTests = this._totalTests > 0; } catch (err) { log2.warn( `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 ); } } _grep(suite) { suite.children.forEach((child) => { if (Array.isArray(child.children)) { return this._grep(child); } if (this.customSpecFilter(child)) { this._totalTests++; } }); } hasTests() { return this._hasTests; } async run() { this._jrunner.env.beforeAll(this.wrapHook("beforeSuite")); this._jrunner.env.afterAll(this.wrapHook("afterSuite")); await this._jrunner.execute(); const result = this._reporter.getFailedCount(); await executeHooksWithArgs("after", this._config.after, [result, this._capabilities, this._specs]); return result; } customSpecFilter(spec) { const { grep, invertGrep } = this._jasmineOpts; const grepMatch = !grep || spec.getFullName().match(new RegExp(grep)) !== null; if (grepMatch === Boolean(invertGrep)) { if (typeof spec.pend === "function") { spec.pend("grep"); } return false; } return true; } /** * Hooks which are added as true Jasmine hooks need to call done() to notify async */ wrapHook(hookName) { return () => executeHooksWithArgs( hookName, this._config[hookName], [this.prepareMessage(hookName)] ).catch((e) => { log2.info(`Error in ${hookName} hook: ${e.stack?.slice(7)}`); }); } prepareMessage(hookName) { const params = { type: hookName }; switch (hookName) { case "beforeSuite": case "afterSuite": params.payload = Object.assign({ file: this._jrunner?.specFiles[0] }, this._lastSpec); break; case "beforeTest": case "afterTest": params.payload = Object.assign({ file: this._jrunner?.specFiles[0] }, this._lastTest); break; } return this.formatMessage(params); } formatMessage(params) { const message = { type: params.type }; if (params.payload) { message.title = params.payload.description; message.fullName = params.payload.fullName || null; message.file = params.payload.file; if (params.payload.failedExpectations && params.payload.failedExpectations.length) { message.errors = params.payload.failedExpectations; message.error = params.payload.failedExpectations[0]; } if (params.payload.id && params.payload.id.startsWith("spec")) { message.parent = this._lastSpec?.description; message.passed = params.payload.failedExpectations.length === 0; } if (params.type === "afterTest") { message.duration = (/* @__PURE__ */ new Date()).getTime() - params.payload.start; } if (typeof params.payload.duration === "number") { message.duration = params.payload.duration; } } return message; } getExpectationResultHandler(jasmine) { const { expectationResultHandler } = this._jasmineOpts; const origHandler = jasmine.Spec.prototype.addExpectationResult; if (typeof expectationResultHandler !== "function") { return origHandler; } return this.expectationResultHandler(origHandler); } expectationResultHandler(origHandler) { const { expectationResultHandler } = this._jasmineOpts; return function(passed, data) { try { expectationResultHandler.call(this, passed, data); } catch (e) { if (passed) { passed = false; data = { passed, message: "expectationResultHandlerError: " + e.message, error: e }; } } return origHandler.call(this, passed, data); }; } #transformMatchers(matchers2) { return Object.entries(matchers2).reduce((prev, [name, fn]) => { prev[name] = (util) => ({ compare: async (actual, expected, ...args) => fn(util).compare(actual, expected, ...args), negativeCompare: async (actual, expected, ...args) => { const { pass, message } = fn(util).compare(actual, expected, ...args); return { pass: !pass, message }; } }); return prev; }, {}); } #setupMatchers(jasmine) { globalThis.jasmine.addMatchers = (matchers2) => globalThis.jasmine.addAsyncMatchers(this.#transformMatchers(matchers2)); const syncMatchers = this.#transformMatchers(jasmine.matchers); const wdioMatchers = [...matchers.entries()].reduce((prev, [name, fn]) => { prev[name] = () => ({ async compare(...args) { const context = getConfig(); const result = fn.apply({ ...context, isNot: false }, args); return jestResultToJasmine(result, false); }, async negativeCompare(...args) { const context = getConfig(); const result = fn.apply({ ...context, isNot: true }, args); return jestResultToJasmine(result, true); } }); return prev; }, {}); return { ...wdioMatchers, ...syncMatchers }; } }; var adapterFactory = {}; adapterFactory.init = async function(...args) { const adapter = new JasmineAdapter(...args); const instance = await adapter.init(); return instance; }; var index_default = adapterFactory; export { JasmineAdapter, adapterFactory, index_default as default };