UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

435 lines (382 loc) 19.3 kB
/*! * OpenUI5 * (c) Copyright 2009-2021 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ "sap/ui/base/Object", "sap/ui/test/gherkin/dataTableUtils", "sap/ui/test/gherkin/simpleGherkinParser", "sap/base/strings/escapeRegExp", "sap/ui/thirdparty/jquery" ], function(UI5Object, dataTableUtils, simpleGherkinParser, escapeRegExp, jQueryDOM) { "use strict"; /** * Generates a generic FeatureTest object based on a Gherkin feature file and a steps definition object. This * FeatureTest object can then be used to easily generate tests in any test framework, e.g. QUnit. * * Full details on how Gherkin is supposed to work are provided on the Gherkin home page: * https://github.com/cucumber/cucumber/wiki/Gherkin * * The standard implementation of Gherkin is in Ruby. This is a JavaScript implementation for English only. * * This class generates a FeatureTest object. The FeatureTest object is composed of a series of TestScenarios, each of * which is composed of a series of TestSteps. Each TestStep is created by matching a Gherkin test step with a step * definition. * * A FeatureTest object looks like this: * <pre> * { // {FeatureTest} an executable object for testing a Gherkin feature * name: "Feature: Serve expensive coffee", // {string} the feature name from the Gherkin file * skip: false, // {boolean} true if the feature should not be executed * wip: false, // {boolean} true if the feature is a work in progress * testScenarios: [{ // {[TestScenario]} test scenarios to be run in this FeatureTest * name: "Scenario: Buy first coffee", // {string} the scenario name from the Gherkin file * skip: false, // {boolean} true if the scenario should not be executed * wip: false, // {boolean} true if the scenario is a work in progress * testSteps: [{ // {[TestStep]} test steps that are part of this TestScenario * isMatch: true, // {boolean} true if the Gherkin scenario matched a step definition * skip: false, // {boolean} true if the test step should not be executed * text: "coffee costs $18 per cup", // {string} the test step's text as defined in the Gherkin file * regex: /regex/, // {RegExp} the matching regular expression from step definitions * parameters: [], // {[object]} parameters derived from regular expression match * func: function(){} // {function} the matching step definition function * },{ * isMatch: true, * skip: false, * text: "I should be served a coffee", * regex: /regex/, * parameters: [], * func: function(){} * }]},{ * name: "Scenario: Buy second coffee", * skip: false, * wip: false, * testSteps: [{ * isMatch: true, * skip: false, * text: "coffee costs $18 per cup", * regex: /regex/, * parameters: [], * func: function(){} * },{ * isMatch: true, * skip: false, * text: "I should be served a second coffee", * regex: /regex/, * parameters: [], * func: function(){} * }]}] * } * </pre> * * @param {(object|string)} vFeature - a feature object generated by sap.ui.test.gherkin.simpleGherkinParser. * Alternatively, this could be the {string} path pointing to a feature file. * @param {function} fnStepDefsConstructor - the constructor for a child class of type * sap.ui.test.gherkin.StepDefinitions * @param {function} [fnAlternateTestStepGenerator] - Optional. If it's specified, this function will be executed * whenever a Gherkin test step has no matching Step Definition. The * function accepts one parameter, a Gherkin test step object with * two {string} attributes "text" and "keyword". The function * returns a TestStep object, as defined above. It is the function * writer's responsibility to prepend "(NOT FOUND)" to the "text" if * the attribute "isMatch" is set to "false". * * @class * @author Rodrigo Jordao * @author Jonathan Benn * @extends sap.ui.base.Object * @alias sap.ui.test.gherkin.GherkinTestGenerator * @since 1.40 * @private */ var GherkinTestGenerator = UI5Object.extend("sap.ui.test.gherkin.GherkinTestGenerator", /** @lends sap.ui.test.gherkin.GherkinTestGenerator.prototype */ { constructor : function(vFeature, fnStepDefsConstructor, fnAlternateTestStepGenerator) { UI5Object.apply(this, arguments); if (typeof vFeature === "string" || vFeature instanceof String) { vFeature = simpleGherkinParser.parseFile(vFeature); // else if the type is not a String and not a Feature object (or not given) } else if (!vFeature || (typeof vFeature !== "object") || !vFeature.scenarios) { throw new Error("GherkinTestGenerator constructor: parameter 'vFeature' must be a valid String or a valid Feature object"); } if ((typeof fnStepDefsConstructor !== "function") || !((new fnStepDefsConstructor())._generateTestStep)) { throw new Error("GherkinTestGenerator constructor: parameter 'fnStepDefsConstructor' must be a valid StepDefinitions constructor"); } if (fnAlternateTestStepGenerator && typeof fnAlternateTestStepGenerator !== "function") { throw new Error("GherkinTestGenerator constructor: if specified, parameter 'fnAlternateTestStepGenerator' must be a valid Function"); } /** * {Feature} a feature object generated by sap.ui.test.gherkin.simpleGherkinParser * * @see sap.ui.test.gherkin.simpleGherkinParser * @private */ this._oFeature = vFeature; /** * {function} the constructor for a child class of type sap.ui.test.gherkin.StepDefinitions * * @see sap.ui.test.gherkin.StepDefinitions * @private */ this._fnStepDefsConstructor = fnStepDefsConstructor; /** * {StepDefinitions} the concrete StepDefinitions object that holds an array of step definitions * * @see sap.ui.test.gherkin.StepDefinitions * @private */ this._oStepDefs = null; /** * {function} generates and returns a TestStep object. If this function is defined, it will be executed whenever * we fail to find a matching Step Definition for a Gherkin step. * * @private */ this._fnAlternateTestStepGenerator = fnAlternateTestStepGenerator || null; }, /** * Creates a new shared context for running tests. Execute this method before testing a new scenario. * * @public */ setUp: function() { this._oStepDefs = new this._fnStepDefsConstructor(); }, /** * If any tests were run, executes the Step Definitions' "closeApplication" method. Also clears the shared * context for running tests. * * @public */ tearDown: function() { if (this._oStepDefs && this._oStepDefs._needsTearDown) { this._oStepDefs.closeApplication(); } this._oStepDefs = null; }, /** * Creates a FeatureTest object, which is generated by stitching together the Gherkin Feature File and Step * Definitions file inputed to the constructor. * * @returns {FeatureTest} an executable test object * @public */ generate : function() { if (!this._oStepDefs) { this.setUp(); } return this._generateFeatureTest(); }, /** * Executes the given TestStep in the shared context of the Step Definitions, with the correct parameters. The * TestStep will not be executed if it should be skipped, in which case this method will return "false". * * @param {TestStep} oTestStep - the test step to execute, obtained from the result of "generate". * @param {boolean} oTestStep.skip - false if the test step should be executed, otherwise true * @param {function} [oTestStep.func] - the step definition function to execute (ignored if "oTestStep.skip" is * true) * @param {any[]} [oTestStep.parameters] - the parameters to pass to "oTestStep.func" during its execution * (ignored if "oTestStep.skip" is true) * @param {object} [assert] - (optional) the QUnit local assert object for this test * @returns {boolean} true if the test step was executed, otherwise false * @throws {Error} if you attempt to call this method before calling "generate" or after calling "tearDown", or if * oTestStep is an invalid TestStep object * @public */ execute: function(oTestStep, assert) { if (!this._oStepDefs) { throw new Error("Run 'generate' before calling 'execute'"); } if (!oTestStep || (!oTestStep.skip && ((typeof oTestStep.func !== "function") || !Array.isArray(oTestStep.parameters)))) { throw new Error("Input parameter 'oTestStep' is not a valid TestStep object."); } // If this test step should not be skipped if (!oTestStep.skip) { // then execute the test step in the Step Definitions shared context this._oStepDefs.assert = assert; oTestStep.func.apply(this._oStepDefs, oTestStep.parameters); this._oStepDefs._needsTearDown = true; } return (!oTestStep.skip); }, /** * Creates an executable feature test, composed of 0 or more test scenarios, each of which is composed of 0 or more * basic test steps. The generated feature test is based on Gherkin document and step definitions fed into the * constructor. * * @private */ _generateFeatureTest: function() { var aExpandedScenarios = []; this._oFeature.scenarios.forEach(function(oScenario) { aExpandedScenarios = aExpandedScenarios.concat(this._expandScenarioOutline(oScenario)); }, this); var aTestScenarios = aExpandedScenarios.map(function(oScenario) { return this._generateTestScenario(oScenario, this._oFeature.background); }, this); var bFeatureIsWip = this._isWip(this._oFeature); var bAllScenariosAreSkipped = aTestScenarios.every(function(oTestScenario) {return oTestScenario.skip;}); return { name: ((bFeatureIsWip) ? "(WIP) " : "") + "Feature: " + this._oFeature.name, skip: bFeatureIsWip || bAllScenariosAreSkipped, wip: bFeatureIsWip, testScenarios: aTestScenarios }; }, /** * Expands a scenario outline into 1 or more concrete scenarios (one concrete scenario for each line in the examples). * * Each concrete scenario will have ": <Examples name> #1", ": <Examples name> #2", etc. appended to its text to * differentiate it. * * @param {Scenario} oScenario - a Gherkin scenario that may or may not be a scenario outline * @returns {Scenario[]} - an array of 1 or more scenarios. If the input was a scenario outline, then the output * is an array of 1 or more concrete scenarios that implement that outline. One concrete * scenario is generated per example line specified in the feature file. If the input was a * regular scenario, then the return value is an array with 1 element: the inputed * scenario. * @private */ _expandScenarioOutline: function(oScenario) { // if this is not a scenario outline OR it's a scenario outline with no active Examples if (!this._isScenarioOutlineWithExamples(oScenario)) { // then don't change anything return [oScenario]; } // else this is a scenario outline with at least one set of Examples var aConcreteScenarios = []; // for each set of active Examples in the Scenario Outline oScenario.examples.filter(this._isNotWip).forEach(function(oExample, i) { var aConvertedExamples = this._convertScenarioExamplesToListOfObjects(oExample.data); // create a concrete scenario from each example in the Examples aConcreteScenarios = aConcreteScenarios.concat(aConvertedExamples.map(function(oConvertedExample, i) { var oScenarioCopy = jQueryDOM.extend(true, {}, oScenario); oScenarioCopy.name += (oExample.name) ? ": " + oExample.name : ""; oScenarioCopy.name += " #" + (i + 1); // for each variable specified for this concrete example jQueryDOM.each(oConvertedExample, function(sVariableName, sVariableValue) { // for each test step in the scenario oScenarioCopy.steps.forEach(function(oStep) { // in the scenario text, replace all occurences of the variable with the concrete value var sEscapedVariableName = escapeRegExp(sVariableName); oStep.text = oStep.text.replace(new RegExp("<" + sEscapedVariableName + ">", "g"), sVariableValue); }); }); return oScenarioCopy; }, this)); }, this); return aConcreteScenarios; }, /** * Prepares an executable test scenario based on a Gherkin document's scenario. * * @param {Scenario} oScenario - the Gherkin scenario for which to generate tests * @param {Scenario} [oBackground] - the Gherkin background scenario that must be run before each regular scenario * @returns {TestScenario} - the test scenario to be executed during testing * @see sap.ui.test.gherkin.simpleGherkinParser#parse * @private */ _generateTestScenario: function(oScenario, oBackground) { var bWip = this._isWip(oScenario); var sScenarioPrependText = this._isScenarioOutline(oScenario) ? "Scenario Outline: " : "Scenario: "; var sScenarioName = (bWip ? "(WIP) " : "") + sScenarioPrependText + oScenario.name; var aTestSteps = (oBackground) ? this._generateTestSteps(bWip, oBackground, false) : []; var bNoActiveExamples = this._isScenarioOutline(oScenario) && !this._isScenarioOutlineWithExamples(oScenario); var bSkip = bNoActiveExamples || aTestSteps.some(function(o) {return !o.isMatch;}); aTestSteps = aTestSteps.concat(this._generateTestSteps(bWip, oScenario, bSkip)); return { name: sScenarioName, skip: bWip || bNoActiveExamples || aTestSteps.every(function(o) {return o.skip && o.isMatch;}), wip: bWip, testSteps: aTestSteps }; }, /** * Creates all the tests for all of the given scenario's steps. It does this by stitching together the Gherkin * specification's steps with the JavaScript step definitions. If any Gherkin step cannot be matched to a * JavaScript step definition then that test and all remaining tests will be skipped. * * If the _fnAlternateTestStepGenerator is defined then it will be used to generate a TestStep if we fail to match * a step definition. * * @param {boolean} bIsWip - true if this test is a work in progress that should be skipped * @param {Scenario} oScenario - the Gherkin scenario on which to base these generated tests * @param {boolean} bSkipping - true if this scenario and all of its steps should be skipped (e.g. because the * background step was not found) * @returns {TestStep[]} - the list of test step objects to be executed during testing * @see sap.ui.test.gherkin.simpleGherkinParser#parse * @private */ _generateTestSteps: function(bIsWip, oScenario, bSkipping) { var aTestSteps = []; for (var i = 0; i < oScenario.steps.length; ++i) { var oStep = oScenario.steps[i]; var oTestStep = this._oStepDefs._generateTestStep(oStep); // if there is no matching regular expression and there is an alternate TestStep generator if (!oTestStep.isMatch && this._fnAlternateTestStepGenerator) { // then use the alternate function to generate a TestStep oTestStep = this._fnAlternateTestStepGenerator(oStep); } // If there is still not a match if (!oTestStep.isMatch) { // then we will skip this and all future test steps bSkipping = true; } oTestStep.skip = bSkipping || bIsWip; if (oTestStep.isMatch && oTestStep.skip) { oTestStep.text = "(SKIPPED) " + oTestStep.text; } aTestSteps.push(oTestStep); } return aTestSteps; }, /** * Converts the given scenario outline examples into a list of objects * * @param {string[]} aExamples - scenario outline examples, can be a 1D or 2D array * @returns {object[]} - a list of objects equivalent to the input data * @see sap.ui.test.gherkin.dataTableUtils.toTable * @private */ _convertScenarioExamplesToListOfObjects: function(aExamples) { // if aExamples is a simple list then convert from simple list to list-of-lists before executing toTable aExamples = aExamples.map(function(i){return (typeof i === "string" || i instanceof String) ? [i] : i;}); return dataTableUtils.toTable(aExamples); }, /** * @param {Scenario} oScenario - a Gherkin scenario (as created by the parser) * @returns true if the given scenario is a scenario outline * @private */ _isScenarioOutline: function(oScenario) { return !!oScenario.examples; }, /** * @param {Scenario} oScenario - a Gherkin scenario (as created by the parser) * @returns true if the given scenario is a scenario outline AND it has active (non-WIP) Examples * @private */ _isScenarioOutlineWithExamples: function(oScenario) { return !!oScenario.examples && (oScenario.examples.length !== 0) && oScenario.examples.some(this._isNotWip); }, /** * @param {object} oObject - any object with a 'tags' attribute (tags are of type array) * @returns true if the given oObject does not have an '@wip' tag * @private */ _isNotWip: function(oObject) { return (jQueryDOM.inArray("@wip", oObject.tags) === -1); }, /** * @param {object} oObject - any object with a 'tags' attribute (tags are of type array) * @returns true if the given oObject has an '@wip' tag * @private */ _isWip: function(oObject) { return !this._isNotWip(oObject); } }); return GherkinTestGenerator; });