@badeball/cypress-cucumber-preprocessor
Version:
[](https://github.com/badeball/cypress-cucumber-preprocessor/actions/workflows/build.yml) [ • 30.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createStringAttachmentHandler = exports.testCaseFinishedHandler = exports.testStepFinishedHandler = exports.testStepStartedHandler = exports.testCaseStartedHandler = exports.specEnvelopesHandler = exports.afterSpecHandler = exports.beforeSpecHandler = exports.CypressCucumberStateError = void 0;
exports.beforeRunHandler = beforeRunHandler;
exports.afterRunHandler = afterRunHandler;
exports.afterScreenshotHandler = afterScreenshotHandler;
exports.frontendTrackingErrorHandler = frontendTrackingErrorHandler;
const fs_1 = __importStar(require("fs"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const promises_1 = require("stream/promises");
const stream_1 = __importDefault(require("stream"));
const util_1 = require("util");
const chalk_1 = __importDefault(require("chalk"));
const messages = __importStar(require("@cucumber/messages"));
const ci_environment_1 = __importDefault(require("@cucumber/ci-environment"));
const split_1 = __importDefault(require("split"));
const constants_1 = require("./constants");
const preprocessor_configuration_1 = require("./preprocessor-configuration");
const paths_1 = require("./helpers/paths");
const messages_1 = require("./helpers/messages");
const memoize_1 = require("./helpers/memoize");
const debug_1 = __importDefault(require("./helpers/debug"));
const error_1 = require("./helpers/error");
const assertions_1 = require("./helpers/assertions");
const formatters_1 = require("./helpers/formatters");
const colors_1 = require("./helpers/colors");
const type_guards_1 = require("./helpers/type-guards");
const strings_1 = require("./helpers/strings");
const version_1 = require("./version");
const resolve = (0, memoize_1.memoize)(preprocessor_configuration_1.resolve);
let state = {
state: "uninitialized",
};
const isFeature = (spec) => spec.name.endsWith(".feature");
const end = (stream) => new Promise((resolve) => stream.end(resolve));
const createPrettyStream = () => {
const line = (0, split_1.default)(null, null, { trailing: false });
const indent = new stream_1.default.Transform({
objectMode: true,
transform(chunk, _, callback) {
callback(null, chunk.length === 0 ? "" : " " + chunk);
},
});
const log = new stream_1.default.Writable({
write(chunk, _, callback) {
console.log(chunk.toString("utf8"));
callback();
},
});
return stream_1.default.compose(line, indent, log);
};
class CypressCucumberStateError extends error_1.CypressCucumberError {
}
exports.CypressCucumberStateError = CypressCucumberStateError;
const createStateError = (stateHandler, currentState) => new CypressCucumberStateError(`Unexpected state in ${stateHandler}: ${currentState}. This almost always means that you or some other plugin, are overwriting this plugin's event handlers. For more information & workarounds, see https://github.com/badeball/cypress-cucumber-preprocessor/blob/master/docs/event-handlers.md (if neither workaround work, please report at ${error_1.homepage})`);
const createGracefullPluginEventHandler = (fn, gracefullReturnValue = undefined) => {
return async (config, ...args) => {
const preprocessor = await resolve(config, config.env, "/");
if (state.state === "tracking-error") {
return gracefullReturnValue;
}
if (preprocessor.state.softErrors) {
try {
return await fn(config, ...args);
}
catch (e) {
state = {
state: "tracking-error",
type: "backend",
error: (0, util_1.inspect)(e),
};
return gracefullReturnValue;
}
}
else {
return fn(config, ...args);
}
};
};
async function beforeRunHandler(config) {
(0, debug_1.default)("beforeRunHandler()");
const preprocessor = await resolve(config, config.env, "/");
if (!preprocessor.isTrackingState) {
return;
}
switch (state.state) {
case "uninitialized":
break;
default:
throw createStateError("beforeRunHandler", state.state);
}
// Copied from https://github.com/cucumber/cucumber-js/blob/v10.0.1/src/cli/helpers.ts#L104-L122.
const meta = {
meta: {
protocolVersion: messages.version,
implementation: {
version: version_1.version,
name: "@badeball/cypress-cucumber-preprocessor",
},
cpu: {
name: os_1.default.arch(),
},
os: {
name: os_1.default.platform(),
version: os_1.default.release(),
},
runtime: {
name: "node.js",
version: process.versions.node,
},
ci: (0, ci_environment_1.default)(process.env),
},
};
const testRunStarted = {
testRunStarted: {
timestamp: (0, messages_1.createTimestamp)(),
},
};
let pretty;
if (preprocessor.pretty.enabled) {
const writable = createPrettyStream();
const eventBroadcaster = (0, formatters_1.createPrettyFormatter)((0, colors_1.useColors)(), (chunk) => writable.write(chunk));
pretty = {
enabled: true,
broadcaster: eventBroadcaster,
writable,
};
}
else {
pretty = {
enabled: false,
};
}
state = {
state: "before-run",
pretty,
messages: {
accumulation: [meta, testRunStarted],
},
};
}
async function afterRunHandler(config, results) {
(0, debug_1.default)("afterRunHandler()");
const preprocessor = await resolve(config, config.env, "/");
if (!preprocessor.isTrackingState) {
return;
}
if (state.state === "tracking-error") {
console.warn(chalk_1.default.yellow(`A Cucumber library state error (shown bellow) occured in the ${state.type}, thus no report is created.`));
console.warn("");
console.warn(chalk_1.default.yellow(state.error));
return;
}
switch (state.state) {
case "after-spec": // This is the normal case.
case "before-run": // This can happen when running only non-feature specs.
break;
default:
throw createStateError("afterRunHandler", state.state);
}
const testRunFinished = {
testRunFinished: {
success: "totalFailed" in results ? results.totalFailed === 0 : false,
timestamp: (0, messages_1.createTimestamp)(),
},
};
if (state.pretty.enabled) {
await end(state.pretty.writable);
}
state = {
state: "after-run",
messages: {
accumulation: state.messages.accumulation.concat(testRunFinished),
},
};
(0, messages_1.removeDuplicatedStepDefinitions)(state.messages.accumulation);
(0, messages_1.removeUnusedDefinitions)(state.messages.accumulation);
if (preprocessor.messages.enabled) {
const messagesPath = (0, paths_1.ensureIsAbsolute)(config.projectRoot, preprocessor.messages.output);
await fs_1.promises.mkdir(path_1.default.dirname(messagesPath), { recursive: true });
await fs_1.promises.writeFile(messagesPath, state.messages.accumulation
.map((message) => JSON.stringify(message))
.join("\n") + "\n");
}
if (preprocessor.json.enabled) {
const jsonPath = (0, paths_1.ensureIsAbsolute)(config.projectRoot, preprocessor.json.output);
await fs_1.promises.mkdir(path_1.default.dirname(jsonPath), { recursive: true });
let jsonOutput;
const eventBroadcaster = (0, formatters_1.createJsonFormatter)(state.messages.accumulation, (chunk) => {
jsonOutput = chunk;
});
try {
for (const message of state.messages.accumulation) {
eventBroadcaster.emit("envelope", message);
}
}
catch (e) {
const message = (messagesOutput) => `JsonFormatter failed with an error shown below. This might be a bug, please report at ${error_1.homepage} and make sure to attach the messages report in your ticket (${messagesOutput}).\n`;
if (preprocessor.messages.enabled) {
console.warn(chalk_1.default.yellow(message(preprocessor.messages.output)));
}
else {
const temporaryMessagesOutput = path_1.default.join(await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), "cypress-cucumber-preprocessor-")), "cucumber-messages.ndjson");
await fs_1.promises.writeFile(temporaryMessagesOutput, state.messages.accumulation
.map((message) => JSON.stringify(message))
.join("\n") + "\n");
console.warn(chalk_1.default.yellow(message(temporaryMessagesOutput)));
}
throw e;
}
(0, assertions_1.assertIsString)(jsonOutput, "Expected JSON formatter to have finished, but it never returned");
await fs_1.promises.writeFile(jsonPath, jsonOutput);
}
if (preprocessor.html.enabled) {
const htmlPath = (0, paths_1.ensureIsAbsolute)(config.projectRoot, preprocessor.html.output);
await fs_1.promises.mkdir(path_1.default.dirname(htmlPath), { recursive: true });
const output = fs_1.default.createWriteStream(htmlPath);
await (0, promises_1.pipeline)(stream_1.default.Readable.from(state.messages.accumulation), (0, formatters_1.createHtmlStream)(), output);
}
if (preprocessor.usage.enabled) {
let usageOutput;
const eventBroadcaster = (0, formatters_1.createUsageFormatter)(state.messages.accumulation, (chunk) => {
usageOutput = chunk;
});
for (const message of state.messages.accumulation) {
eventBroadcaster.emit("envelope", message);
}
(0, assertions_1.assertIsString)(usageOutput, "Expected usage formatter to have finished, but it never returned");
if (preprocessor.usage.output === "stdout") {
console.log((0, strings_1.indent)(usageOutput, { count: 2 }));
}
else {
const usagePath = (0, paths_1.ensureIsAbsolute)(config.projectRoot, preprocessor.usage.output);
await fs_1.promises.mkdir(path_1.default.dirname(usagePath), { recursive: true });
await fs_1.promises.writeFile(usagePath, usageOutput);
}
}
}
exports.beforeSpecHandler = createGracefullPluginEventHandler(async (config, spec) => {
(0, debug_1.default)("beforeSpecHandler()");
if (!isFeature(spec)) {
return;
}
const preprocessor = await resolve(config, config.env, "/");
if (!preprocessor.isTrackingState) {
return;
}
/**
* Ideally this would only run when current state is either "before-run" or "after-spec". However,
* reload-behavior means that this is not necessarily true. Reloading can occur in the following
* scenarios:
*
* - before()
* - beforeEach()
* - in a step
* - afterEach()
* - after()
*
* If it happens in the three latter scenarios, the current / previous test will be re-run by
* Cypress under a new domain. In these cases, messages associated with the latest test will have
* to be discarded and a "Reloading.." message will appear *if* pretty output is enabled. If that
* is the case, then the pretty reporter instance will also have re-instantiated and primed with
* envelopes associated with the current spec.
*
* To make matters worse, it's impossible in this handler to determine of a reload occurs due to
* a beforeEach hook or an afterEach hook. In the latter case, messages must be discarded. This is
* however not true for the former case.
*/
switch (state.state) {
case "before-run":
case "after-spec":
state = {
state: "before-spec",
spec,
pretty: state.pretty,
messages: state.messages,
};
return;
}
// This will be the case for reloads occurring in a before(), in which case we do nothing,
// because "received-envelopes" would anyway be the next natural state.
if (state.state === "before-spec" &&
config.env[`${constants_1.INTERNAL_PROPERTY_NAME}_simulate_backend_error`] !== true) {
return;
}
switch (state.state) {
case "received-envelopes": // This will be the case for reloading occurring in a beforeEach().
case "step-started": // This will be the case for reloading occurring in a step.
case "test-finished": // This will be the case for reloading occurring in any after-ish hook (and possibly beforeEach).
if (state.spec.relative === spec.relative) {
state = {
state: "has-reloaded",
spec: spec,
pretty: state.pretty,
messages: state.messages,
};
return;
}
// eslint-disable-next-line no-fallthrough
default:
throw createStateError("beforeSpecHandler", state.state);
}
});
exports.afterSpecHandler = createGracefullPluginEventHandler(async (config, spec, results) => {
(0, debug_1.default)("afterSpecHandler()");
if (!isFeature(spec)) {
return;
}
const preprocessor = await resolve(config, config.env, "/");
if (!preprocessor.isTrackingState) {
return;
}
(0, assertions_1.assert)(state.state !== "tracking-error", "State tracking-error should not be possible within a gracefull plugin event handler");
/**
* This pretty much can't happen and the check is merely to satisfy TypeScript in the next block.
*/
switch (state.state) {
case "uninitialized":
case "after-run":
throw createStateError("afterSpecHandler", state.state);
}
const browserCrashExprCol = [
/We detected that the .+ process just crashed/,
/We detected that the .+ Renderer process just crashed/,
];
const error = results.error;
if (error != null && browserCrashExprCol.some((expr) => expr.test(error))) {
console.log(chalk_1.default.yellow(`\nDue to browser crash, no reports are created for ${spec.relative}.`));
state = {
state: "after-spec",
pretty: state.pretty,
messages: {
accumulation: state.messages.accumulation,
},
};
return;
}
switch (state.state) {
case "test-finished": // This is the normal case.
case "before-spec": // This can happen if a spec doesn't contain any tests.
case "received-envelopes": // This can happen in case of a failing beforeEach hook.
break;
default:
throw createStateError("afterSpecHandler", state.state);
}
// `results` is undefined when running via `cypress open`.
// However, `isTrackingState` is never true in open-mode, thus this should be defined.
(0, assertions_1.assert)(results, "Expected results to be defined");
const wasRemainingSkipped = results.tests.some((test) => { var _a; return (_a = test.displayError) === null || _a === void 0 ? void 0 : _a.match(constants_1.HOOK_FAILURE_EXPR); });
if (wasRemainingSkipped) {
console.log(chalk_1.default.yellow(` Hook failures can't be represented in any reports (messages / json / html), thus none is created for ${spec.relative}.`));
state = {
state: "after-spec",
pretty: state.pretty,
messages: {
accumulation: state.messages.accumulation,
},
};
}
else {
if (state.state === "before-spec") {
// IE. the spec didn't contain any tests.
state = {
state: "after-spec",
pretty: state.pretty,
messages: {
accumulation: state.messages.accumulation,
},
};
}
else {
// The spec did contain tests.
state = {
state: "after-spec",
pretty: state.pretty,
messages: {
accumulation: (0, messages_1.orderMessages)(state.messages.accumulation.concat(state.messages.current)),
},
};
}
}
});
async function afterScreenshotHandler(config, details) {
(0, debug_1.default)("afterScreenshotHandler()");
const preprocessor = await resolve(config, config.env, "/");
if (!preprocessor.isTrackingState ||
!preprocessor.attachments.addScreenshots) {
return details;
}
switch (state.state) {
case "step-started":
break;
default:
return details;
}
let buffer;
try {
buffer = await fs_1.promises.readFile(details.path);
}
catch (_a) {
return details;
}
const message = {
attachment: {
testCaseStartedId: state.testCaseStartedId,
testStepId: state.testStepStartedId,
body: buffer.toString("base64"),
mediaType: "image/png",
contentEncoding: "BASE64",
},
};
state.messages.current.push(message);
return details;
}
exports.specEnvelopesHandler = createGracefullPluginEventHandler(async (config, data) => {
(0, debug_1.default)("specEnvelopesHandler()");
switch (state.state) {
case "before-spec":
break;
case "has-reloaded":
state = {
state: "has-reloaded-received-envelopes",
spec: state.spec,
specEnvelopes: data.messages,
pretty: state.pretty,
messages: state.messages,
};
return true;
default:
throw createStateError("specEnvelopesHandler", state.state);
}
if (state.pretty.enabled) {
for (const message of data.messages) {
state.pretty.broadcaster.emit("envelope", message);
}
}
state = {
state: "received-envelopes",
spec: state.spec,
pretty: state.pretty,
messages: {
accumulation: state.messages.accumulation,
current: data.messages,
},
};
return true;
}, true);
exports.testCaseStartedHandler = createGracefullPluginEventHandler(async (config, data) => {
(0, debug_1.default)("testCaseStartedHandler()");
switch (state.state) {
case "received-envelopes":
case "test-finished":
break;
case "has-reloaded-received-envelopes":
{
const iLastTestCaseStarted = state.messages.current.findLastIndex((message) => message.testCaseStarted);
const lastTestCaseStarted = iLastTestCaseStarted > -1
? state.messages.current[iLastTestCaseStarted]
: undefined;
// A test is being re-run.
if ((lastTestCaseStarted === null || lastTestCaseStarted === void 0 ? void 0 : lastTestCaseStarted.testCaseStarted.id) === data.id) {
if (state.pretty.enabled) {
await end(state.pretty.writable);
// Reloading occurred right within a step, so we output an extra newline.
if (state.messages.current[state.messages.current.length - 1]
.testStepStarted != null) {
console.log();
}
console.log(" Reloading..");
console.log();
const writable = createPrettyStream();
const broadcaster = (0, formatters_1.createPrettyFormatter)((0, colors_1.useColors)(), (chunk) => writable.write(chunk));
for (const message of state.specEnvelopes) {
broadcaster.emit("envelope", message);
}
state.pretty = {
enabled: true,
writable,
broadcaster,
};
}
// Discard messages of previous test, which is being re-run.
state.messages.current = state.messages.current.slice(0, iLastTestCaseStarted);
}
}
break;
default:
throw createStateError("testCaseStartedHandler", state.state);
}
if (state.pretty.enabled) {
state.pretty.broadcaster.emit("envelope", {
testCaseStarted: data,
});
}
state = {
state: "test-started",
spec: state.spec,
pretty: state.pretty,
messages: {
accumulation: state.messages.accumulation,
current: state.messages.current.concat({ testCaseStarted: data }),
},
testCaseStartedId: data.id,
};
return true;
}, true);
exports.testStepStartedHandler = createGracefullPluginEventHandler(async (config, data) => {
(0, debug_1.default)("testStepStartedHandler()");
switch (state.state) {
case "test-started":
case "step-finished":
break;
// This state can happen in cases where an error is "rescued".
case "step-started":
break;
default:
throw createStateError("testStepStartedHandler", state.state);
}
if (state.pretty.enabled) {
state.pretty.broadcaster.emit("envelope", {
testStepStarted: data,
});
}
state = {
state: "step-started",
spec: state.spec,
pretty: state.pretty,
messages: {
accumulation: state.messages.accumulation,
current: state.messages.current.concat({ testStepStarted: data }),
},
testCaseStartedId: state.testCaseStartedId,
testStepStartedId: data.testStepId,
};
return true;
}, true);
exports.testStepFinishedHandler = createGracefullPluginEventHandler(async (config, options, testStepFinished) => {
var _a;
(0, debug_1.default)("testStepFinishedHandler()");
switch (state.state) {
case "step-started":
break;
default:
throw createStateError("testStepFinishedHandler", state.state);
}
if (state.pretty.enabled) {
state.pretty.broadcaster.emit("envelope", {
testStepFinished,
});
}
const { testCaseStartedId, testStepId } = testStepFinished;
const { testCaseId: pickleId } = (0, assertions_1.ensure)(state.messages.current
.map((message) => message.testCaseStarted)
.filter(type_guards_1.notNull)
.find((testCaseStarted) => testCaseStarted.id === testCaseStartedId), "Expected to find a testCaseStarted");
const testCase = (0, assertions_1.ensure)(state.messages.current
.map((message) => message.testCase)
.filter(type_guards_1.notNull)
.find((testCase) => testCase.id === pickleId), "Expected to find a testCase");
const { pickleStepId, hookId } = (0, assertions_1.ensure)(testCase.testSteps.find((testStep) => testStep.id === testStepId), "Expected to find a testStep");
if (pickleStepId != null) {
const pickle = (0, assertions_1.ensure)(state.messages.current
.map((message) => message.pickle)
.filter(type_guards_1.notNull)
.find((pickle) => pickle.id === pickleId), "Expected to find a pickle");
const pickleStep = (0, assertions_1.ensure)(pickle.steps.find((step) => step.id === pickleStepId), "Expected to find a pickleStep");
const gherkinDocument = (0, assertions_1.ensure)(state.messages.current
.map((message) => message.gherkinDocument)
.filter(type_guards_1.notNull)
.find((gherkinDocument) => gherkinDocument.uri === pickle.uri), "Expected to find a gherkinDocument");
const attachments = [];
const attach = (data, mediaTypeOrOptions) => {
var _a;
let options;
if (mediaTypeOrOptions == null) {
options = {};
}
else if (typeof mediaTypeOrOptions === "string") {
options = { mediaType: mediaTypeOrOptions };
}
else {
options = mediaTypeOrOptions;
}
if (typeof data === "string") {
const mediaType = (_a = options.mediaType) !== null && _a !== void 0 ? _a : "text/plain";
if (mediaType.startsWith("base64:")) {
attachments.push({
data,
mediaType: mediaType.replace("base64:", ""),
encoding: messages.AttachmentContentEncoding.BASE64,
});
}
else {
attachments.push({
data,
mediaType,
encoding: messages.AttachmentContentEncoding.IDENTITY,
});
}
}
else if (data instanceof Buffer) {
if (typeof options.mediaType !== "string") {
throw Error("Buffer attachments must specify a media type");
}
attachments.push({
data: data.toString("base64"),
mediaType: options.mediaType,
encoding: messages.AttachmentContentEncoding.BASE64,
});
}
else {
throw Error("Invalid attachment data: must be a Buffer or string");
}
};
await ((_a = options.onAfterStep) === null || _a === void 0 ? void 0 : _a.call(options, {
result: testStepFinished.testStepResult,
pickle,
pickleStep,
gherkinDocument,
testCaseStartedId,
testStepId,
attach,
log: (text) => attach(text, "text/x.cucumber.log+plain"),
}));
for (const attachment of attachments) {
await (0, exports.createStringAttachmentHandler)(config, attachment);
}
}
else {
(0, assertions_1.assert)(hookId != null, "Expected a hookId in absence of pickleStepId");
}
state = {
state: "step-finished",
spec: state.spec,
pretty: state.pretty,
messages: {
accumulation: state.messages.accumulation,
current: state.messages.current.concat({ testStepFinished }),
},
testCaseStartedId: state.testCaseStartedId,
};
return true;
}, true);
exports.testCaseFinishedHandler = createGracefullPluginEventHandler(async (config, data) => {
(0, debug_1.default)("testCaseFinishedHandler()");
switch (state.state) {
case "test-started":
case "step-finished":
break;
default:
throw createStateError("testCaseFinishedHandler", state.state);
}
if (state.pretty.enabled) {
state.pretty.broadcaster.emit("envelope", {
testCaseFinished: data,
});
}
state = {
state: "test-finished",
spec: state.spec,
pretty: state.pretty,
messages: {
accumulation: state.messages.accumulation,
current: state.messages.current.concat({ testCaseFinished: data }),
},
};
return true;
}, true);
exports.createStringAttachmentHandler = createGracefullPluginEventHandler(async (config, { data, fileName, mediaType, encoding }) => {
(0, debug_1.default)("createStringAttachmentHandler()");
const preprocessor = await resolve(config, config.env, "/");
if (!preprocessor.isTrackingState) {
return true;
}
switch (state.state) {
case "step-started":
break;
default:
throw createStateError("createStringAttachmentHandler", state.state);
}
const message = {
attachment: {
testCaseStartedId: state.testCaseStartedId,
testStepId: state.testStepStartedId,
body: data,
fileName,
mediaType: mediaType,
contentEncoding: encoding,
},
};
state.messages.current.push(message);
return true;
}, true);
function frontendTrackingErrorHandler(config, data) {
state = {
state: "tracking-error",
type: "frontend",
error: data,
};
return true;
}