UNPKG

@badeball/cypress-cucumber-preprocessor

Version:

[![Build status](https://github.com/badeball/cypress-cucumber-preprocessor/actions/workflows/build.yml/badge.svg)](https://github.com/badeball/cypress-cucumber-preprocessor/actions/workflows/build.yml) [![Npm package weekly downloads](https://badgen.net/n

739 lines (738 loc) 30.4 kB
"use strict"; 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; }