@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
435 lines (382 loc) • 19.3 kB
JavaScript
/*!
* 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;
});