@wdio/jasmine-framework
Version:
A WebdriverIO plugin. Adapter for Jasmine testing framework.
521 lines (517 loc) • 17.5 kB
JavaScript
// 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+)*| \/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
};