├── project ├── build.properties ├── build-dependencies.sbt ├── plugins.sbt ├── I18nGenerator.scala └── ScalacOptions.scala ├── .jvmopts ├── version.sbt ├── .git-blame-ignore-revs ├── .scalafmt.conf ├── integration-tests ├── common │ └── src │ │ └── test │ │ ├── scala │ │ ├── cukes │ │ │ ├── model │ │ │ │ ├── Cuke.scala │ │ │ │ ├── Person.scala │ │ │ │ └── Snake.scala │ │ │ ├── RunCukesTest.scala │ │ │ ├── TypeRegistryConfiguration.scala │ │ │ └── StepDefs.scala │ │ ├── misc │ │ │ ├── RunMiscTest.scala │ │ │ └── OptionalCaptureGroupsSteps.scala │ │ ├── object │ │ │ ├── RunObjectTest.scala │ │ │ └── ObjectSteps.scala │ │ ├── isolated │ │ │ ├── RunIsolatedTest.scala │ │ │ └── IsolatedSteps.scala │ │ ├── docstring │ │ │ ├── RunDocStringTest.scala │ │ │ └── DocStringSteps.scala │ │ ├── datatables │ │ │ ├── RunDatatablesTest.scala │ │ │ └── DatatableSteps.scala │ │ ├── parametertypes │ │ │ ├── RunParameterTypesTest.scala │ │ │ └── ParameterTypesSteps.scala │ │ └── statichooks │ │ │ ├── StaticHooksSteps.scala │ │ │ └── RunStaticHooksTest.scala │ │ └── resources │ │ ├── isolated │ │ ├── isolated2.feature │ │ └── isolated.feature │ │ ├── junit-platform.properties │ │ ├── misc │ │ └── OptionalCaptureGroups.feature │ │ ├── object │ │ └── object.feature │ │ ├── statichooks │ │ ├── statichooks.feature │ │ └── statichooks2.feature │ │ ├── docstring │ │ └── Docstring.feature │ │ ├── datatables │ │ ├── Datatable.feature │ │ ├── DatatableAsScala.feature │ │ └── DataTableType.feature │ │ ├── parametertypes │ │ └── ParameterTypes.feature │ │ └── cukes │ │ └── cukes.feature ├── jackson2 │ └── src │ │ └── test │ │ ├── resources │ │ ├── junit-platform.properties │ │ └── jackson │ │ │ └── Jackson.feature │ │ └── scala │ │ └── jackson │ │ ├── RunJacksonTest.scala │ │ └── JacksonSteps.scala ├── jackson3 │ └── src │ │ └── test │ │ ├── resources │ │ ├── junit-platform.properties │ │ └── jackson3 │ │ │ └── Jackson3.feature │ │ └── scala │ │ └── jackson3 │ │ ├── RunJackson3Test.scala │ │ └── Jackson3Steps.scala └── picocontainer │ └── src │ └── test │ ├── resources │ ├── junit-platform.properties │ └── di │ │ └── di.feature │ └── scala │ └── di │ ├── DI_A.scala │ ├── DI_B.scala │ ├── DI_C.scala │ └── RunDependencyInjectionTest.scala ├── cucumber-scala └── src │ ├── main │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── io.cucumber.core.backend.BackendProviderService │ ├── scala │ │ └── io │ │ │ └── cucumber │ │ │ └── scala │ │ │ ├── PendingException.scala │ │ │ ├── package.html │ │ │ ├── ScalaStaticHookDetails.scala │ │ │ ├── ScalaHookDetails.scala │ │ │ ├── ScalaParameterTypeDetails.scala │ │ │ ├── ScalaTypeResolver.scala │ │ │ ├── ScalaDsl.scala │ │ │ ├── BaseScalaDsl.scala │ │ │ ├── ScalaDocStringTypeDetails.scala │ │ │ ├── UnknownClassType.scala │ │ │ ├── ScalaParameterInfo.scala │ │ │ ├── ScalaBackendProviderService.scala │ │ │ ├── HookType.scala │ │ │ ├── Aliases.scala │ │ │ ├── ScalaDefaultTransformerDetails.scala │ │ │ ├── DocStringTypeDsl.scala │ │ │ ├── DataTableDefinitionBody.scala │ │ │ ├── ScalaStaticHookDefinition.scala │ │ │ ├── ScalaDataTableDefinition.scala │ │ │ ├── ScalaStepDetails.scala │ │ │ ├── ScalaDataTableCellDefinition.scala │ │ │ ├── ScalaDataTableOptionalCellDefinition.scala │ │ │ ├── Utils.scala │ │ │ ├── IncorrectStepDefinitionException.scala │ │ │ ├── ScalaDataTableRowDefinition.scala │ │ │ ├── ScalaDataTableEntryDefinition.scala │ │ │ ├── ScalaDataTableOptionalRowDefinition.scala │ │ │ ├── ScalaDataTableOptionalEntryDefinition.scala │ │ │ ├── AbstractGlueDefinition.scala │ │ │ ├── ScalaDocStringTypeDefinition.scala │ │ │ ├── JacksonDefaultDataTableEntryTransformer.scala │ │ │ ├── ScalaDefaultParameterTransformerDefinition.scala │ │ │ ├── ScalaParameterTypeDefinition.scala │ │ │ ├── ScalaStepDefinition.scala │ │ │ ├── Jackson3DefaultDataTableEntryTransformer.scala │ │ │ ├── ScalaHookDefinition.scala │ │ │ ├── ScalaDefaultDataTableCellTransformerDefinition.scala │ │ │ ├── ScalaDataTableTypeDetails.scala │ │ │ ├── ScalaDefaultDataTableEntryTransformerDefinition.scala │ │ │ ├── IncorrectHookDefinitionException.scala │ │ │ ├── AbstractDatatableElementTransformerDefinition.scala │ │ │ ├── ScalaSnippet.scala │ │ │ ├── ScalaDataTableTypeDefinition.scala │ │ │ ├── GlueAdaptor.scala │ │ │ ├── DataTableTypeDsl.scala │ │ │ ├── ScalaBackend.scala │ │ │ ├── DefaultTransformerDsl.scala │ │ │ └── Scenario.scala │ └── codegen │ │ └── gen.scala │ └── test │ └── scala │ └── io │ └── cucumber │ └── scala │ ├── steps │ ├── classes │ │ ├── SingleFile.scala │ │ └── MultipleInSameFile.scala │ ├── dependencyinjection │ │ ├── Injector.scala │ │ └── Injected.scala │ ├── errors │ │ ├── staticclasshooks │ │ │ └── StaticClassHooksDefinition.scala │ │ ├── incorrectclasshooks │ │ │ └── IncorrectClassHooksDefinition.scala │ │ └── incorrectobjecthooks │ │ │ └── IncorrectObjectHooksDefinition.scala │ ├── traits │ │ └── StepsInTrait.scala │ └── objects │ │ └── StepsInObject.scala │ ├── TestFeatureParser.scala │ ├── ScalaDslDocStringTypeTest.scala │ ├── ScalaDslDefaultParameterTransformerTest.scala │ ├── ScalaDslParameterTypeTest.scala │ ├── ScalaDslDefaultDataTableEntryTransformerTest.scala │ └── ScalaDslDefaultDataTableCellTransformerTest.scala ├── renovate.json ├── examples ├── examples-junit4 │ ├── README.md │ └── src │ │ ├── test │ │ ├── resources │ │ │ └── cucumber │ │ │ │ └── examples │ │ │ │ └── scalacalculator │ │ │ │ └── basic_arithmetic.feature │ │ └── scala │ │ │ └── cucumber │ │ │ └── examples │ │ │ └── scalacalculator │ │ │ ├── RunCukesTest.scala │ │ │ └── RpnCalculatorStepDefinitions.scala │ │ └── main │ │ └── scala │ │ └── cucumber │ │ └── examples │ │ └── scalacalculator │ │ └── RpnCalculator.scala └── examples-junit5 │ ├── README.md │ └── src │ ├── test │ ├── resources │ │ ├── junit-platform.properties │ │ └── cucumber │ │ │ └── examples │ │ │ └── scalacalculator │ │ │ └── basic_arithmetic.feature │ └── scala │ │ └── cucumber │ │ └── examples │ │ └── scalacalculator │ │ ├── RunCukesTest.scala │ │ └── RpnCalculatorStepDefinitions.scala │ └── main │ └── scala │ └── cucumber │ └── examples │ └── scalacalculator │ └── RpnCalculator.scala ├── .gitignore ├── .github ├── settings.yml └── workflows │ ├── version-policy-check.yml │ ├── build.yml │ └── release-sbt.yml ├── CONTRIBUTING.md ├── scripts ├── remove-empty-sections-changelog.awk ├── update-install-doc.sh └── update-changelog.sh ├── docs ├── install.md ├── upgrade_v8.md ├── step_definitions.md ├── upgrade_v7.md ├── upgrade_v6.md ├── default_jackson_datatable_transformer.md ├── build.md ├── scala_implementation.md ├── upgrade_v5.md ├── hooks.md └── usage.md ├── Makefile ├── LICENCE ├── RELEASING.md ├── .devcontainer └── devcontainer.json └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.7 2 | -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -Dsun.net.client.defaultReadTimeout=60000 -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "8.38.1-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # scalafmt 2 | f48a978dfc46fabf7acdcab70a90b242c5565caa 3 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.10.2 2 | 3 | preset=default 4 | 5 | runner.dialect=scala3 6 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/cukes/model/Cuke.scala: -------------------------------------------------------------------------------- 1 | package cukes.model 2 | 3 | case class Cukes(number: Int, color: String) 4 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/resources/META-INF/services/io.cucumber.core.backend.BackendProviderService: -------------------------------------------------------------------------------- 1 | io.cucumber.scala.ScalaBackendProviderService -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>cucumber/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /examples/examples-junit4/README.md: -------------------------------------------------------------------------------- 1 | # Cucumber Scala Example - JUnit 4 2 | 3 | This project is an example of Cucumber Scala integration in a Scala SBT project with JUnit 4. 4 | -------------------------------------------------------------------------------- /examples/examples-junit5/README.md: -------------------------------------------------------------------------------- 1 | # Cucumber Scala Example - JUnit 5 2 | 3 | This project is an example of Cucumber Scala integration in a Scala SBT project with JUnit 5. 4 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/PendingException.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | class PendingException extends RuntimeException("TODO: implement me") {} 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | **/*.iml 3 | **/target/ 4 | .bsp/ 5 | release.properties 6 | pom.xml.releaseBackup 7 | pom.xml.versionsBackup 8 | .metals/ 9 | .vscode/ 10 | .bloop/ 11 | metals.sbt -------------------------------------------------------------------------------- /project/build-dependencies.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies += "io.cucumber" % "cucumber-core" % "7.33.0" 2 | libraryDependencies += "org.scala-lang.modules" %% "scala-collection-compat" % "2.14.0" 3 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/package.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | See the Scala API for cucumber-jvm-scala. 4 |

5 | 6 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | 3 | repository: 4 | name: cucumber-jvm-scala 5 | description: Cucumber Scala 6 | 7 | collaborators: 8 | - username: gaeljw 9 | permission: maintain 10 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/isolated/isolated2.feature: -------------------------------------------------------------------------------- 1 | Feature: Isolated 2 2 | 3 | Scenario: Second test 4 | Given I set the list of values to 5 | | 10 | 6 | And I multiply by 2 7 | Then the list of values is 8 | | 20 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | # Workaround for https://github.com/sbt/sbt-jupiter-interface/issues/142 2 | # See also https://github.com/cucumber/cucumber-jvm/pull/3023 3 | cucumber.junit-platform.discovery.as-root-engine=false -------------------------------------------------------------------------------- /integration-tests/jackson2/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | # Workaround for https://github.com/sbt/sbt-jupiter-interface/issues/142 2 | # See also https://github.com/cucumber/cucumber-jvm/pull/3023 3 | cucumber.junit-platform.discovery.as-root-engine=false -------------------------------------------------------------------------------- /integration-tests/jackson3/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | # Workaround for https://github.com/sbt/sbt-jupiter-interface/issues/142 2 | # See also https://github.com/cucumber/cucumber-jvm/pull/3023 3 | cucumber.junit-platform.discovery.as-root-engine=false -------------------------------------------------------------------------------- /integration-tests/picocontainer/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | # Workaround for https://github.com/sbt/sbt-jupiter-interface/issues/142 2 | # See also https://github.com/cucumber/cucumber-jvm/pull/3023 3 | cucumber.junit-platform.discovery.as-root-engine=false -------------------------------------------------------------------------------- /examples/examples-junit5/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | cucumber.plugin=pretty 2 | # Workaround for https://github.com/sbt/sbt-jupiter-interface/issues/142 3 | # See also https://github.com/cucumber/cucumber-jvm/pull/3023 4 | cucumber.junit-platform.discovery.as-root-engine=false -------------------------------------------------------------------------------- /examples/examples-junit4/src/test/resources/cucumber/examples/scalacalculator/basic_arithmetic.feature: -------------------------------------------------------------------------------- 1 | @foo 2 | Feature: Basic Arithmetic 3 | 4 | Scenario: Adding 5 | # Try to change one of the values below to provoke a failure 6 | When I add 4.0 and 5.0 7 | Then the result is 9.0 8 | -------------------------------------------------------------------------------- /examples/examples-junit5/src/test/resources/cucumber/examples/scalacalculator/basic_arithmetic.feature: -------------------------------------------------------------------------------- 1 | @foo 2 | Feature: Basic Arithmetic 3 | 4 | Scenario: Adding 5 | # Try to change one of the values below to provoke a failure 6 | When I add 4.0 and 5.0 7 | Then the result is 9.0 8 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/steps/classes/SingleFile.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala.steps.classes 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | 5 | class StepsC extends ScalaDsl with EN { 6 | 7 | Then("""stepsC""") { () => 8 | // Nothing 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/misc/OptionalCaptureGroups.feature: -------------------------------------------------------------------------------- 1 | Feature: Optional capture groups are supported 2 | 3 | Scenario: present, using Java's Optional 4 | Given I have the name: Jack 5 | 6 | Scenario: absent, using Java's Optional 7 | Given I don't have the name: 8 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/cukes/model/Person.scala: -------------------------------------------------------------------------------- 1 | package cukes.model 2 | 3 | /** Test model for a "Person" 4 | * @param name 5 | * of person 6 | */ 7 | case class Person(name: String) { 8 | 9 | def hello = { 10 | "Hello, I'm " + name + "!" 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/isolated/isolated.feature: -------------------------------------------------------------------------------- 1 | Feature: Isolated 2 | 3 | Scenario: First test 4 | Given I set the list of values to 5 | | 1 | 6 | | 2 | 7 | | 3 | 8 | And I multiply by 2 9 | Then the list of values is 10 | | 2 | 11 | | 4 | 12 | | 6 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaStaticHookDetails.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.scala.Aliases.StaticHookDefinitionBody 4 | 5 | case class ScalaStaticHookDetails( 6 | order: Int, 7 | body: StaticHookDefinitionBody, 8 | stackTraceElement: StackTraceElement 9 | ) 10 | -------------------------------------------------------------------------------- /integration-tests/picocontainer/src/test/scala/di/DI_A.scala: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | 5 | class DI_A extends ScalaDsl with EN { 6 | 7 | var input: String = _ 8 | 9 | Given("""a step defined in class DI-A with arg {string}""") { (arg: String) => 10 | input = arg 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /integration-tests/picocontainer/src/test/scala/di/DI_B.scala: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | 5 | class DI_B extends ScalaDsl with EN { 6 | 7 | var input: String = _ 8 | 9 | Given("""a step defined in class DI-B with arg {string}""") { (arg: String) => 10 | input = arg 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaHookDetails.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import Aliases.HookDefinitionBody 4 | 5 | case class ScalaHookDetails( 6 | tagExpression: String, 7 | order: Int, 8 | body: HookDefinitionBody, 9 | stackTraceElement: StackTraceElement, 10 | hookType: ScopedHookType 11 | ) 12 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/steps/dependencyinjection/Injector.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala.steps.dependencyinjection 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | 5 | class Injector(injected: Injected) extends ScalaDsl with EN { 6 | 7 | Then("""Injector""") { () => 8 | println(injected.x) 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaParameterTypeDetails.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import scala.reflect.ClassTag 4 | 5 | case class ScalaParameterTypeDetails[R]( 6 | name: String, 7 | regex: String, 8 | body: List[String] => R, 9 | tag: ClassTag[R], 10 | stackTraceElement: StackTraceElement 11 | ) 12 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/steps/dependencyinjection/Injected.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala.steps.dependencyinjection 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | 5 | class Injected extends ScalaDsl with EN { 6 | 7 | var x: String = _ 8 | 9 | Given("""injected steps""") { () => 10 | // Nothing 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/cukes/model/Snake.scala: -------------------------------------------------------------------------------- 1 | package cukes.model 2 | 3 | /** Test model "Snake" to exercise the custom mapper functionality 4 | * 5 | * @param length 6 | * of the snake in characters 7 | * @param direction 8 | * in which snake is moving 'west, 'east, etc 9 | */ 10 | case class Snake(length: Int, direction: Symbol) {} 11 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaTypeResolver.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.lang.reflect.Type 4 | 5 | import io.cucumber.core.backend.TypeResolver 6 | 7 | class ScalaTypeResolver(val `type`: Type) extends TypeResolver { 8 | 9 | override def resolve(): Type = { 10 | // No fancy logic needed 11 | `type` 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDsl.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | /** Base trait for a scala step definition implementation. 4 | */ 5 | trait ScalaDsl 6 | extends BaseScalaDsl 7 | with StepDsl 8 | with HookDsl 9 | with DataTableTypeDsl 10 | with DocStringTypeDsl 11 | with ParameterTypeDsl 12 | with DefaultTransformerDsl {} 13 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/BaseScalaDsl.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | private[scala] trait BaseScalaDsl { 4 | 5 | val NO_REPLACEMENT = Seq[String]() 6 | val EMPTY_TAG_EXPRESSION = "" 7 | val DEFAULT_BEFORE_ORDER = 1000 8 | val DEFAULT_AFTER_ORDER = 1000 9 | 10 | private[scala] val registry: ScalaDslRegistry = new ScalaDslRegistry() 11 | 12 | } 13 | -------------------------------------------------------------------------------- /examples/examples-junit4/src/test/scala/cucumber/examples/scalacalculator/RunCukesTest.scala: -------------------------------------------------------------------------------- 1 | package cucumber.examples.scalacalculator 2 | 3 | import io.cucumber.junit.{Cucumber, CucumberOptions} 4 | import org.junit.runner.RunWith 5 | 6 | import scala.annotation.nowarn 7 | 8 | @nowarn 9 | @RunWith(classOf[Cucumber]) 10 | @CucumberOptions(plugin = Array("pretty")) 11 | class RunCukesTest 12 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/cukes/RunCukesTest.scala: -------------------------------------------------------------------------------- 1 | package cukes 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api._ 5 | 6 | @Suite 7 | @IncludeEngines(Array("cucumber")) 8 | @SelectPackages(Array("cukes")) 9 | @ConfigurationParameter( 10 | key = Constants.GLUE_PROPERTY_NAME, 11 | value = "cukes" 12 | ) 13 | class RunCukesTest 14 | -------------------------------------------------------------------------------- /integration-tests/picocontainer/src/test/resources/di/di.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to be able to have some step classes depend on another one 2 | 3 | Scenario: Nominal case 4 | Given a step defined in class DI-A with arg "A" 5 | And a step defined in class DI-B with arg "B" 6 | When a step defined in class DI-C uses them both 7 | Then both values are combined into "AB" 8 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDocStringTypeDetails.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.scala.Aliases.DocStringDefinitionBody 4 | 5 | import java.lang.reflect.{Type => JType} 6 | 7 | case class ScalaDocStringTypeDetails[T]( 8 | contentType: String, 9 | body: DocStringDefinitionBody[T], 10 | `type`: JType, 11 | stackTraceElement: StackTraceElement 12 | ) 13 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/UnknownClassType.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.CucumberBackendException 4 | 5 | class UnknownClassType(clazz: Class[_], cause: Throwable) 6 | extends CucumberBackendException( 7 | s"Cucumber was not able to handle class ${clazz.getName}. Please report this issue to cucumber-scala project.", 8 | cause 9 | ) 10 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/object/object.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to be able to use steps defined in objects even though they will persist their state across scenarios 2 | 3 | Scenario: First scenario 4 | Given I have a calculator 5 | When I do 2 + 2 6 | Then I got 4 7 | 8 | Scenario: Second scenario 9 | Given I have a calculator 10 | When I do 5 + 6 11 | Then I got 11 12 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/misc/RunMiscTest.scala: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api.{ 5 | ConfigurationParameter, 6 | IncludeEngines, 7 | SelectPackages, 8 | Suite 9 | } 10 | 11 | @Suite 12 | @IncludeEngines(Array("cucumber")) 13 | @SelectPackages(Array("misc")) 14 | @ConfigurationParameter( 15 | key = Constants.GLUE_PROPERTY_NAME, 16 | value = "misc" 17 | ) 18 | class RunMiscTest 19 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/object/RunObjectTest.scala: -------------------------------------------------------------------------------- 1 | package `object` 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api.{ 5 | ConfigurationParameter, 6 | IncludeEngines, 7 | SelectPackages, 8 | Suite 9 | } 10 | 11 | @Suite 12 | @IncludeEngines(Array("cucumber")) 13 | @SelectPackages(Array("object")) 14 | @ConfigurationParameter( 15 | key = Constants.GLUE_PROPERTY_NAME, 16 | value = "object" 17 | ) 18 | class RunObjectTest 19 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaParameterInfo.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.lang.reflect.Type 4 | 5 | import io.cucumber.core.backend.{ParameterInfo, TypeResolver} 6 | 7 | class ScalaParameterInfo(typeResolver: ScalaTypeResolver) 8 | extends ParameterInfo { 9 | 10 | override def getType: Type = typeResolver.`type` 11 | 12 | override def isTransposed: Boolean = false 13 | 14 | override def getTypeResolver: TypeResolver = typeResolver 15 | 16 | } 17 | -------------------------------------------------------------------------------- /examples/examples-junit5/src/test/scala/cucumber/examples/scalacalculator/RunCukesTest.scala: -------------------------------------------------------------------------------- 1 | package cucumber.examples.scalacalculator 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api._ 5 | 6 | @Suite 7 | @IncludeEngines(Array("cucumber")) 8 | @SelectPackages(Array("cucumber.examples.scalacalculator")) 9 | @ConfigurationParameter( 10 | key = Constants.GLUE_PROPERTY_NAME, 11 | value = "cucumber.examples.scalacalculator" 12 | ) 13 | class RunCukesTest 14 | -------------------------------------------------------------------------------- /integration-tests/jackson2/src/test/scala/jackson/RunJacksonTest.scala: -------------------------------------------------------------------------------- 1 | package jackson 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api.{ 5 | ConfigurationParameter, 6 | IncludeEngines, 7 | SelectPackages, 8 | Suite 9 | } 10 | 11 | @Suite 12 | @IncludeEngines(Array("cucumber")) 13 | @SelectPackages(Array("jackson")) 14 | @ConfigurationParameter( 15 | key = Constants.GLUE_PROPERTY_NAME, 16 | value = "jackson" 17 | ) 18 | class RunJacksonTest 19 | -------------------------------------------------------------------------------- /integration-tests/picocontainer/src/test/scala/di/DI_C.scala: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | 5 | class DI_C(a: DI_A, b: DI_B) extends ScalaDsl with EN { 6 | 7 | private var combination: String = _ 8 | 9 | When("""a step defined in class DI-C uses them both""") { () => 10 | combination = a.input + b.input 11 | } 12 | 13 | Then("""both values are combined into {string}""") { (expected: String) => 14 | assert(combination == expected) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/isolated/RunIsolatedTest.scala: -------------------------------------------------------------------------------- 1 | package isolated 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api.{ 5 | ConfigurationParameter, 6 | IncludeEngines, 7 | SelectPackages, 8 | Suite 9 | } 10 | 11 | @Suite 12 | @IncludeEngines(Array("cucumber")) 13 | @SelectPackages(Array("isolated")) 14 | @ConfigurationParameter( 15 | key = Constants.GLUE_PROPERTY_NAME, 16 | value = "isolated" 17 | ) 18 | class RunIsolatedTest 19 | -------------------------------------------------------------------------------- /integration-tests/jackson3/src/test/scala/jackson3/RunJackson3Test.scala: -------------------------------------------------------------------------------- 1 | package jackson3 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api.{ 5 | ConfigurationParameter, 6 | IncludeEngines, 7 | SelectPackages, 8 | Suite 9 | } 10 | 11 | @Suite 12 | @IncludeEngines(Array("cucumber")) 13 | @SelectPackages(Array("jackson3")) 14 | @ConfigurationParameter( 15 | key = Constants.GLUE_PROPERTY_NAME, 16 | value = "jackson3" 17 | ) 18 | class RunJackson3Test 19 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/docstring/RunDocStringTest.scala: -------------------------------------------------------------------------------- 1 | package docstring 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api.{ 5 | ConfigurationParameter, 6 | IncludeEngines, 7 | SelectPackages, 8 | Suite 9 | } 10 | 11 | @Suite 12 | @IncludeEngines(Array("cucumber")) 13 | @SelectPackages(Array("docstring")) 14 | @ConfigurationParameter( 15 | key = Constants.GLUE_PROPERTY_NAME, 16 | value = "docstring" 17 | ) 18 | class RunDocStringTest 19 | -------------------------------------------------------------------------------- /integration-tests/picocontainer/src/test/scala/di/RunDependencyInjectionTest.scala: -------------------------------------------------------------------------------- 1 | package di 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api.{ 5 | ConfigurationParameter, 6 | IncludeEngines, 7 | SelectPackages, 8 | Suite 9 | } 10 | 11 | @Suite 12 | @IncludeEngines(Array("cucumber")) 13 | @SelectPackages(Array("di")) 14 | @ConfigurationParameter( 15 | key = Constants.GLUE_PROPERTY_NAME, 16 | value = "di" 17 | ) 18 | class RunDependencyInjectionTest 19 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/datatables/RunDatatablesTest.scala: -------------------------------------------------------------------------------- 1 | package datatables 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api.{ 5 | ConfigurationParameter, 6 | IncludeEngines, 7 | SelectPackages, 8 | Suite 9 | } 10 | 11 | @Suite 12 | @IncludeEngines(Array("cucumber")) 13 | @SelectPackages(Array("datatables")) 14 | @ConfigurationParameter( 15 | key = Constants.GLUE_PROPERTY_NAME, 16 | value = "datatables" 17 | ) 18 | class RunDatatablesTest 19 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/statichooks/statichooks.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to use beforeAll/afterAll hooks 2 | 3 | Scenario: Scenario A 4 | Then BeforeAll count is 1 5 | Then AfterAll count is 0 6 | When I run scenario "A" 7 | Then BeforeAll count is 1 8 | Then AfterAll count is 0 9 | 10 | Scenario: Scenario B 11 | Then BeforeAll count is 1 12 | Then AfterAll count is 0 13 | When I run scenario "B" 14 | Then BeforeAll count is 1 15 | Then AfterAll count is 0 16 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/statichooks/statichooks2.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to use beforeAll/afterAll hooks 2 | 3 | Scenario: Scenario C 4 | Then BeforeAll count is 1 5 | Then AfterAll count is 0 6 | When I run scenario "C" 7 | Then BeforeAll count is 1 8 | Then AfterAll count is 0 9 | 10 | Scenario: Scenario D 11 | Then BeforeAll count is 1 12 | Then AfterAll count is 0 13 | When I run scenario "D" 14 | Then BeforeAll count is 1 15 | Then AfterAll count is 0 16 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/parametertypes/RunParameterTypesTest.scala: -------------------------------------------------------------------------------- 1 | package parametertypes 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.platform.suite.api.{ 5 | ConfigurationParameter, 6 | IncludeEngines, 7 | SelectPackages, 8 | Suite 9 | } 10 | 11 | @Suite 12 | @IncludeEngines(Array("cucumber")) 13 | @SelectPackages(Array("parametertypes")) 14 | @ConfigurationParameter( 15 | key = Constants.GLUE_PROPERTY_NAME, 16 | value = "parametertypes" 17 | ) 18 | class RunParameterTypesTest 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Any contribution is welcome: 4 | - completing or fixing documentation 5 | - reporting or fixing issue 6 | - reporting missing feature (compared to other implementations) 7 | - developing a new feature 8 | 9 | Please use this Github project for contributing, either through an issue or a Pull Request. 10 | 11 | ## Documentation 12 | 13 | These pages aim to help Cucumber Scala developers understand the codebase. 14 | 15 | - [Build](docs/build.md) 16 | - [Scala implementation details](docs/scala_implementation.md) 17 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/staticclasshooks/StaticClassHooksDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala.steps.errors.staticclasshooks 2 | 3 | import io.cucumber.scala.ScalaDsl 4 | 5 | //@formatter:off 6 | class StaticClassHooksDefinition extends ScalaDsl { 7 | 8 | // On a single line to avoid difference between Scala versions for the location 9 | 10 | // Static hook not allowed in classes 11 | BeforeAll { () } 12 | 13 | // Static hook not allowed in classes 14 | AfterAll { () } 15 | 16 | } 17 | //@formatter:on 18 | -------------------------------------------------------------------------------- /scripts/remove-empty-sections-changelog.awk: -------------------------------------------------------------------------------- 1 | function start_buffering() { 2 | buf = $0 3 | } 4 | function store_line_in_buffer() { 5 | buf = buf ORS $0 6 | } 7 | function clear_buffer() { 8 | buf = "" 9 | } 10 | /^### (Added|Changed|Deprecated|Removed|Fixed)$/ { 11 | start_buffering() 12 | next 13 | } 14 | /^## / { 15 | clear_buffer() 16 | } 17 | /^ *$/ { 18 | if (buf != "") { 19 | store_line_in_buffer() 20 | } else { 21 | print $0 22 | } 23 | } 24 | !/^ *$/ { 25 | if (buf != "") { 26 | print buf 27 | clear_buffer() 28 | } 29 | print $0 30 | } 31 | -------------------------------------------------------------------------------- /scripts/update-install-doc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -uf -o pipefail 3 | 4 | # Reads installation doc from STDIN and writes out a new one to STDOUT where: 5 | # 6 | # * the version number is updated for both Maven and sbt 7 | # 8 | 9 | new_version=$1 10 | 11 | installdoc=$([0-9]\+.[0-9]\+.[0-9]\+<\/version>/${new_version}<\/version>/g") 15 | # sbt 16 | installdoc=$(echo "${installdoc}" | sed "s/% \"[0-9]\+.[0-9]\+.[0-9]\+\" %/% \"${new_version}\" %/g") 17 | 18 | # Output 19 | echo "${installdoc}" -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaBackendProviderService.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.util.function.Supplier 4 | 5 | import io.cucumber.core.backend.{ 6 | Backend, 7 | BackendProviderService, 8 | Container, 9 | Lookup 10 | } 11 | 12 | class ScalaBackendProviderService extends BackendProviderService { 13 | 14 | override def create( 15 | lookup: Lookup, 16 | container: Container, 17 | classLoaderSupplier: Supplier[ClassLoader] 18 | ): Backend = { 19 | new ScalaBackend(lookup, container, classLoaderSupplier) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Dependency 4 | 5 | ### SBT 6 | 7 | To use Cucumber Scala in your project, add the following line to your `build.sbt`: 8 | 9 | ```scala 10 | libraryDependencies += "io.cucumber" %% "cucumber-scala" % "8.38.0" % Test 11 | ``` 12 | 13 | ### Maven 14 | 15 | To use Cucumber Scala in your project, add the following dependency to your `pom.xml`: 16 | 17 | ```xml 18 | 19 | io.cucumber 20 | cucumber-scala_2.13 21 | 8.38.0 22 | test 23 | 24 | ``` 25 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/steps/traits/StepsInTrait.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala.steps.traits 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | 5 | trait TraitWithSteps extends ScalaDsl with EN { 6 | 7 | Before { 8 | // Nothing 9 | () 10 | } 11 | 12 | BeforeStep { 13 | // Nothing 14 | () 15 | } 16 | 17 | After { 18 | // Nothing 19 | () 20 | } 21 | 22 | AfterStep { 23 | // Nothing 24 | () 25 | } 26 | 27 | Given("""Given step""") { () => 28 | // Nothing 29 | } 30 | 31 | } 32 | 33 | class StepsInTrait extends TraitWithSteps {} 34 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/HookType.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | sealed trait HookType 4 | 5 | sealed trait ScopedHookType extends HookType 6 | 7 | object ScopedHookType { 8 | 9 | case object BEFORE extends ScopedHookType 10 | 11 | case object BEFORE_STEP extends ScopedHookType 12 | 13 | case object AFTER extends ScopedHookType 14 | 15 | case object AFTER_STEP extends ScopedHookType 16 | 17 | } 18 | 19 | sealed trait StaticHookType extends HookType 20 | 21 | object StaticHookType { 22 | 23 | case object BEFORE_ALL extends StaticHookType 24 | 25 | case object AFTER_ALL extends StaticHookType 26 | 27 | } 28 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/steps/classes/MultipleInSameFile.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala.steps.classes 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | 5 | class StepsA extends ScalaDsl with EN { 6 | 7 | Before { 8 | // Nothing 9 | () 10 | } 11 | 12 | BeforeStep { 13 | // Nothing 14 | () 15 | } 16 | 17 | After { 18 | // Nothing 19 | () 20 | } 21 | 22 | AfterStep { 23 | // Nothing 24 | () 25 | } 26 | 27 | Given("""stepA""") { () => 28 | // Nothing 29 | } 30 | 31 | } 32 | 33 | class StepsB extends ScalaDsl with EN { 34 | 35 | When("""stepsB""") { () => 36 | // Nothing 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/steps/objects/StepsInObject.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala.steps.objects 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | 5 | object StepsInObject extends ScalaDsl with EN { 6 | 7 | BeforeAll { 8 | // Nothing 9 | () 10 | } 11 | 12 | Before { 13 | // Nothing 14 | () 15 | } 16 | 17 | BeforeStep { 18 | // Nothing 19 | () 20 | } 21 | 22 | AfterAll { 23 | // Nothing 24 | () 25 | } 26 | 27 | After { 28 | // Nothing 29 | () 30 | } 31 | 32 | AfterStep { 33 | // Nothing 34 | () 35 | } 36 | 37 | Given("""Given step""") { () => 38 | // Nothing 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Cross compilation matrix 2 | addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.11.0") 3 | 4 | // Scalafmt (formatter) 5 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") 6 | 7 | // Version policy check 8 | addSbtPlugin("ch.epfl.scala" % "sbt-version-policy" % "3.2.1") 9 | 10 | // Release 11 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0") 12 | 13 | // Publishing 14 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2") 15 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") 16 | 17 | // Junit 5 18 | addSbtPlugin("com.github.sbt.junit" % "sbt-jupiter-interface" % "0.17.0") 19 | 20 | // Usage of BOMs 21 | addSbtPlugin("com.here.platform" % "sbt-bom" % "1.0.31") 22 | -------------------------------------------------------------------------------- /integration-tests/jackson2/src/test/resources/jackson/Jackson.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to provide a basic DataTable transformer using Jackson 2 | 3 | Scenario: Use the default transformer with a basic case class 4 | Given I have the following datatable 5 | | field1 | field2 | field3 | 6 | | 1.2 | true | abc | 7 | | 2.3 | false | def | 8 | | 3.4 | true | ghj | 9 | 10 | Scenario: Use the default transformer with a basic case class and empty values 11 | Given I have the following datatable, with an empty value 12 | | field1 | field2 | field3 | 13 | | 1.2 | true | abc | 14 | | 2.3 | false | [blank] | 15 | | 3.4 | true | ghj | -------------------------------------------------------------------------------- /integration-tests/jackson3/src/test/resources/jackson3/Jackson3.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to provide a basic DataTable transformer using Jackson 2 | 3 | Scenario: Use the default transformer with a basic case class 4 | Given I have the following datatable 5 | | field1 | field2 | field3 | 6 | | 1.2 | true | abc | 7 | | 2.3 | false | def | 8 | | 3.4 | true | ghj | 9 | 10 | Scenario: Use the default transformer with a basic case class and empty values 11 | Given I have the following datatable, with an empty value 12 | | field1 | field2 | field3 | 13 | | 1.2 | true | abc | 14 | | 2.3 | false | [blank] | 15 | | 3.4 | true | ghj | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/Aliases.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | /** Contains some aliases to help match this codebase with cucumber-java 4 | */ 5 | object Aliases { 6 | 7 | type StaticHookDefinitionBody = () => Unit 8 | 9 | type HookDefinitionBody = Scenario => Unit 10 | 11 | type StepDefinitionBody = () => Unit 12 | 13 | type DocStringDefinitionBody[T] = String => T 14 | 15 | type DefaultParameterTransformerBody = 16 | (String, java.lang.reflect.Type) => AnyRef 17 | 18 | type DefaultDataTableCellTransformerBody = 19 | (String, java.lang.reflect.Type) => AnyRef 20 | 21 | type DefaultDataTableEntryTransformerBody = 22 | (Map[String, String], java.lang.reflect.Type) => AnyRef 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /usr/bin/env bash 2 | 3 | default: 4 | sbt clean +publishLocal 5 | .PHONY: default 6 | 7 | VERSION = $(shell sbt "print cucumberScala/version" | tail -n 1) 8 | 9 | clean: 10 | sbt clean 11 | .PHONY: clean 12 | 13 | update-installdoc: 14 | cat docs/install.md | ./scripts/update-install-doc.sh $(VERSION) > docs/install.md.tmp 15 | mv docs/install.md.tmp docs/install.md 16 | .PHONY: update-installdoc 17 | 18 | update-changelog: 19 | cat CHANGELOG.md | ./scripts/update-changelog.sh $(VERSION) > CHANGELOG.md.tmp 20 | mv CHANGELOG.md.tmp CHANGELOG.md 21 | .PHONY: update-changelog 22 | 23 | prepare-release: update-changelog update-installdoc 24 | git commit -am "Update CHANGELOG and docs for v$(VERSION)" 25 | git push 26 | .PHONY: prepare-release 27 | -------------------------------------------------------------------------------- /examples/examples-junit4/src/test/scala/cucumber/examples/scalacalculator/RpnCalculatorStepDefinitions.scala: -------------------------------------------------------------------------------- 1 | package cucumber.examples.scalacalculator 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl, Scenario} 4 | import org.junit.Assert._ 5 | 6 | class RpnCalculatorStepDefinitions extends ScalaDsl with EN { 7 | 8 | val calc = new RpnCalculator 9 | 10 | When("""I add {double} and {double}""") { (arg1: Double, arg2: Double) => 11 | calc push arg1 12 | calc push arg2 13 | calc push "+" 14 | } 15 | 16 | Then("the result is {double}") { (expected: Double) => 17 | assertEquals(expected, calc.value, 0.001) 18 | } 19 | 20 | Before("not @foo") { (scenario: Scenario) => 21 | println(s"Runs before scenarios *not* tagged with @foo (${scenario.getId})") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/object/ObjectSteps.scala: -------------------------------------------------------------------------------- 1 | package `object` 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | 6 | import scala.annotation.nowarn 7 | 8 | @nowarn 9 | object ObjectSteps extends ScalaDsl with EN { 10 | 11 | private var calculator: Calculator = _ 12 | private var result: Int = -1 13 | 14 | Given("""I have a calculator""") { 15 | calculator = new Calculator() 16 | } 17 | 18 | When("""I do {int} + {int}""") { (a: Int, b: Int) => 19 | result = calculator.add(a, b) 20 | } 21 | 22 | Then("""I got {int}""") { (expectedResult: Int) => 23 | assertEquals(expectedResult, result) 24 | } 25 | 26 | private class Calculator { 27 | def add(a: Int, b: Int) = a + b 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /examples/examples-junit5/src/test/scala/cucumber/examples/scalacalculator/RpnCalculatorStepDefinitions.scala: -------------------------------------------------------------------------------- 1 | package cucumber.examples.scalacalculator 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl, Scenario} 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | 6 | class RpnCalculatorStepDefinitions extends ScalaDsl with EN { 7 | 8 | val calc = new RpnCalculator 9 | 10 | When("""I add {double} and {double}""") { (arg1: Double, arg2: Double) => 11 | calc push arg1 12 | calc push arg2 13 | calc push "+" 14 | } 15 | 16 | Then("the result is {double}") { (expected: Double) => 17 | assertEquals(expected, calc.value, 0.001) 18 | } 19 | 20 | Before("not @foo") { (scenario: Scenario) => 21 | println(s"Runs before scenarios *not* tagged with @foo (${scenario.getId})") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDefaultTransformerDetails.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import Aliases.{ 4 | DefaultDataTableCellTransformerBody, 5 | DefaultDataTableEntryTransformerBody, 6 | DefaultParameterTransformerBody 7 | } 8 | 9 | case class ScalaDefaultParameterTransformerDetails( 10 | body: DefaultParameterTransformerBody, 11 | stackTraceElement: StackTraceElement 12 | ) 13 | 14 | case class ScalaDefaultDataTableCellTransformerDetails( 15 | emptyPatterns: Seq[String], 16 | body: DefaultDataTableCellTransformerBody, 17 | stackTraceElement: StackTraceElement 18 | ) 19 | 20 | case class ScalaDefaultDataTableEntryTransformerDetails( 21 | emptyPatterns: Seq[String], 22 | body: DefaultDataTableEntryTransformerBody, 23 | stackTraceElement: StackTraceElement 24 | ) 25 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/DocStringTypeDsl.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.scala.Aliases.DocStringDefinitionBody 4 | 5 | private[scala] trait DocStringTypeDsl extends BaseScalaDsl { self => 6 | 7 | /** Register doc string type. 8 | * 9 | * @param contentType 10 | * Name of the content type. 11 | * @param body 12 | * a function that creates an instance of T from the doc 13 | * string 14 | * @tparam T 15 | * type to convert to 16 | */ 17 | def DocStringType[T]( 18 | contentType: String 19 | )(body: DocStringDefinitionBody[T])(implicit ev: Stepable[T]): Unit = { 20 | registry.registerDocStringType( 21 | ScalaDocStringTypeDetails[T]( 22 | contentType, 23 | body, 24 | ev.asJavaType, 25 | Utils.frame(self) 26 | ) 27 | ) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /docs/upgrade_v8.md: -------------------------------------------------------------------------------- 1 | # Upgrading from 7.x to 8.x 2 | 3 | Prior to upgrading to v8.0.0 upgrade to latest v7.x and stop using all deprecated features. 4 | Some features will log a deprecation warning. 5 | 6 | See also: 7 | - [Cucumber Scala CHANGELOG](../CHANGELOG.md) 8 | - [Cucumber JVM CHANGELOG](https://github.com/cucumber/cucumber-jvm/blob/main/CHANGELOG.md) 9 | 10 | ## Cannot use `DataTable#asX` inside a `DataTableType` 11 | 12 | You should not use the methods `DataTable#asX()` in `DataTableType`s. 13 | It was working in previous versions, but it will now raise an exception when running your tests. 14 | 15 | Instead you should use: 16 | - Replace `DataTable#asList()` with `DataTable#values()` 17 | - Replace `DataTable#asLists()` with `DataTable#cells()` 18 | - Replace `DataTable#asMaps()` with `DataTable#entries()` 19 | 20 | 21 | More context for this change at https://github.com/cucumber/common/pull/1419 22 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/isolated/IsolatedSteps.scala: -------------------------------------------------------------------------------- 1 | package isolated 2 | 3 | import java.util.{List => JList} 4 | import io.cucumber.scala.{EN, ScalaDsl} 5 | 6 | import scala.jdk.CollectionConverters._ 7 | 8 | class IsolatedSteps extends ScalaDsl with EN { 9 | 10 | var mutableValues: List[Int] = List() 11 | 12 | Given("""I set the list of values to""") { (values: JList[Int]) => 13 | // Obviously this is silly, as we keep the previous value but this is exactly what we want to test 14 | // Isolated scenarios should ensure that the previous value is not kept 15 | mutableValues = mutableValues ++ values.asScala.toList 16 | } 17 | 18 | Given("""I multiply by {int}""") { (mult: Int) => 19 | mutableValues = mutableValues.map(i => i * mult) 20 | } 21 | 22 | Then("""the list of values is""") { (values: JList[Int]) => 23 | assert(mutableValues == values.asScala.toList) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/DataTableDefinitionBody.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.datatable.DataTable 4 | 5 | trait DataTableEntryDefinitionBody[T] { 6 | 7 | def transform(entry: Map[String, String]): T 8 | 9 | } 10 | 11 | trait DataTableOptionalEntryDefinitionBody[T] { 12 | 13 | def transform(entry: Map[String, Option[String]]): T 14 | 15 | } 16 | 17 | trait DataTableRowDefinitionBody[T] { 18 | 19 | def transform(row: Seq[String]): T 20 | 21 | } 22 | 23 | trait DataTableOptionalRowDefinitionBody[T] { 24 | 25 | def transform(row: Seq[Option[String]]): T 26 | 27 | } 28 | 29 | trait DataTableCellDefinitionBody[T] { 30 | 31 | def transform(cell: String): T 32 | 33 | } 34 | 35 | trait DataTableOptionalCellDefinitionBody[T] { 36 | 37 | def transform(cell: Option[String]): T 38 | 39 | } 40 | 41 | trait DataTableDefinitionBody[T] { 42 | 43 | def transform(dataTable: DataTable): T 44 | 45 | } 46 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaStaticHookDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.StaticHookDefinition 4 | 5 | trait ScalaStaticHookDefinition 6 | extends StaticHookDefinition 7 | with AbstractGlueDefinition { 8 | 9 | val hookDetails: ScalaStaticHookDetails 10 | 11 | override val location: StackTraceElement = hookDetails.stackTraceElement 12 | 13 | override def execute(): Unit = { 14 | executeAsCucumber(hookDetails.body.apply()) 15 | } 16 | 17 | override def getOrder: Int = hookDetails.order 18 | 19 | } 20 | 21 | object ScalaStaticHookDefinition { 22 | 23 | def apply( 24 | scalaHookDetails: ScalaStaticHookDetails 25 | ): ScalaStaticHookDefinition = { 26 | new ScalaGlobalStaticHookDefinition(scalaHookDetails) 27 | } 28 | 29 | } 30 | 31 | class ScalaGlobalStaticHookDefinition( 32 | override val hookDetails: ScalaStaticHookDetails 33 | ) extends ScalaStaticHookDefinition {} 34 | -------------------------------------------------------------------------------- /examples/examples-junit4/src/main/scala/cucumber/examples/scalacalculator/RpnCalculator.scala: -------------------------------------------------------------------------------- 1 | package cucumber.examples.scalacalculator 2 | 3 | import scala.collection.mutable.Queue 4 | 5 | sealed trait Arg 6 | 7 | object Arg { 8 | implicit def op(s: String): Op = Op(s) 9 | implicit def value(v: Double): Val = Val(v) 10 | } 11 | 12 | case class Op(value: String) extends Arg 13 | case class Val(value: Double) extends Arg 14 | 15 | class RpnCalculator { 16 | private val stack = Queue.empty[Double] 17 | 18 | private def op(f: (Double, Double) => Double) = 19 | stack += f(stack.dequeue(), stack.dequeue()) 20 | 21 | def push(arg: Arg): Unit = { 22 | arg match { 23 | case Op("+") => op(_ + _) 24 | case Op("-") => op(_ - _) 25 | case Op("*") => op(_ * _) 26 | case Op("/") => op(_ / _) 27 | case Val(value) => stack += value 28 | case _ => () 29 | } 30 | () 31 | } 32 | 33 | def value: Double = stack.head 34 | } 35 | -------------------------------------------------------------------------------- /examples/examples-junit5/src/main/scala/cucumber/examples/scalacalculator/RpnCalculator.scala: -------------------------------------------------------------------------------- 1 | package cucumber.examples.scalacalculator 2 | 3 | import scala.collection.mutable.Queue 4 | 5 | sealed trait Arg 6 | 7 | object Arg { 8 | implicit def op(s: String): Op = Op(s) 9 | implicit def value(v: Double): Val = Val(v) 10 | } 11 | 12 | case class Op(value: String) extends Arg 13 | case class Val(value: Double) extends Arg 14 | 15 | class RpnCalculator { 16 | private val stack = Queue.empty[Double] 17 | 18 | private def op(f: (Double, Double) => Double) = 19 | stack += f(stack.dequeue(), stack.dequeue()) 20 | 21 | def push(arg: Arg): Unit = { 22 | arg match { 23 | case Op("+") => op(_ + _) 24 | case Op("-") => op(_ - _) 25 | case Op("*") => op(_ * _) 26 | case Op("/") => op(_ / _) 27 | case Val(value) => stack += value 28 | case _ => () 29 | } 30 | () 31 | } 32 | 33 | def value: Double = stack.head 34 | } 35 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/misc/OptionalCaptureGroupsSteps.scala: -------------------------------------------------------------------------------- 1 | package misc 2 | 3 | import java.util.Optional 4 | 5 | import io.cucumber.scala.{EN, ScalaDsl} 6 | 7 | class OptionalCaptureGroupsSteps extends ScalaDsl with EN { 8 | 9 | // Scala 2.13 only 10 | // import scala.jdk.OptionConverters._ 11 | 12 | import OptionalCaptureGroupsSteps._ 13 | 14 | Given("""^I have the name:\s?(.+)?$""") { (name: Optional[String]) => 15 | val option = name.toScala 16 | assert(option.isDefined) 17 | assert(option.getOrElse("Nope") == "Jack") 18 | } 19 | 20 | Given("""^I don't have the name:\s?(.+)?$""") { (name: Optional[String]) => 21 | val option = name.toScala 22 | assert(option.isEmpty) 23 | } 24 | 25 | } 26 | 27 | object OptionalCaptureGroupsSteps { 28 | 29 | implicit class RichOptional[A](private val o: java.util.Optional[A]) 30 | extends AnyVal { 31 | 32 | def toScala: Option[A] = if (o.isPresent) Some(o.get) else None 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/statichooks/StaticHooksSteps.scala: -------------------------------------------------------------------------------- 1 | package statichooks 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | 6 | import scala.annotation.nowarn 7 | 8 | @nowarn 9 | object StaticHooksSteps extends ScalaDsl with EN { 10 | 11 | var countBeforeAll: Int = 0 12 | var countAfterAll: Int = 0 13 | 14 | BeforeAll { 15 | countBeforeAll = countBeforeAll + 1 16 | } 17 | 18 | AfterAll { 19 | countAfterAll = countAfterAll + 1 20 | } 21 | 22 | When("""I run scenario {string}""") { (scenarioName: String) => 23 | println(s"Running scenario $scenarioName") 24 | () 25 | } 26 | 27 | Then("""BeforeAll count is {int}""") { (count: Int) => 28 | println(s"BeforeAll = $countBeforeAll") 29 | assertEquals(count, countBeforeAll) 30 | } 31 | 32 | Then("""AfterAll count is {int}""") { (count: Int) => 33 | println(s"AfterAll = $countAfterAll") 34 | assertEquals(count, countAfterAll) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/docstring/Docstring.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to use DocStringType 2 | 3 | Scenario: Using a DocStringType 4 | Given the following json text 5 | """json 6 | { 7 | "key": "value" 8 | } 9 | """ 10 | Then I have a json text 11 | 12 | Scenario: Using another DocStringType 13 | Given the following xml text 14 | """xml 15 | 16 | """ 17 | Then I have a xml text 18 | 19 | Scenario: Using no content type 20 | Given the following raw text 21 | """ 22 | something raw 23 | """ 24 | Then I have a raw text 25 | 26 | Scenario: Generic type - string 27 | Given the following string list 28 | """ 29 | item 1 30 | item 2 31 | """ 32 | Then I have a string list "item 1,item 2" 33 | 34 | Scenario: Generic type - int 35 | Given the following int list 36 | """ 37 | 1 38 | 2 39 | """ 40 | Then I have a int list "1,2" 41 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/incorrectclasshooks/IncorrectClassHooksDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala.steps.errors.incorrectclasshooks 2 | 3 | import io.cucumber.scala.ScalaDsl 4 | 5 | //@formatter:off 6 | class IncorrectClassHooksDefinition extends ScalaDsl { 7 | 8 | // On a single line to avoid difference between Scala versions for the location 9 | 10 | // A body that does not return Unit => interpreted as missing body 11 | BeforeAll { 22 } 12 | 13 | // A body that does not return Unit => interpreted as missing body 14 | Before { 1 } 15 | 16 | // A body that does not return Unit => interpreted as missing body 17 | BeforeStep { "toto" } 18 | 19 | // A body that does not return Unit => interpreted as missing body 20 | AfterAll { 66 } 21 | 22 | // A body that does not return Unit => interpreted as missing body 23 | After { 33 } 24 | 25 | // A body that does not return Unit => interpreted as missing body 26 | AfterStep { "toto" } 27 | 28 | } 29 | //@formatter:on 30 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/steps/errors/incorrectobjecthooks/IncorrectObjectHooksDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala.steps.errors.incorrectobjecthooks 2 | 3 | import io.cucumber.scala.ScalaDsl 4 | 5 | //@formatter:off 6 | object IncorrectObjectHooksDefinition extends ScalaDsl { 7 | 8 | // On a single line to avoid difference between Scala versions for the location 9 | 10 | // A body that does not return Unit => interpreted as missing body 11 | BeforeAll { 22 } 12 | 13 | // A body that does not return Unit => interpreted as missing body 14 | Before { 1 } 15 | 16 | // A body that does not return Unit => interpreted as missing body 17 | BeforeStep { "toto" } 18 | 19 | // A body that does not return Unit => interpreted as missing body 20 | AfterAll { 66 } 21 | 22 | // A body that does not return Unit => interpreted as missing body 23 | After { 33 } 24 | 25 | // A body that does not return Unit => interpreted as missing body 26 | AfterStep { "toto" } 27 | 28 | } 29 | //@formatter:on 30 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDataTableDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.ScenarioScoped 4 | import io.cucumber.datatable.{DataTable, DataTableType, TableTransformer} 5 | 6 | import scala.annotation.nowarn 7 | 8 | trait ScalaDataTableDefinition[T] extends ScalaDataTableTypeDefinition { 9 | 10 | override val details: ScalaDataTableTableTypeDetails[T] 11 | 12 | private val transformer: TableTransformer[T] = (table: DataTable) => { 13 | details.body.transform(replaceEmptyPatternsWithEmptyString(table)) 14 | } 15 | 16 | override val dataTableType = 17 | new DataTableType(details.tag.runtimeClass, transformer) 18 | 19 | } 20 | 21 | @nowarn 22 | class ScalaScenarioScopedDataTableDefinition[T]( 23 | override val details: ScalaDataTableTableTypeDetails[T] 24 | ) extends ScalaDataTableDefinition[T] 25 | with ScenarioScoped {} 26 | 27 | class ScalaGlobalDataTableDefinition[T]( 28 | override val details: ScalaDataTableTableTypeDetails[T] 29 | ) extends ScalaDataTableDefinition[T] {} 30 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaStepDetails.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.lang.reflect.{Type => JType} 4 | 5 | /** Implementation of step definition for scala. 6 | * 7 | * @param frame 8 | * Representation of a stack frame containing information about the context 9 | * in which a step was defined. Allows retrospective queries about the 10 | * definition of a step. 11 | * @param name 12 | * The name of the step definition class, e.g. 13 | * cucumber.runtime.scala.test.CukesStepDefinitions 14 | * @param pattern 15 | * The regex matcher that defines the cucumber step, e.g. /I eat (.*) cukes$/ 16 | * @param types 17 | * Parameters types of body step definition 18 | * @param body 19 | * Function body of a step definition. This is what actually runs the code 20 | * within the step def. 21 | */ 22 | private[scala] case class ScalaStepDetails( 23 | frame: StackTraceElement, 24 | name: String, 25 | pattern: String, 26 | types: Seq[JType], 27 | body: List[Any] => Any 28 | ) 29 | -------------------------------------------------------------------------------- /.github/workflows/version-policy-check.yml: -------------------------------------------------------------------------------- 1 | name: Version policy check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | versionpolicycheck: 10 | name: Version policy check 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ ubuntu-latest ] 15 | java: 16 | - 21 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | 20 | - uses: actions/checkout@v6 21 | 22 | - name: Cache sbt 23 | uses: actions/cache@v5 24 | with: 25 | path: | 26 | ~/.sbt 27 | ~/.ivy2/cache 28 | ~/.cache/coursier 29 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 30 | 31 | - name: Set up JDK 32 | uses: actions/setup-java@v5 33 | with: 34 | java-version: ${{ matrix.java }} 35 | distribution: 'temurin' 36 | 37 | - uses: sbt/setup-sbt@v1 38 | 39 | - name: Version check 40 | run: sbt "project cucumberScala" versionPolicyCheck 41 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDataTableCellDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.ScenarioScoped 4 | import io.cucumber.datatable.{DataTableType, TableCellTransformer} 5 | 6 | import scala.annotation.nowarn 7 | 8 | trait ScalaDataTableCellDefinition[T] extends ScalaDataTableTypeDefinition { 9 | 10 | override val details: ScalaDataTableCellTypeDetails[T] 11 | 12 | private val transformer: TableCellTransformer[T] = (cell: String) => { 13 | details.body.transform(replaceEmptyPatternsWithEmptyString(cell)) 14 | } 15 | 16 | override val dataTableType = 17 | new DataTableType(details.tag.runtimeClass, transformer) 18 | 19 | } 20 | 21 | @nowarn 22 | class ScalaScenarioScopedDataTableCellDefinition[T]( 23 | override val details: ScalaDataTableCellTypeDetails[T] 24 | ) extends ScalaDataTableCellDefinition[T] 25 | with ScenarioScoped {} 26 | 27 | class ScalaGlobalDataTableCellDefinition[T]( 28 | override val details: ScalaDataTableCellTypeDetails[T] 29 | ) extends ScalaDataTableCellDefinition[T] {} 30 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/statichooks/RunStaticHooksTest.scala: -------------------------------------------------------------------------------- 1 | package statichooks 2 | 3 | import io.cucumber.junit.platform.engine.Constants 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.{AfterAll, BeforeAll} 6 | import org.junit.platform.suite.api.{ 7 | ConfigurationParameter, 8 | IncludeEngines, 9 | SelectPackages, 10 | Suite 11 | } 12 | 13 | @Suite 14 | @IncludeEngines(Array("cucumber")) 15 | @SelectPackages(Array("statichooks")) 16 | @ConfigurationParameter( 17 | key = Constants.GLUE_PROPERTY_NAME, 18 | value = "statichooks" 19 | ) 20 | class RunStaticHooksTest 21 | 22 | object RunStaticHooksTest { 23 | 24 | @BeforeAll 25 | def beforeAllJunit(): Unit = { 26 | assertEquals( 27 | 0L, 28 | StaticHooksSteps.countBeforeAll.toLong, 29 | "Before Cucumber's BeforeAll" 30 | ) 31 | } 32 | 33 | @AfterAll 34 | def afterAllJunit(): Unit = { 35 | assertEquals( 36 | 1L, 37 | StaticHooksSteps.countAfterAll.toLong, 38 | "After Cucumber's AfterAll" 39 | ) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) The Cucumber Organisation 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/TestFeatureParser.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.io.{ByteArrayInputStream, InputStream} 4 | import java.net.URI 5 | import java.nio.charset.StandardCharsets 6 | import java.util.UUID 7 | import java.util.function.Supplier 8 | 9 | import io.cucumber.core.feature.{FeatureIdentifier, FeatureParser} 10 | import io.cucumber.core.gherkin.Feature 11 | import io.cucumber.core.resource.Resource 12 | 13 | object TestFeatureParser { 14 | 15 | def parse(source: String): Feature = { 16 | parse("file:test.feature", source) 17 | } 18 | 19 | def parse(uri: String, source: String): Feature = { 20 | parse(FeatureIdentifier.parse(uri), source) 21 | } 22 | 23 | def parse(uri: URI, source: String): Feature = { 24 | val supplier: Supplier[UUID] = () => UUID.randomUUID() 25 | 26 | new FeatureParser(supplier) 27 | .parseResource(new Resource { 28 | override def getUri: URI = uri 29 | 30 | override def getInputStream: InputStream = 31 | new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)) 32 | }) 33 | .orElse(null) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDataTableOptionalCellDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.ScenarioScoped 4 | import io.cucumber.datatable.{DataTableType, TableCellTransformer} 5 | 6 | import scala.annotation.nowarn 7 | 8 | trait ScalaDataTableOptionalCellDefinition[T] 9 | extends ScalaDataTableTypeDefinition { 10 | 11 | override val details: ScalaDataTableOptionalCellTypeDetails[T] 12 | 13 | private val transformer: TableCellTransformer[T] = (cell: String) => { 14 | details.body.transform(Option(replaceEmptyPatternsWithEmptyString(cell))) 15 | } 16 | 17 | override val dataTableType = 18 | new DataTableType(details.tag.runtimeClass, transformer) 19 | 20 | } 21 | 22 | @nowarn 23 | class ScalaScenarioScopedDataTableOptionalCellDefinition[T]( 24 | override val details: ScalaDataTableOptionalCellTypeDetails[T] 25 | ) extends ScalaDataTableOptionalCellDefinition[T] 26 | with ScenarioScoped {} 27 | 28 | class ScalaGlobalDataTableOptionalCellDefinition[T]( 29 | override val details: ScalaDataTableOptionalCellTypeDetails[T] 30 | ) extends ScalaDataTableOptionalCellDefinition[T] {} 31 | -------------------------------------------------------------------------------- /integration-tests/jackson2/src/test/scala/jackson/JacksonSteps.scala: -------------------------------------------------------------------------------- 1 | package jackson 2 | 3 | import io.cucumber.scala.{EN, JacksonDefaultDataTableEntryTransformer, ScalaDsl} 4 | 5 | import scala.jdk.CollectionConverters._ 6 | 7 | case class MyCaseClass(field1: Double, field2: Boolean, field3: String) 8 | 9 | class JacksonSteps 10 | extends ScalaDsl 11 | with EN 12 | with JacksonDefaultDataTableEntryTransformer { 13 | 14 | override def emptyStringReplacement: String = "[blank]" 15 | 16 | Given("I have the following datatable") { 17 | (data: java.util.List[MyCaseClass]) => 18 | val expected = Seq( 19 | MyCaseClass(1.2, true, "abc"), 20 | MyCaseClass(2.3, false, "def"), 21 | MyCaseClass(3.4, true, "ghj") 22 | ) 23 | assert(data.asScala == expected) 24 | } 25 | 26 | Given("I have the following datatable, with an empty value") { 27 | (data: java.util.List[MyCaseClass]) => 28 | val expected = Seq( 29 | MyCaseClass(1.2, true, "abc"), 30 | MyCaseClass(2.3, false, ""), 31 | MyCaseClass(3.4, true, "ghj") 32 | ) 33 | assert(data.asScala == expected) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ========= 3 | 4 | Releases are automated via a [GitHub Actions workflow](./.github/workflows/release-sbt.yml). Only people with permission to push to `release/*` branches can make releases. 5 | 6 | See [Cucumber release process](https://github.com/cucumber/.github/blob/main/RELEASING.md) for the whole process. 7 | 8 | ## Preparation 9 | 10 | 1. Decide what the next version should be according to semver 11 | ```bash 12 | export next_release= # <- insert version number here 13 | ``` 14 | 1. Update the `version.sbt` file with version to release: 15 | ```bash 16 | echo "ThisBuild / version := \"$next_release\"" > version.sbt 17 | ``` 18 | 1. Update the CHANGELOG and documentation, commit and push: 19 | ```bash 20 | make prepare-release 21 | ``` 22 | 23 | ## Release 24 | 25 | 1. Push to a new `release/*` branch to trigger the `release-*` workflows 26 | ```bash 27 | git push origin main:release/v$next_release 28 | ``` 29 | 1. Wait until the `release-*` workflows in GitHub Actions have passed 30 | 1. In `version.sbt`, bump the **patch** version and append `-SNAPSHOT` (e.g. `1.2.4-SNAPSHOT`) and commit/push 31 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/Utils.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | private[scala] object Utils { 4 | 5 | /** Return the stack frame to allow us to identify where in a step definition 6 | * file we are currently based 7 | */ 8 | def frame(self: Any): StackTraceElement = { 9 | val frames = Thread.currentThread().getStackTrace 10 | val currentClass = self.getClass.getName 11 | // Note: the -1 check is here for Scala < 2.13 and objects 12 | findLast(frames)(f => 13 | f.getClassName == currentClass && f.getLineNumber != -1 14 | ) match { 15 | case Some(stackFrame) => stackFrame 16 | case None => 17 | throw new IllegalStateException( 18 | s"Not able to find stack frame for $currentClass" 19 | ) 20 | } 21 | } 22 | 23 | // Copied from Scala 2.13 library, not available in 2.12 nor in scala-collections-compat 24 | private def findLast[A](seq: Array[A])(p: A => Boolean): Option[A] = { 25 | val it = seq.reverseIterator 26 | while (it.hasNext) { 27 | val elem = it.next() 28 | if (p(elem)) return Some(elem) 29 | } 30 | None 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/cukes/TypeRegistryConfiguration.scala: -------------------------------------------------------------------------------- 1 | package cukes 2 | 3 | import cukes.model.{Cukes, Person, Snake} 4 | import io.cucumber.scala.ScalaDsl 5 | 6 | class TypeRegistryConfiguration extends ScalaDsl { 7 | 8 | /** Transforms an ASCII snake into an object, for example: 9 | * 10 | * {{{ 11 | * ====> becomes Snake(length = 5, direction = 'east) 12 | * ==> becomes Snake(length = 3, direction = 'east) 13 | * }}} 14 | */ 15 | ParameterType("snake", "[=><]+") { s => 16 | val size = s.length 17 | val direction = s.toList match { 18 | case '<' :: _ => Symbol("west") 19 | case l if l.last == '>' => Symbol("east") 20 | case _ => Symbol("unknown") 21 | } 22 | Snake(size, direction) 23 | } 24 | 25 | ParameterType("person", ".+") { s => 26 | Person(s) 27 | } 28 | 29 | ParameterType("boolean", "true|false") { s => 30 | s.trim.equals("true") 31 | } 32 | 33 | ParameterType("char", ".") { s => 34 | s.charAt(0) 35 | } 36 | 37 | DataTableType { (map: Map[String, String]) => 38 | Cukes(map("Number").toInt, map("Color")) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/IncorrectStepDefinitionException.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.exception.CucumberException 4 | 5 | object IncorrectStepDefinitionException { 6 | 7 | // Allows to use """ in """xxx"""" strings 8 | private val tripleDoubleQuotes = "\"\"\"" 9 | 10 | val errorMessage: String = 11 | s"""The arguments received doesn't match the step definition. 12 | |This can happen if you are using a regular expression in your step definition with optional capture groups but mandatory parameters. 13 | | 14 | |For instance: 15 | | 16 | | Given($tripleDoubleQuotes^I am logged in(?: as (.+))?$$$tripleDoubleQuotes) { (user: String) => 17 | | // Some code 18 | | } 19 | | 20 | |For now, the easiest solution is to declare two steps: one with the capture groups, one without. 21 | |If you feel this is not working for you, please manifest yourself on https://github.com/cucumber/cucumber-jvm-scala/issues/3""".stripMargin 22 | 23 | } 24 | 25 | class IncorrectStepDefinitionException 26 | extends CucumberException(IncorrectStepDefinitionException.errorMessage) {} 27 | -------------------------------------------------------------------------------- /docs/step_definitions.md: -------------------------------------------------------------------------------- 1 | # Step definitions 2 | 3 | Step definitions (`Given`, `When`, `Then`) are the glue between features written in Gherkin and the actual tests implementation. 4 | 5 | Cucumber supports two types of expressions: 6 | 7 | - Cucumber expressions 8 | - Regular expressions 9 | 10 | See also the [reference documentation](https://docs.cucumber.io/docs/cucumber/step-definitions/#expressions). 11 | 12 | ## Cucumber expressions 13 | 14 | [Cucumber expressions](https://docs.cucumber.io/docs/cucumber/cucumber-expressions/) 15 | 16 | The following Gherkin step: 17 | ```gherkin 18 | Given I have 42 cucumbers in my belly 19 | ``` 20 | 21 | Can be implemented with following Cucumber Expression in Scala: 22 | ```scala 23 | Given("""I have {int} cucumbers in my belly"""){ (cucumberCount: Int) => 24 | // Do something 25 | } 26 | ``` 27 | 28 | ## Regular expressions 29 | 30 | The following Gherkin step: 31 | ```gherkin 32 | Given I have 42 cucumbers in my belly 33 | ``` 34 | 35 | Can be implemented with following Regular Expression in Scala: 36 | ```scala 37 | Given("""^I have (\d+) cucumbers in my belly$"""){ (cucumberCount: Int) => 38 | // Do something 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Cucumber Scala CI 2 | 3 | on: 4 | workflow_call: 5 | pull_request: 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | - renovate/** 12 | 13 | jobs: 14 | build: 15 | name: Build and test 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ ubuntu-latest ] 20 | java: 21 | - 17 22 | - 21 23 | - 25 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | 27 | - uses: actions/checkout@v6 28 | 29 | - name: Cache sbt 30 | uses: actions/cache@v5 31 | with: 32 | path: | 33 | ~/.sbt 34 | ~/.ivy2/cache 35 | ~/.cache/coursier 36 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 37 | 38 | - name: Set up JDK 39 | uses: actions/setup-java@v5 40 | with: 41 | java-version: ${{ matrix.java }} 42 | distribution: 'temurin' 43 | 44 | - uses: sbt/setup-sbt@v1 45 | 46 | - name: Formatting check 47 | run: sbt scalafmtCheckAll 48 | 49 | - name: Run tests 50 | run: sbt +compile +test 51 | 52 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDataTableRowDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.util.{List => JavaList} 4 | import io.cucumber.core.backend.ScenarioScoped 5 | import io.cucumber.datatable.{DataTableType, TableRowTransformer} 6 | 7 | import scala.annotation.nowarn 8 | import scala.jdk.CollectionConverters._ 9 | 10 | trait ScalaDataTableRowDefinition[T] extends ScalaDataTableTypeDefinition { 11 | 12 | override val details: ScalaDataTableRowTypeDetails[T] 13 | 14 | private val transformer: TableRowTransformer[T] = (row: JavaList[String]) => { 15 | details.body.transform( 16 | replaceEmptyPatternsWithEmptyString(row.asScala.toSeq) 17 | ) 18 | } 19 | 20 | override val dataTableType = 21 | new DataTableType(details.tag.runtimeClass, transformer) 22 | 23 | } 24 | 25 | @nowarn 26 | class ScalaScenarioScopedDataTableRowDefinition[T]( 27 | override val details: ScalaDataTableRowTypeDetails[T] 28 | ) extends ScalaDataTableRowDefinition[T] 29 | with ScenarioScoped {} 30 | 31 | class ScalaGlobalDataTableRowDefinition[T]( 32 | override val details: ScalaDataTableRowTypeDetails[T] 33 | ) extends ScalaDataTableRowDefinition[T] {} 34 | -------------------------------------------------------------------------------- /integration-tests/jackson3/src/test/scala/jackson3/Jackson3Steps.scala: -------------------------------------------------------------------------------- 1 | package jackson3 2 | 3 | import io.cucumber.scala.{ 4 | EN, 5 | Jackson3DefaultDataTableEntryTransformer, 6 | ScalaDsl 7 | } 8 | 9 | import scala.jdk.CollectionConverters._ 10 | 11 | case class MyCaseClass(field1: Double, field2: Boolean, field3: String) 12 | 13 | class Jackson3Steps 14 | extends ScalaDsl 15 | with EN 16 | with Jackson3DefaultDataTableEntryTransformer { 17 | 18 | override def emptyStringReplacement: String = "[blank]" 19 | 20 | Given("I have the following datatable") { 21 | (data: java.util.List[MyCaseClass]) => 22 | val expected = Seq( 23 | MyCaseClass(1.2, true, "abc"), 24 | MyCaseClass(2.3, false, "def"), 25 | MyCaseClass(3.4, true, "ghj") 26 | ) 27 | assert(data.asScala == expected) 28 | } 29 | 30 | Given("I have the following datatable, with an empty value") { 31 | (data: java.util.List[MyCaseClass]) => 32 | val expected = Seq( 33 | MyCaseClass(1.2, true, "abc"), 34 | MyCaseClass(2.3, false, ""), 35 | MyCaseClass(3.4, true, "ghj") 36 | ) 37 | assert(data.asScala == expected) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDataTableEntryDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.util.{Map => JavaMap} 4 | import io.cucumber.core.backend.ScenarioScoped 5 | import io.cucumber.datatable.{DataTableType, TableEntryTransformer} 6 | 7 | import scala.annotation.nowarn 8 | import scala.jdk.CollectionConverters._ 9 | 10 | trait ScalaDataTableEntryDefinition[T] extends ScalaDataTableTypeDefinition { 11 | 12 | override val details: ScalaDataTableEntryTypeDetails[T] 13 | 14 | private val transformer: TableEntryTransformer[T] = 15 | (entry: JavaMap[String, String]) => { 16 | replaceEmptyPatternsWithEmptyString(entry.asScala.toMap) 17 | .map(details.body.transform) 18 | .get 19 | } 20 | 21 | override val dataTableType = 22 | new DataTableType(details.tag.runtimeClass, transformer) 23 | 24 | } 25 | 26 | @nowarn 27 | class ScalaScenarioScopedDataTableEntryDefinition[T]( 28 | override val details: ScalaDataTableEntryTypeDetails[T] 29 | ) extends ScalaDataTableEntryDefinition[T] 30 | with ScenarioScoped {} 31 | 32 | class ScalaGlobalDataTableEntryDefinition[T]( 33 | override val details: ScalaDataTableEntryTypeDetails[T] 34 | ) extends ScalaDataTableEntryDefinition[T] {} 35 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/java 3 | { 4 | "name": "Java", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/java:3-17", 7 | 8 | "features": { 9 | "ghcr.io/devcontainers/features/java:1": { 10 | "version": "none", 11 | "installMaven": "false", 12 | "installGradle": "false" 13 | }, 14 | "ghcr.io/devcontainers-contrib/features/sbt-sdkman:2": {} 15 | }, 16 | "customizations": { 17 | "vscode": { 18 | "extensions": [ 19 | "scala-lang.scala", 20 | "lightbend.vscode-sbt-scala", 21 | "scalameta.metals" 22 | ] 23 | } 24 | } 25 | 26 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 27 | // "forwardPorts": [], 28 | 29 | // Use 'postCreateCommand' to run commands after the container is created. 30 | // "postCreateCommand": "java -version", 31 | 32 | // Configure tool-specific properties. 33 | // "customizations": {}, 34 | 35 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 36 | // "remoteUser": "root" 37 | } 38 | -------------------------------------------------------------------------------- /docs/upgrade_v7.md: -------------------------------------------------------------------------------- 1 | # Upgrading from 6.x to 7.x 2 | 3 | Upgrading from v6 should be straightforward. 4 | Prior to upgrading to v7.0.0 upgrade to latest v6.x and stop using all deprecated features. 5 | Some features will log a deprecation warning. 6 | 7 | See also: 8 | - [Cucumber Scala CHANGELOG](../CHANGELOG.md) 9 | 10 | ## Scala 3 support 11 | 12 | This release brings Scala 3 support. 13 | 14 | ### Syntactic changes 15 | 16 | If you use Scala 3, you might need to change slightly some of your glue code: 17 | - parenthesis are now necessary even around a single-argument step or hook definition 18 | ```scala 19 | // Won't compile anymore 20 | Given("Something {}") { str: String => 21 | // ... 22 | } 23 | 24 | // Instead use: 25 | Given("Something {}") { (str: String) => 26 | // ... 27 | } 28 | ``` 29 | - hooks must explicitly return `Unit` (most of the time you already had compile errors with such statements in Scala 2.x as well) 30 | ```scala 31 | Before { 32 | // ... // Some code not retuning Unit 33 | () // Explicit Unit 34 | } 35 | ``` 36 | 37 | ### Other changes 38 | 39 | The line numbers provided in reports might slightly change 40 | from start of a step definition to end of a step definition in some cases. 41 | In case of errors, these line numbers should be more accurate than before. 42 | -------------------------------------------------------------------------------- /docs/upgrade_v6.md: -------------------------------------------------------------------------------- 1 | # Upgrading from 5.x to 6.x 2 | 3 | Upgrading from v5 should be straightforward. 4 | Prior to upgrading to v6.0.0 upgrade to v5.7.0 and stop using all deprecated features. 5 | Some features will log a deprecation warning. 6 | 7 | See also: 8 | - [Cucumber Scala CHANGELOG](../CHANGELOG.md) 9 | - [Cucumber JVM CHANGELOG](https://github.com/cucumber/cucumber-jvm/blob/main/CHANGELOG.md) 10 | - [Cucumber JVM v6 Release Notes](https://github.com/cucumber/cucumber-jvm/blob/main/release-notes/v6.0.0.md) 11 | 12 | ## Map DataTables to Scala types 13 | 14 | You can now map `DataTable`s to Scala collection types using additional `asScalaXxx` methods on the `DataTable` class. 15 | 16 | **The benefit of using Scala types** if that you will be handling `Option`s instead of potentially `null` values in the Java collections. 17 | 18 | For instance with `asScalaMaps`: 19 | 20 | ```scala 21 | import io.cucumber.scala.{ScalaDsl, EN} 22 | import io.cucumber.scala.Implicits._ 23 | 24 | class StepDefs extends ScalaDsl with EN { 25 | 26 | Given("the following table as List of Map") { (table: DataTable) => 27 | val scalaTable: Seq[Map[String, Option[Int]]] = table.asScalaMaps[String, Int] 28 | // Do something 29 | } 30 | 31 | } 32 | ``` 33 | 34 | See the [DataTable documentation](./datatables.md) for more details. 35 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDataTableOptionalRowDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.util.{List => JavaList} 4 | import io.cucumber.core.backend.ScenarioScoped 5 | import io.cucumber.datatable.{DataTableType, TableRowTransformer} 6 | 7 | import scala.annotation.nowarn 8 | import scala.jdk.CollectionConverters._ 9 | 10 | trait ScalaDataTableOptionalRowDefinition[T] 11 | extends ScalaDataTableTypeDefinition { 12 | 13 | override val details: ScalaDataTableOptionalRowTypeDetails[T] 14 | 15 | private val transformer: TableRowTransformer[T] = (row: JavaList[String]) => { 16 | details.body.transform( 17 | row.asScala 18 | .map(replaceEmptyPatternsWithEmptyString) 19 | .map(Option.apply) 20 | .toSeq 21 | ) 22 | } 23 | 24 | override val dataTableType = 25 | new DataTableType(details.tag.runtimeClass, transformer) 26 | 27 | } 28 | 29 | @nowarn 30 | class ScalaScenarioScopedDataTableOptionalRowDefinition[T]( 31 | override val details: ScalaDataTableOptionalRowTypeDetails[T] 32 | ) extends ScalaDataTableOptionalRowDefinition[T] 33 | with ScenarioScoped {} 34 | 35 | class ScalaGlobalDataTableOptionalRowDefinition[T]( 36 | override val details: ScalaDataTableOptionalRowTypeDetails[T] 37 | ) extends ScalaDataTableOptionalRowDefinition[T] {} 38 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDataTableOptionalEntryDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.util.{Map => JavaMap} 4 | import io.cucumber.core.backend.ScenarioScoped 5 | import io.cucumber.datatable.{DataTableType, TableEntryTransformer} 6 | 7 | import scala.annotation.nowarn 8 | import scala.jdk.CollectionConverters._ 9 | 10 | trait ScalaDataTableOptionalEntryDefinition[T] 11 | extends ScalaDataTableTypeDefinition { 12 | 13 | override val details: ScalaDataTableOptionalEntryTypeDetails[T] 14 | 15 | private val transformer: TableEntryTransformer[T] = 16 | (entry: JavaMap[String, String]) => { 17 | replaceEmptyPatternsWithEmptyString(entry.asScala.toMap) 18 | .map(_.map { case (k, v) => (k, Option(v)) }) 19 | .map(details.body.transform) 20 | .get 21 | } 22 | 23 | override val dataTableType = 24 | new DataTableType(details.tag.runtimeClass, transformer) 25 | 26 | } 27 | 28 | @nowarn 29 | class ScalaScenarioScopedDataTableOptionalEntryDefinition[T]( 30 | override val details: ScalaDataTableOptionalEntryTypeDetails[T] 31 | ) extends ScalaDataTableOptionalEntryDefinition[T] 32 | with ScenarioScoped {} 33 | 34 | class ScalaGlobalDataTableOptionalEntryDefinition[T]( 35 | override val details: ScalaDataTableOptionalEntryTypeDetails[T] 36 | ) extends ScalaDataTableOptionalEntryDefinition[T] {} 37 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/AbstractGlueDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.lang.reflect.InvocationTargetException 4 | import java.util.Optional 5 | 6 | import io.cucumber.core.backend.{ 7 | CucumberInvocationTargetException, 8 | Located, 9 | SourceReference 10 | } 11 | 12 | import scala.util.{Failure, Try} 13 | 14 | trait AbstractGlueDefinition extends Located { 15 | 16 | val location: StackTraceElement 17 | 18 | private lazy val sourceReference: SourceReference = 19 | SourceReference.fromStackTraceElement(location) 20 | 21 | override def getLocation(): String = { 22 | location.toString 23 | } 24 | 25 | override def isDefinedAt(stackTraceElement: StackTraceElement): Boolean = { 26 | location.getFileName != null && location.getFileName == stackTraceElement.getFileName 27 | } 28 | 29 | override def getSourceReference(): Optional[SourceReference] = { 30 | Optional.of(sourceReference) 31 | } 32 | 33 | /** Executes the block of code and handle failures in the way asked by 34 | * Cucumber specification: that is throwing a 35 | * CucumberInvocationTargetException. 36 | */ 37 | protected def executeAsCucumber(block: => Unit): Unit = { 38 | Try(block).recoverWith { case ex => 39 | Failure( 40 | new CucumberInvocationTargetException( 41 | this, 42 | new InvocationTargetException(ex) 43 | ) 44 | ) 45 | }.get 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/datatables/Datatable.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to parse DataTables properly 2 | 3 | Scenario: As datatable 4 | Given the following table as DataTable 5 | | key1 | key2 | key3 | 6 | | val11 | val12 | val13 | 7 | | val21 | val22 | val23 | 8 | | val31 | val32 | val33 | 9 | 10 | Scenario: As List of Map 11 | Given the following table as List of Map 12 | | key1 | key2 | key3 | 13 | | val11 | val12 | val13 | 14 | | val21 | val22 | val23 | 15 | | val31 | val32 | val33 | 16 | 17 | Scenario: As List of List 18 | Given the following table as List of List 19 | | val11 | val12 | val13 | 20 | | val21 | val22 | val23 | 21 | | val31 | val32 | val33 | 22 | 23 | Scenario: As Map of Map 24 | Given the following table as Map of Map 25 | | | key1 | key2 | key3 | 26 | | row1 | val11 | val12 | val13 | 27 | | row2 | val21 | val22 | val23 | 28 | | row3 | val31 | val32 | val33 | 29 | 30 | Scenario: As Map of List 31 | Given the following table as Map of List 32 | | row1 | val11 | val12 | val13 | 33 | | row2 | val21 | val22 | val23 | 34 | | row3 | val31 | val32 | val33 | 35 | 36 | Scenario: As Map 37 | Given the following table as Map 38 | | row1 | val11 | 39 | | row2 | val21 | 40 | | row3 | val31 | 41 | 42 | Scenario: As List 43 | Given the following table as List 44 | | val11 | 45 | | val21 | 46 | | val31 | 47 | -------------------------------------------------------------------------------- /.github/workflows/release-sbt.yml: -------------------------------------------------------------------------------- 1 | name: Release scala package 2 | 3 | on: 4 | push: 5 | branches: 6 | - release/* 7 | 8 | jobs: 9 | pre-release-check: 10 | uses: cucumber/.github/.github/workflows/prerelease-checks.yml@main 11 | 12 | build: 13 | uses: ./.github/workflows/build.yml 14 | 15 | publish-sbt: 16 | name: Publish scala package 17 | needs: [pre-release-check, build] 18 | runs-on: ubuntu-latest 19 | environment: Release 20 | 21 | steps: 22 | - uses: actions/checkout@v6 23 | 24 | - run: | 25 | git config user.name github-actions 26 | git config user.email github-actions@github.com 27 | 28 | - uses: actions/setup-java@v5 29 | with: 30 | distribution: 'temurin' 31 | java-version: "17" 32 | 33 | - uses: sbt/setup-sbt@v1 34 | 35 | - uses: cucumber/action-publish-sbt@v1.0.2 36 | with: 37 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 38 | gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} 39 | nexus-username: ${{ secrets.SONATYPE_USERNAME }} 40 | nexus-password: ${{ secrets.SONATYPE_PASSWORD }} 41 | 42 | create-github-release: 43 | name: Create GitHub Release and Git tag 44 | runs-on: ubuntu-latest 45 | needs: [publish-sbt] 46 | environment: Release 47 | permissions: 48 | contents: write 49 | steps: 50 | - uses: actions/checkout@v6 51 | - uses: cucumber/action-create-github-release@v1.1.1 52 | with: 53 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /project/I18nGenerator.scala: -------------------------------------------------------------------------------- 1 | import io.cucumber.gherkin.GherkinDialectProvider 2 | import scala.jdk.CollectionConverters._ 3 | 4 | object I18nGenerator { 5 | 6 | private val dialectProvider = new GherkinDialectProvider() 7 | 8 | // The generated files for these languages don't compile 9 | private val unsupported = Seq( 10 | "em", // Emoji 11 | "en-tx" // Texan 12 | ) 13 | 14 | private val allLanguages = dialectProvider 15 | .getLanguages() 16 | .asScala 17 | .filterNot(l => unsupported.contains(l)) 18 | 19 | private def keywordVal(kw: String): String = { 20 | val keyworkValName = java.text.Normalizer 21 | .normalize(kw.replaceAll("[\\s',!]", ""), java.text.Normalizer.Form.NFC) 22 | s""" val $keyworkValName = new Step("$keyworkValName")""" 23 | } 24 | 25 | private def traitCode(language: String): String = { 26 | val traitName = language.replaceAll("[\\s-]", "_").toUpperCase() 27 | val keywords = dialectProvider 28 | .getDialect(language) 29 | .get() 30 | .getStepKeywords() 31 | .asScala 32 | .filter(kw => !kw.contains('*') && !kw.matches("^\\d.*")) 33 | .sorted 34 | .distinct 35 | 36 | s""" 37 | |trait $traitName { 38 | | this: ScalaDsl => 39 | |${keywords.map(kw => keywordVal(kw)).mkString("\n\n")} 40 | |} 41 | |""".stripMargin 42 | } 43 | 44 | val i18n: String = s""" 45 | |package io.cucumber.scala 46 | | 47 | |${allLanguages.map(l => traitCode(l)).mkString("\n\n")} 48 | |""".stripMargin 49 | 50 | } 51 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDocStringTypeDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.{DocStringTypeDefinition, ScenarioScoped} 4 | import io.cucumber.docstring.DocStringType 5 | import io.cucumber.docstring.DocStringType.Transformer 6 | 7 | import scala.annotation.nowarn 8 | 9 | abstract class ScalaDocStringTypeDefinition[T] 10 | extends DocStringTypeDefinition 11 | with AbstractGlueDefinition { 12 | 13 | val details: ScalaDocStringTypeDetails[T] 14 | 15 | override val location: StackTraceElement = details.stackTraceElement 16 | 17 | private val transformer: Transformer[T] = (s: String) => { 18 | details.body.apply(s) 19 | } 20 | 21 | override val docStringType: DocStringType = 22 | new DocStringType(details.`type`, details.contentType, transformer) 23 | 24 | } 25 | 26 | object ScalaDocStringTypeDefinition { 27 | 28 | def apply[T]( 29 | details: ScalaDocStringTypeDetails[T], 30 | scenarioScoped: Boolean 31 | ): ScalaDocStringTypeDefinition[T] = { 32 | if (scenarioScoped) { 33 | new ScalaScenarioScopedDocStringTypeDefinition(details) 34 | } else { 35 | new ScalaGlobalDocStringTypeDefinition(details) 36 | } 37 | } 38 | 39 | } 40 | 41 | @nowarn 42 | class ScalaScenarioScopedDocStringTypeDefinition[T]( 43 | override val details: ScalaDocStringTypeDetails[T] 44 | ) extends ScalaDocStringTypeDefinition[T] 45 | with ScenarioScoped {} 46 | 47 | class ScalaGlobalDocStringTypeDefinition[T]( 48 | override val details: ScalaDocStringTypeDetails[T] 49 | ) extends ScalaDocStringTypeDefinition[T] {} 50 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/JacksonDefaultDataTableEntryTransformer.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 5 | 6 | /**

This trait register a `DefaultDataTableEntryTransformer` using Jackson 7 | * `ObjectMapper`.

8 | * 9 | *

The `[empty]` string is used as default empty string replacement. You can 10 | * override it if you need to.

11 | * 12 | *

Note: Jackson is not included with Cucumber Scala, you have to add the 13 | * dependency: `com.fasterxml.jackson.module:jackson-module-scala` to your 14 | * project if you want to use this trait.

15 | * 16 | *

For Jackson 3.x, use `Jackson3DefaultDataTableEntryTransformer` 17 | * instead.

18 | */ 19 | trait JacksonDefaultDataTableEntryTransformer extends ScalaDsl { 20 | 21 | /** Define the string to be used as replacement for empty. Default is 22 | * `[empty]`. 23 | */ 24 | def emptyStringReplacement: String = "[empty]" 25 | 26 | /** Create the Jackson ObjectMapper to be used. Default is a simple 27 | * ObjectMapper with DefaultScalaModule registered. 28 | */ 29 | def createObjectMapper(): ObjectMapper = { 30 | val objectMapper = new ObjectMapper() 31 | objectMapper.registerModule(DefaultScalaModule) 32 | } 33 | 34 | private lazy val objectMapper: ObjectMapper = createObjectMapper() 35 | 36 | DefaultDataTableEntryTransformer(emptyStringReplacement) { 37 | (fromValue: Map[String, String], toValueType: java.lang.reflect.Type) => 38 | objectMapper.convertValue[AnyRef]( 39 | fromValue, 40 | objectMapper.constructType(toValueType) 41 | ) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/ScalaDslDocStringTypeTest.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend._ 4 | import org.junit.jupiter.api.Test 5 | 6 | import scala.annotation.nowarn 7 | 8 | @nowarn 9 | class ScalaDslDocStringTypeTest { 10 | 11 | @Test 12 | def testDocStringType(): Unit = { 13 | 14 | class Glue extends ScalaDsl with EN { 15 | DocStringType("doc") { docString => 16 | new StringBuilder(docString) 17 | } 18 | } 19 | 20 | val glue = new Glue() 21 | 22 | assertClassDocStringType(glue.registry.docStringTypes.head) 23 | } 24 | 25 | // -------------------- Test on object -------------------- 26 | // Note: for now there is no difference between the two in ScalaDsl but better safe than sorry 27 | 28 | @Test 29 | def testObjectDocStringType(): Unit = { 30 | 31 | object Glue extends ScalaDsl with EN { 32 | DocStringType("doc") { docString => 33 | new StringBuilder(docString) 34 | } 35 | } 36 | 37 | assertObjectDocStringType(Glue.registry.docStringTypes.head) 38 | } 39 | 40 | private def assertClassDocStringType( 41 | details: ScalaDocStringTypeDetails[_] 42 | ): Unit = { 43 | assertDocStringType(ScalaDocStringTypeDefinition(details, true)) 44 | } 45 | 46 | private def assertObjectDocStringType( 47 | details: ScalaDocStringTypeDetails[_] 48 | ): Unit = { 49 | assertDocStringType(ScalaDocStringTypeDefinition(details, false)) 50 | } 51 | 52 | private def assertDocStringType( 53 | docStringType: DocStringTypeDefinition 54 | ): Unit = { 55 | // Cannot assert much because everything is strangely private in DocStringTypeDefinition 56 | // Real feature tests will do the job 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDefaultParameterTransformerDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.lang.reflect.Type 4 | import io.cucumber.core.backend.{ 5 | DefaultParameterTransformerDefinition, 6 | ScenarioScoped 7 | } 8 | import io.cucumber.cucumberexpressions.ParameterByTypeTransformer 9 | 10 | import scala.annotation.nowarn 11 | 12 | trait ScalaDefaultParameterTransformerDefinition 13 | extends DefaultParameterTransformerDefinition 14 | with AbstractGlueDefinition { 15 | 16 | val details: ScalaDefaultParameterTransformerDetails 17 | 18 | override val location: StackTraceElement = details.stackTraceElement 19 | 20 | override val parameterByTypeTransformer: ParameterByTypeTransformer = 21 | (fromValue: String, toValue: Type) => { 22 | details.body.apply(fromValue, toValue) 23 | } 24 | 25 | } 26 | 27 | object ScalaDefaultParameterTransformerDefinition { 28 | 29 | def apply( 30 | details: ScalaDefaultParameterTransformerDetails, 31 | scenarioScoped: Boolean 32 | ): ScalaDefaultParameterTransformerDefinition = { 33 | if (scenarioScoped) { 34 | new ScalaScenarioScopedDefaultParameterTransformerDefinition(details) 35 | } else { 36 | new ScalaGlobalDefaultParameterTransformerDefinition(details) 37 | } 38 | } 39 | 40 | } 41 | 42 | @nowarn 43 | class ScalaScenarioScopedDefaultParameterTransformerDefinition( 44 | override val details: ScalaDefaultParameterTransformerDetails 45 | ) extends ScalaDefaultParameterTransformerDefinition 46 | with ScenarioScoped {} 47 | 48 | class ScalaGlobalDefaultParameterTransformerDefinition( 49 | override val details: ScalaDefaultParameterTransformerDetails 50 | ) extends ScalaDefaultParameterTransformerDefinition {} 51 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaParameterTypeDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.{ParameterTypeDefinition, ScenarioScoped} 4 | import io.cucumber.cucumberexpressions.{CaptureGroupTransformer, ParameterType} 5 | 6 | import scala.annotation.nowarn 7 | import scala.jdk.CollectionConverters._ 8 | 9 | trait ScalaParameterTypeDefinition[R] 10 | extends ParameterTypeDefinition 11 | with AbstractGlueDefinition { 12 | 13 | val details: ScalaParameterTypeDetails[R] 14 | 15 | override val location: StackTraceElement = details.stackTraceElement 16 | 17 | private val transformer: CaptureGroupTransformer[R] = 18 | (parameterContent: Array[String]) => { 19 | details.body.apply(parameterContent.toList) 20 | } 21 | 22 | override val parameterType: ParameterType[R] = new ParameterType[R]( 23 | details.name, 24 | Seq(details.regex).asJava, 25 | details.tag.runtimeClass.asInstanceOf[Class[R]], 26 | transformer 27 | ) 28 | 29 | } 30 | 31 | object ScalaParameterTypeDefinition { 32 | 33 | def apply[R]( 34 | stepDetails: ScalaParameterTypeDetails[R], 35 | scenarioScoped: Boolean 36 | ): ScalaParameterTypeDefinition[R] = { 37 | if (scenarioScoped) { 38 | new ScalaScenarioScopedParameterTypeDefinition(stepDetails) 39 | } else { 40 | new ScalaGlobalParameterTypeDefinition(stepDetails) 41 | } 42 | } 43 | 44 | } 45 | 46 | @nowarn 47 | class ScalaScenarioScopedParameterTypeDefinition[R]( 48 | override val details: ScalaParameterTypeDetails[R] 49 | ) extends ScalaParameterTypeDefinition[R] 50 | with ScenarioScoped {} 51 | 52 | class ScalaGlobalParameterTypeDefinition[R]( 53 | override val details: ScalaParameterTypeDetails[R] 54 | ) extends ScalaParameterTypeDefinition[R] {} 55 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaStepDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.lang.reflect.{Type => JType} 4 | import java.util.{List => JList} 5 | import io.cucumber.core.backend.{ParameterInfo, ScenarioScoped, StepDefinition} 6 | 7 | import scala.annotation.nowarn 8 | import scala.jdk.CollectionConverters._ 9 | 10 | trait ScalaStepDefinition extends StepDefinition with AbstractGlueDefinition { 11 | 12 | val stepDetails: ScalaStepDetails 13 | 14 | override val location: StackTraceElement = stepDetails.frame 15 | 16 | override val parameterInfos: JList[ParameterInfo] = fromTypes( 17 | stepDetails.types 18 | ) 19 | 20 | private def fromTypes(types: Seq[JType]): JList[ParameterInfo] = { 21 | types 22 | .map(new ScalaTypeResolver(_)) 23 | .map(new ScalaParameterInfo(_)) 24 | .toList 25 | .asInstanceOf[List[ParameterInfo]] 26 | .asJava 27 | } 28 | 29 | override def execute(args: Array[AnyRef]): Unit = { 30 | executeAsCucumber { 31 | stepDetails.body(args.toList) 32 | () 33 | } 34 | } 35 | 36 | override def getPattern: String = stepDetails.pattern 37 | 38 | } 39 | 40 | object ScalaStepDefinition { 41 | 42 | def apply( 43 | stepDetails: ScalaStepDetails, 44 | scenarioScoped: Boolean 45 | ): ScalaStepDefinition = { 46 | if (scenarioScoped) { 47 | new ScalaScenarioScopedStepDefinition(stepDetails) 48 | } else { 49 | new ScalaGlobalStepDefinition(stepDetails) 50 | } 51 | } 52 | 53 | } 54 | 55 | @nowarn 56 | class ScalaScenarioScopedStepDefinition( 57 | override val stepDetails: ScalaStepDetails 58 | ) extends ScalaStepDefinition 59 | with ScenarioScoped {} 60 | 61 | class ScalaGlobalStepDefinition(override val stepDetails: ScalaStepDetails) 62 | extends ScalaStepDefinition {} 63 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/Jackson3DefaultDataTableEntryTransformer.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import tools.jackson.databind.ObjectMapper 4 | import tools.jackson.databind.json.JsonMapper 5 | import tools.jackson.module.scala.ScalaModule 6 | 7 | /**

This trait register a `DefaultDataTableEntryTransformer` using Jackson 8 | * `ObjectMapper`.

9 | * 10 | *

The `[empty]` string is used as default empty string replacement. You can 11 | * override it if you need to.

12 | * 13 | *

Note: Jackson is not included with Cucumber Scala, you have to add the 14 | * dependency: `tools.jackson.module:jackson-module-scala` to your project if 15 | * you want to use this trait.

16 | * 17 | *

For Jackson 2.x, use `JacksonDefaultDataTableEntryTransformer` 18 | * instead.

19 | */ 20 | trait Jackson3DefaultDataTableEntryTransformer extends ScalaDsl { 21 | 22 | /** Define the string to be used as replacement for empty. Default is 23 | * `[empty]`. 24 | */ 25 | def emptyStringReplacement: String = "[empty]" 26 | 27 | /** Create the Jackson ObjectMapper to be used. Default is a simple JsonMapper 28 | * with ScalaModule (including all builtin modules) registered. 29 | */ 30 | def createObjectMapper(): ObjectMapper = { 31 | val scalaModule = ScalaModule 32 | .builder() 33 | .addAllBuiltinModules() 34 | .build() 35 | JsonMapper.builder().addModule(scalaModule).build() 36 | } 37 | 38 | private lazy val objectMapper: ObjectMapper = createObjectMapper() 39 | 40 | DefaultDataTableEntryTransformer(emptyStringReplacement) { 41 | (fromValue: Map[String, String], toValueType: java.lang.reflect.Type) => 42 | objectMapper.convertValue[AnyRef]( 43 | fromValue, 44 | objectMapper.constructType(toValueType) 45 | ) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaHookDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.{HookDefinition, ScenarioScoped, TestCaseState} 4 | import io.cucumber.scala.ScopedHookType.{AFTER, AFTER_STEP, BEFORE, BEFORE_STEP} 5 | 6 | import java.util.Optional 7 | import scala.annotation.nowarn 8 | 9 | trait ScalaHookDefinition extends HookDefinition with AbstractGlueDefinition { 10 | 11 | val hookDetails: ScalaHookDetails 12 | 13 | override val location: StackTraceElement = hookDetails.stackTraceElement 14 | 15 | override def execute(state: TestCaseState): Unit = { 16 | executeAsCucumber(hookDetails.body.apply(new Scenario(state))) 17 | } 18 | 19 | override def getTagExpression: String = hookDetails.tagExpression 20 | 21 | override def getOrder: Int = hookDetails.order 22 | 23 | override def getHookType: Optional[HookDefinition.HookType] = { 24 | val javaHookType = hookDetails.hookType match { 25 | case BEFORE => HookDefinition.HookType.BEFORE 26 | case AFTER => HookDefinition.HookType.AFTER 27 | case BEFORE_STEP => HookDefinition.HookType.BEFORE_STEP 28 | case AFTER_STEP => HookDefinition.HookType.AFTER_STEP 29 | } 30 | Optional.of(javaHookType) 31 | } 32 | 33 | } 34 | 35 | object ScalaHookDefinition { 36 | 37 | def apply( 38 | scalaHookDetails: ScalaHookDetails, 39 | scenarioScoped: Boolean 40 | ): ScalaHookDefinition = { 41 | if (scenarioScoped) { 42 | new ScalaScenarioScopedHookDefinition(scalaHookDetails) 43 | } else { 44 | new ScalaGlobalHookDefinition(scalaHookDetails) 45 | } 46 | } 47 | 48 | } 49 | 50 | @nowarn 51 | class ScalaScenarioScopedHookDefinition( 52 | override val hookDetails: ScalaHookDetails 53 | ) extends ScalaHookDefinition 54 | with ScenarioScoped {} 55 | 56 | class ScalaGlobalHookDefinition(override val hookDetails: ScalaHookDetails) 57 | extends ScalaHookDefinition {} 58 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDefaultDataTableCellTransformerDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.lang.reflect.Type 4 | import io.cucumber.core.backend.{ 5 | DefaultDataTableCellTransformerDefinition, 6 | ScenarioScoped 7 | } 8 | import io.cucumber.datatable.TableCellByTypeTransformer 9 | 10 | import scala.annotation.nowarn 11 | 12 | trait ScalaDefaultDataTableCellTransformerDefinition 13 | extends DefaultDataTableCellTransformerDefinition 14 | with AbstractDatatableElementTransformerDefinition { 15 | 16 | val details: ScalaDefaultDataTableCellTransformerDetails 17 | 18 | override val emptyPatterns: Seq[String] = details.emptyPatterns 19 | 20 | override val location: StackTraceElement = details.stackTraceElement 21 | 22 | override val tableCellByTypeTransformer: TableCellByTypeTransformer = 23 | (fromValue: String, toTypeValue: Type) => { 24 | details.body.apply( 25 | replaceEmptyPatternsWithEmptyString(fromValue), 26 | toTypeValue 27 | ) 28 | } 29 | 30 | } 31 | 32 | object ScalaDefaultDataTableCellTransformerDefinition { 33 | 34 | def apply( 35 | details: ScalaDefaultDataTableCellTransformerDetails, 36 | scenarioScoped: Boolean 37 | ): ScalaDefaultDataTableCellTransformerDefinition = { 38 | if (scenarioScoped) { 39 | new ScalaScenarioScopedDataTableCellTransformerDefinition(details) 40 | } else { 41 | new ScalaGlobalDataTableCellTransformerDefinition(details) 42 | } 43 | } 44 | 45 | } 46 | 47 | @nowarn 48 | class ScalaScenarioScopedDataTableCellTransformerDefinition( 49 | override val details: ScalaDefaultDataTableCellTransformerDetails 50 | ) extends ScalaDefaultDataTableCellTransformerDefinition 51 | with ScenarioScoped {} 52 | 53 | class ScalaGlobalDataTableCellTransformerDefinition( 54 | override val details: ScalaDefaultDataTableCellTransformerDetails 55 | ) extends ScalaDefaultDataTableCellTransformerDefinition {} 56 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/parametertypes/ParameterTypes.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to handle ParameterType definitions 2 | 3 | Scenario: define parameter type with single argument 4 | Given "string builder" parameter, defined by lambda 5 | 6 | Scenario: define parameter type with two arguments 7 | Given balloon coordinates 123,456, defined by lambda 8 | 9 | Scenario: define parameter type with three arguments 10 | Given kebab made from mushroom, meat and veg, defined by lambda 11 | 12 | Scenario: define parameter type with parameterized type, string undefined 13 | Given an optional string parameter value "" undefined 14 | 15 | Scenario: define parameter type with parameterized type, string defined 16 | Given an optional string parameter value "toto" defined 17 | 18 | Scenario: define parameter type with parameterized type, int undefined 19 | Given an optional int parameter value undefined 20 | 21 | Scenario: define parameter type with parameterized type, int defined 22 | Given an optional int parameter value 5 defined 23 | 24 | Scenario: define default parameter transformer 25 | Given kebab made from anonymous meat, defined by lambda 26 | 27 | Scenario: define default data table cell transformer - DataTable 28 | Given default data table cells, defined by lambda 29 | | Kebab | 30 | | [empty] | 31 | 32 | Scenario: define default data table cell transformer - JList[Jlist] 33 | Given default data table cells, defined by lambda, as rows 34 | | Kebab | 35 | | [empty] | 36 | 37 | Scenario: define default data table entry transformer - DataTable 38 | Given default data table entries, defined by lambda 39 | | dinner | 40 | | Kebab | 41 | | [empty] | 42 | 43 | Scenario: define default data table entry transformer - JList 44 | Given default data table entries, defined by lambda, as rows 45 | | dinner | 46 | | Kebab | 47 | | [empty] | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDataTableTypeDetails.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import scala.reflect.ClassTag 4 | 5 | sealed trait ScalaDataTableTypeDetails[T] { 6 | def emptyPatterns: Seq[String] 7 | def tag: ClassTag[T] 8 | def stackTraceElement: StackTraceElement 9 | } 10 | 11 | case class ScalaDataTableEntryTypeDetails[T]( 12 | emptyPatterns: Seq[String], 13 | body: DataTableEntryDefinitionBody[T], 14 | tag: ClassTag[T], 15 | stackTraceElement: StackTraceElement 16 | ) extends ScalaDataTableTypeDetails[T] 17 | 18 | case class ScalaDataTableOptionalEntryTypeDetails[T]( 19 | emptyPatterns: Seq[String], 20 | body: DataTableOptionalEntryDefinitionBody[T], 21 | tag: ClassTag[T], 22 | stackTraceElement: StackTraceElement 23 | ) extends ScalaDataTableTypeDetails[T] 24 | 25 | case class ScalaDataTableRowTypeDetails[T]( 26 | emptyPatterns: Seq[String], 27 | body: DataTableRowDefinitionBody[T], 28 | tag: ClassTag[T], 29 | stackTraceElement: StackTraceElement 30 | ) extends ScalaDataTableTypeDetails[T] 31 | 32 | case class ScalaDataTableOptionalRowTypeDetails[T]( 33 | emptyPatterns: Seq[String], 34 | body: DataTableOptionalRowDefinitionBody[T], 35 | tag: ClassTag[T], 36 | stackTraceElement: StackTraceElement 37 | ) extends ScalaDataTableTypeDetails[T] 38 | 39 | case class ScalaDataTableCellTypeDetails[T]( 40 | emptyPatterns: Seq[String], 41 | body: DataTableCellDefinitionBody[T], 42 | tag: ClassTag[T], 43 | stackTraceElement: StackTraceElement 44 | ) extends ScalaDataTableTypeDetails[T] 45 | 46 | case class ScalaDataTableOptionalCellTypeDetails[T]( 47 | emptyPatterns: Seq[String], 48 | body: DataTableOptionalCellDefinitionBody[T], 49 | tag: ClassTag[T], 50 | stackTraceElement: StackTraceElement 51 | ) extends ScalaDataTableTypeDetails[T] 52 | 53 | case class ScalaDataTableTableTypeDetails[T]( 54 | emptyPatterns: Seq[String], 55 | body: DataTableDefinitionBody[T], 56 | tag: ClassTag[T], 57 | stackTraceElement: StackTraceElement 58 | ) extends ScalaDataTableTypeDetails[T] 59 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/docstring/DocStringSteps.scala: -------------------------------------------------------------------------------- 1 | package docstring 2 | 3 | import io.cucumber.scala.{EN, ScalaDsl} 4 | 5 | class DocStringSteps extends ScalaDsl with EN { 6 | 7 | case class JsonText(json: String) 8 | 9 | case class XmlText(xml: String) 10 | 11 | case class RawText(raw: String) 12 | 13 | var _text: Any = _ 14 | 15 | DocStringType("json") { (text) => 16 | JsonText(text) 17 | } 18 | 19 | DocStringType("xml") { (text) => 20 | XmlText(text) 21 | } 22 | 23 | DocStringType("") { (text) => 24 | RawText(text) 25 | } 26 | 27 | // Tests generic type 28 | DocStringType[Seq[String]]("") { (text) => 29 | text.split('\n').toSeq 30 | } 31 | 32 | DocStringType[Seq[Int]]("") { (text) => 33 | text.split('\n').map(_.toInt).toSeq 34 | } 35 | 36 | Given("the following json text") { (json: JsonText) => 37 | _text = json 38 | } 39 | 40 | Given("the following xml text") { (xml: XmlText) => 41 | _text = xml 42 | } 43 | 44 | Given("the following raw text") { (raw: RawText) => 45 | _text = raw 46 | } 47 | 48 | Given("the following string list") { (list: Seq[String]) => 49 | _text = list 50 | } 51 | 52 | Given("the following int list") { (list: Seq[Int]) => 53 | _text = list 54 | } 55 | 56 | Then("I have a json text") { 57 | assert(_text.isInstanceOf[JsonText]) 58 | } 59 | 60 | Then("I have a xml text") { 61 | assert(_text.isInstanceOf[XmlText]) 62 | } 63 | 64 | Then("I have a raw text") { 65 | assert(_text.isInstanceOf[RawText]) 66 | } 67 | 68 | Then("I have a string list {string}") { (expectedList: String) => 69 | assert(_text.isInstanceOf[Seq[_]]) 70 | assert(_text.asInstanceOf[Seq[_]].head.isInstanceOf[String]) 71 | assert(_text.asInstanceOf[Seq[String]] == expectedList.split(',').toSeq) 72 | } 73 | 74 | Then("I have a int list {string}") { (expectedList: String) => 75 | assert(_text.isInstanceOf[Seq[_]]) 76 | assert(_text.asInstanceOf[Seq[_]].head.isInstanceOf[Int]) 77 | assert( 78 | _text.asInstanceOf[Seq[Int]] == expectedList.split(',').map(_.toInt).toSeq 79 | ) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDefaultDataTableEntryTransformerDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.lang.reflect.Type 4 | import java.util.{Map => JavaMap} 5 | import io.cucumber.core.backend.{ 6 | DefaultDataTableEntryTransformerDefinition, 7 | ScenarioScoped 8 | } 9 | import io.cucumber.datatable.{ 10 | TableCellByTypeTransformer, 11 | TableEntryByTypeTransformer 12 | } 13 | 14 | import scala.annotation.nowarn 15 | import scala.jdk.CollectionConverters._ 16 | 17 | trait ScalaDefaultDataTableEntryTransformerDefinition 18 | extends DefaultDataTableEntryTransformerDefinition 19 | with AbstractDatatableElementTransformerDefinition { 20 | 21 | val details: ScalaDefaultDataTableEntryTransformerDetails 22 | 23 | override val emptyPatterns: Seq[String] = details.emptyPatterns 24 | 25 | override val location: StackTraceElement = details.stackTraceElement 26 | 27 | override val tableEntryByTypeTransformer: TableEntryByTypeTransformer = ( 28 | fromValue: JavaMap[String, String], 29 | toValueType: Type, 30 | _: TableCellByTypeTransformer 31 | ) => { 32 | replaceEmptyPatternsWithEmptyString(fromValue.asScala.toMap) 33 | .map(details.body.apply(_, toValueType)) 34 | .get 35 | } 36 | 37 | override val headersToProperties: Boolean = true 38 | 39 | } 40 | 41 | object ScalaDefaultDataTableEntryTransformerDefinition { 42 | 43 | def apply( 44 | details: ScalaDefaultDataTableEntryTransformerDetails, 45 | scenarioScoped: Boolean 46 | ): ScalaDefaultDataTableEntryTransformerDefinition = { 47 | if (scenarioScoped) { 48 | new ScalaScenarioScopedDataTableEntryTransformerDefinition(details) 49 | } else { 50 | new ScalaGlobalDataTableEntryTransformerDefinition(details) 51 | } 52 | } 53 | 54 | } 55 | 56 | @nowarn 57 | class ScalaScenarioScopedDataTableEntryTransformerDefinition( 58 | override val details: ScalaDefaultDataTableEntryTransformerDetails 59 | ) extends ScalaDefaultDataTableEntryTransformerDefinition 60 | with ScenarioScoped {} 61 | 62 | class ScalaGlobalDataTableEntryTransformerDefinition( 63 | override val details: ScalaDefaultDataTableEntryTransformerDetails 64 | ) extends ScalaDefaultDataTableEntryTransformerDefinition {} 65 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/codegen/gen.scala: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Generates the evil looking apply methods in StepDsl#StepBody for Function1 to Function22 4 | * Scala 3 5 | */ 6 | for (i <- 1 to 22) { 7 | val ts = (1 to i).map("T".+).mkString(", ") 8 | val implicits = (1 to i).map(n => s"t$n: Stepable[T$n]").mkString(", ") 9 | val implicitsParams = (1 to i).map(n => s"t$n").mkString(", ") 10 | val listParams = (1 to i).map("a" + _ + ":AnyRef").mkString(", ") 11 | val pf = (1 to i).map(n => "a" + n + ".asInstanceOf[T" + n + "]").mkString(",\n ") 12 | 13 | println(s""" 14 | |def apply[$ts](f: ($ts) => Any)(using $implicits): Unit = { 15 | | register($implicitsParams) { 16 | | case List($listParams) => 17 | | f($pf) 18 | | case _ => 19 | | throw new IncorrectStepDefinitionException() 20 | | } 21 | |}""".stripMargin) 22 | } 23 | 24 | /* 25 | * Generates the apply methods in ParameterTypeDsl for Function1 to Function22 26 | */ 27 | for (i <- 1 to 22) { 28 | // String, String, ..., String 29 | val types = (1 to i).map(_ => "String").mkString(", ") 30 | // p1, p2, ..., p22 31 | val args = (1 to i).map(j => s"p$j").mkString(", ") 32 | 33 | val template = 34 | s""" 35 | |def apply[R](f: ($types) => R)(implicit tag: ClassTag[R]): Unit = { 36 | | register { 37 | | case List($args) => 38 | | f($args) 39 | | } 40 | |} 41 | |""".stripMargin 42 | 43 | println(template) 44 | } 45 | 46 | /* 47 | * Generates the Stepable implicit methods 48 | */ 49 | for (i <- (1 to 9).reverse) { 50 | 51 | val underscores = (1 to i).map(_ => "_").mkString(", ") 52 | val types = (1 to i).map(j => s"X$j").mkString(", ") 53 | val typesStepable = (1 to i).map(j => s"X$j: Stepable").mkString(", ") 54 | val typeArgs = (1 to i).map(j => s"implicitly[Stepable[X$j]].asJavaType").mkString(", ") 55 | 56 | val template = 57 | s""" 58 | |implicit def stepable$i[T[$underscores], $typesStepable](implicit ct: ClassTag[T[$types]]): Stepable[T[$types]] = 59 | | new Stepable[T[$types]] { 60 | | def asJavaType: JavaType = 61 | | new ScalaParameterizedType( 62 | | ct.runtimeClass, 63 | | Array( 64 | | $typeArgs 65 | | ) 66 | | ) 67 | | }""".stripMargin 68 | 69 | println(template) 70 | } 71 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/IncorrectHookDefinitionException.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.CucumberBackendException 4 | 5 | sealed abstract class IncorrectHookDefinitionException(message: String) 6 | extends CucumberBackendException(message) 7 | 8 | object IncorrectHookDefinitionException { 9 | 10 | def undefinedHooksErrorMessage(expectedHooks: Seq[UndefinedHook]): String = { 11 | val hooksListToDisplay = expectedHooks.map { eh => 12 | s" - ${eh.stackTraceElement.getFileName}:${eh.stackTraceElement.getLineNumber} (${eh.hookType})" 13 | } 14 | 15 | s"""Some hooks are not defined properly: 16 | |${hooksListToDisplay.mkString("\n")} 17 | | 18 | |This can be caused by defining hooks where the body returns a Int or String rather than Unit. 19 | | 20 | |For instance, the following code: 21 | | 22 | | Before { 23 | | someInitMethodReturningInt() 24 | | } 25 | | 26 | |Should be replaced with: 27 | | 28 | | Before { 29 | | someInitMethodReturningInt() 30 | | () 31 | | } 32 | |""".stripMargin 33 | } 34 | 35 | def scenarioScopedStaticHookErrorMessage( 36 | staticHooks: Seq[ScalaStaticHookDetails] 37 | ): String = { 38 | val hooksListToDisplay: Seq[String] = staticHooks.map { h => 39 | s" - ${h.stackTraceElement.getFileName}:${h.stackTraceElement.getLineNumber}" 40 | } 41 | 42 | s"""Some hooks are not defined properly: 43 | |${hooksListToDisplay.mkString("\n")} 44 | | 45 | |This can be caused by defining static hooks (BeforeAll/AfterAll) in a class rather than in a object. 46 | |Such hooks can only be defined in a static context. 47 | |""".stripMargin 48 | } 49 | 50 | } 51 | 52 | class UndefinedHooksException(val undefinedHooks: Seq[UndefinedHook]) 53 | extends IncorrectHookDefinitionException( 54 | IncorrectHookDefinitionException.undefinedHooksErrorMessage( 55 | undefinedHooks 56 | ) 57 | ) {} 58 | 59 | class ScenarioScopedStaticHookException( 60 | val staticHooks: Seq[ScalaStaticHookDetails] 61 | ) extends IncorrectHookDefinitionException( 62 | IncorrectHookDefinitionException.scenarioScopedStaticHookErrorMessage( 63 | staticHooks 64 | ) 65 | ) {} 66 | 67 | case class UndefinedHook( 68 | hookType: HookType, 69 | stackTraceElement: StackTraceElement 70 | ) 71 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/AbstractDatatableElementTransformerDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.datatable.DataTable 4 | 5 | import scala.util.{Failure, Success, Try} 6 | import scala.jdk.CollectionConverters._ 7 | 8 | trait AbstractDatatableElementTransformerDefinition 9 | extends AbstractGlueDefinition { 10 | 11 | val emptyPatterns: Seq[String] 12 | 13 | protected def replaceEmptyPatternsWithEmptyString( 14 | row: Seq[String] 15 | ): Seq[String] = { 16 | row.map(replaceEmptyPatternsWithEmptyString) 17 | } 18 | 19 | protected def replaceEmptyPatternsWithEmptyString( 20 | table: DataTable 21 | ): DataTable = { 22 | val rawWithEmptyStrings = table 23 | .cells() 24 | .asScala 25 | .map(_.asScala.toSeq) 26 | .map(replaceEmptyPatternsWithEmptyString) 27 | .map(_.asJava) 28 | .toSeq 29 | .asJava 30 | 31 | DataTable.create(rawWithEmptyStrings, table.getTableConverter) 32 | } 33 | 34 | protected def replaceEmptyPatternsWithEmptyString( 35 | fromValue: Map[String, String] 36 | ): Try[Map[String, String]] = { 37 | val replacement = fromValue.toSeq.map { case (key, value) => 38 | val potentiallyEmptyKey = replaceEmptyPatternsWithEmptyString(key) 39 | val potentiallyEmptyValue = replaceEmptyPatternsWithEmptyString(value) 40 | 41 | (potentiallyEmptyKey, potentiallyEmptyValue) 42 | } 43 | 44 | if (containsDuplicateKey(replacement)) { 45 | Failure(createDuplicateKeyAfterReplacement(fromValue)) 46 | } else { 47 | Success(replacement.toMap) 48 | } 49 | } 50 | 51 | protected def replaceEmptyPatternsWithEmptyString(t: String): String = { 52 | if (emptyPatterns.contains(t)) { 53 | "" 54 | } else { 55 | t 56 | } 57 | } 58 | 59 | private def containsDuplicateKey(seq: Seq[(String, Any)]): Boolean = { 60 | seq.map { case (key, _) => key }.toSet.size != seq.size 61 | } 62 | 63 | private def createDuplicateKeyAfterReplacement( 64 | fromValue: Map[String, String] 65 | ): IllegalArgumentException = { 66 | val conflict = 67 | emptyPatterns.filter(emptyPattern => fromValue.contains(emptyPattern)) 68 | val msg = 69 | s"After replacing ${conflict.headOption 70 | .getOrElse("")} and ${conflict.drop(1).headOption.getOrElse("")} with empty strings the datatable entry contains duplicate keys: $fromValue" 71 | new IllegalArgumentException(msg) 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /scripts/update-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -uf -o pipefail 3 | 4 | # Reads a changelog from STDIN and writes out a new one to STDOUT where: 5 | # 6 | # * The [Unreleased] diff link is updated 7 | # * A new diff link for the new release is added 8 | # * The ## [Unreleased] header is changed to a version header with date 9 | # * The empty sections are removed 10 | # * A new, empty [Unreleased] paragraph is added at the top 11 | # 12 | 13 | changelog=$(&2 echo "No version found in link: ${unreleased_link}" 54 | exit 1 55 | fi 56 | 57 | # Insert a new release diff link 58 | 59 | insertion_line_number=$((line_number + 1)) 60 | release_link=$(echo "${changelog}" | head -n ${insertion_line_number} | tail -1) 61 | new_release_link=$(echo "${release_link}" | \ 62 | sed "s/${last_version}/${new_version}/g" | \ 63 | sed "s/v[0-9]\+.[0-9]\+.[0-9]\+/v${last_version}/") 64 | 65 | changelog=$(echo "${changelog}" | sed "${insertion_line_number} i \\ 66 | ${new_release_link} 67 | ") 68 | 69 | # Remove empty sections 70 | 71 | scripts_path="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" 72 | changelog=$(echo "${changelog}" | awk -f "${scripts_path}/remove-empty-sections-changelog.awk") 73 | 74 | # Insert a new [Unreleased] header 75 | 76 | changelog=$(echo "${changelog}" | sed "s/----/----\\ 77 | ${header_escaped}\\ 78 | /g") 79 | 80 | echo "${changelog}" 81 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaSnippet.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.lang.reflect.Type 4 | import java.text.MessageFormat 5 | import java.util.{Map => JMap} 6 | import java.util.Optional 7 | 8 | import io.cucumber.core.backend.Snippet 9 | import io.cucumber.datatable.DataTable 10 | 11 | import scala.jdk.CollectionConverters._ 12 | 13 | object ScalaSnippet { 14 | 15 | // Allows to use """ in """xxx"""" strings 16 | val tripleDoubleQuotes = "\"\"\"" 17 | 18 | } 19 | 20 | class ScalaSnippet extends Snippet { 21 | 22 | import ScalaSnippet.tripleDoubleQuotes 23 | 24 | override def language(): Optional[String] = { 25 | Optional.of("scala") 26 | } 27 | 28 | override def template(): MessageFormat = { 29 | new MessageFormat( 30 | s"""{0}(${tripleDoubleQuotes}{1}${tripleDoubleQuotes}) '{' ({3}) => 31 | | // {4} 32 | | throw new ${classOf[PendingException].getName}() 33 | |'}'""".stripMargin 34 | ) 35 | } 36 | 37 | override def tableHint(): String = { 38 | """| // For automatic transformation, change DataTable to one of 39 | | // E, List, List>, List>, Map or 40 | | // Map>. E,K,V must be a String, Integer, Float, 41 | | // Double, Byte, Short, Long, BigInteger or BigDecimal. 42 | | // 43 | | // For other transformations you can register a DataTableType.""".stripMargin 44 | } 45 | 46 | override def escapePattern(pattern: String): String = pattern 47 | 48 | override def arguments(map: JMap[String, Type]): String = { 49 | map.asScala 50 | .map { case (argName, argType) => s"$argName: ${getArgType(argType)}" } 51 | .mkString(", ") 52 | } 53 | 54 | private def getArgType(argType: Type): String = { 55 | argType match { 56 | // Scala classes 57 | // TODO is there a native Scala way of doing so? 58 | case cType: Class[_] if cType == classOf[java.lang.Integer] => "Int" 59 | case cType: Class[_] if cType == classOf[java.lang.Long] => "Long" 60 | case cType: Class[_] if cType == classOf[java.lang.Float] => "Float" 61 | case cType: Class[_] if cType == classOf[java.lang.Double] => "Double" 62 | // Java behavior 63 | case cType: Class[_] if cType == classOf[DataTable] => cType.getName 64 | case cType: Class[_] => cType.getSimpleName 65 | case _ => argType.toString 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaDataTableTypeDefinition.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.DataTableTypeDefinition 4 | 5 | trait ScalaDataTableTypeDefinition 6 | extends DataTableTypeDefinition 7 | with AbstractDatatableElementTransformerDefinition { 8 | 9 | val details: ScalaDataTableTypeDetails[_] 10 | 11 | override val location: StackTraceElement = details.stackTraceElement 12 | 13 | override val emptyPatterns: Seq[String] = details.emptyPatterns 14 | 15 | } 16 | 17 | object ScalaDataTableTypeDefinition { 18 | 19 | def apply[T]( 20 | details: ScalaDataTableTypeDetails[T], 21 | scenarioScoped: Boolean 22 | ): ScalaDataTableTypeDefinition = { 23 | details match { 24 | case entryDetails: ScalaDataTableEntryTypeDetails[_] => 25 | if (scenarioScoped) { 26 | new ScalaScenarioScopedDataTableEntryDefinition[T](entryDetails) 27 | } else { 28 | new ScalaGlobalDataTableEntryDefinition[T](entryDetails) 29 | } 30 | case entryDetails: ScalaDataTableOptionalEntryTypeDetails[_] => 31 | if (scenarioScoped) { 32 | new ScalaScenarioScopedDataTableOptionalEntryDefinition[T]( 33 | entryDetails 34 | ) 35 | } else { 36 | new ScalaGlobalDataTableOptionalEntryDefinition[T](entryDetails) 37 | } 38 | case rowDetails: ScalaDataTableRowTypeDetails[_] => 39 | if (scenarioScoped) { 40 | new ScalaScenarioScopedDataTableRowDefinition[T](rowDetails) 41 | } else { 42 | new ScalaGlobalDataTableRowDefinition[T](rowDetails) 43 | } 44 | case rowDetails: ScalaDataTableOptionalRowTypeDetails[_] => 45 | if (scenarioScoped) { 46 | new ScalaScenarioScopedDataTableOptionalRowDefinition[T](rowDetails) 47 | } else { 48 | new ScalaGlobalDataTableOptionalRowDefinition[T](rowDetails) 49 | } 50 | case cellDetails: ScalaDataTableCellTypeDetails[_] => 51 | if (scenarioScoped) { 52 | new ScalaScenarioScopedDataTableCellDefinition[T](cellDetails) 53 | } else { 54 | new ScalaGlobalDataTableCellDefinition[T](cellDetails) 55 | } 56 | case cellDetails: ScalaDataTableOptionalCellTypeDetails[_] => 57 | if (scenarioScoped) { 58 | new ScalaScenarioScopedDataTableOptionalCellDefinition[T](cellDetails) 59 | } else { 60 | new ScalaGlobalDataTableOptionalCellDefinition[T](cellDetails) 61 | } 62 | case rowDetails: ScalaDataTableTableTypeDetails[_] => 63 | if (scenarioScoped) { 64 | new ScalaScenarioScopedDataTableDefinition[T](rowDetails) 65 | } else { 66 | new ScalaGlobalDataTableDefinition[T](rowDetails) 67 | } 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/GlueAdaptor.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend.Glue 4 | 5 | class GlueAdaptor(glue: Glue) { 6 | 7 | /** Load the step definitions and hooks from a ScalaDsl instance into the 8 | * glue. 9 | * 10 | * @param registry 11 | * ScalaDsl instance registry 12 | * @param scenarioScoped 13 | * true for class instances, false for object singletons 14 | */ 15 | def loadRegistry( 16 | registry: ScalaDslRegistry, 17 | scenarioScoped: Boolean 18 | ): Unit = { 19 | 20 | // If the registry is not consistent, this indicates a mistake in the users definition and we want to let him know. 21 | registry.checkConsistency(scenarioScoped).left.foreach { 22 | (ex: IncorrectHookDefinitionException) => 23 | throw ex 24 | } 25 | 26 | registry.stepDefinitions 27 | .map(ScalaStepDefinition(_, scenarioScoped)) 28 | .foreach(glue.addStepDefinition) 29 | 30 | // The presence of beforeAll/afterAll hooks with scenarioScoped is checked by checkConsistency above 31 | if (!scenarioScoped) { 32 | registry.beforeAllHooks 33 | .map(ScalaStaticHookDefinition(_)) 34 | .foreach(glue.addBeforeAllHook) 35 | registry.afterAllHooks 36 | .map(ScalaStaticHookDefinition(_)) 37 | .foreach(glue.addAfterAllHook) 38 | } 39 | 40 | registry.beforeHooks 41 | .map(ScalaHookDefinition(_, scenarioScoped)) 42 | .foreach(glue.addBeforeHook) 43 | registry.afterHooks 44 | .map(ScalaHookDefinition(_, scenarioScoped)) 45 | .foreach(glue.addAfterHook) 46 | registry.beforeStepHooks 47 | .map(ScalaHookDefinition(_, scenarioScoped)) 48 | .foreach(glue.addBeforeStepHook) 49 | registry.afterStepHooks 50 | .map(ScalaHookDefinition(_, scenarioScoped)) 51 | .foreach(glue.addAfterStepHook) 52 | 53 | registry.docStringTypes 54 | .map(ScalaDocStringTypeDefinition(_, scenarioScoped)) 55 | .foreach(glue.addDocStringType) 56 | registry.dataTableTypes 57 | .map(ScalaDataTableTypeDefinition(_, scenarioScoped)) 58 | .foreach(glue.addDataTableType) 59 | registry.parameterTypes 60 | .map(ScalaParameterTypeDefinition(_, scenarioScoped)) 61 | .foreach(glue.addParameterType) 62 | 63 | registry.defaultParameterTransformers 64 | .map(ScalaDefaultParameterTransformerDefinition(_, scenarioScoped)) 65 | .foreach(glue.addDefaultParameterTransformer) 66 | registry.defaultDataTableCellTransformers 67 | .map(ScalaDefaultDataTableCellTransformerDefinition(_, scenarioScoped)) 68 | .foreach(glue.addDefaultDataTableCellTransformer) 69 | registry.defaultDataTableEntryTransformers 70 | .map(ScalaDefaultDataTableEntryTransformerDefinition(_, scenarioScoped)) 71 | .foreach(glue.addDefaultDataTableEntryTransformer) 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /docs/default_jackson_datatable_transformer.md: -------------------------------------------------------------------------------- 1 | # Default Jackson DataTable Transformer 2 | 3 | Cucumber Scala provides an optional Default DataTable Transformer that uses Jackson. 4 | 5 | It can be used to automatically convert DataTables to case classes without defining custom converters. 6 | 7 | ## Add Jackson dependency 8 | 9 | ### Jackson 2.x 10 | 11 | To use this optional transformer, you need to have Jackson Scala in your dependencies. 12 | 13 | ```xml 14 | 15 | com.fasterxml.jackson.module 16 | jackson-module-scala_2.13 17 | 2.20.0 18 | test 19 | 20 | ``` 21 | 22 | Or: 23 | ```sbt 24 | libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.20.0" % Test 25 | ``` 26 | 27 | The current version of Cucumber Scala has been tested against Jackson Module Scala **version 2.20.0**. 28 | 29 | ### Jackson 3.x 30 | 31 | To use this optional transformer, you need to have Jackson Scala in your dependencies. 32 | 33 | ```xml 34 | 35 | tools.jackson.module 36 | jackson-module-scala_2.13 37 | 3.0.0 38 | test 39 | 40 | ``` 41 | 42 | Or: 43 | ```sbt 44 | libraryDependencies += "tools.jackson.module" %% "jackson-module-scala" % "3.0.0" % Test 45 | ``` 46 | 47 | The current version of Cucumber Scala has been tested against Jackson Module Scala **version 3.0.0**. 48 | 49 | ## Add the transformer 50 | 51 | The transformer has to be added to your glue code by extending the `JacksonDefaultDataTableEntryTransformer` (Jackson 2.x) 52 | or `Jackson3DefaultDataTableEntryTransformer` (Jackson 3.x) trait. 53 | 54 | For instance: 55 | ```scala 56 | class MySteps extends ScalaDsl with EN with Jackson3DefaultDataTableEntryTransformer { 57 | // Your usual glue code 58 | } 59 | ``` 60 | 61 | Note that it should be included only once in your glue code. If you use multiple glue classes, either add it to only one of them or add it to a separate `object`. 62 | 63 | ### Empty string replacement 64 | 65 | The default empty string replacement used by the default transformer is `[empty]`. 66 | 67 | You can override it if you need to: 68 | ```scala 69 | override def emptyStringReplacement: String = "[blank]" 70 | ``` 71 | 72 | ## Example 73 | 74 | Then, let the transformer do its work! 75 | 76 | For instance, the following DataTable: 77 | ```gherkin 78 | Given I have the following datatable 79 | | field1 | field2 | field3 | 80 | | 1.2 | true | abc | 81 | | 2.3 | false | def | 82 | | 3.4 | true | ghj | 83 | ``` 84 | 85 | will be automatically converted to the following case class: 86 | ```scala 87 | case class MyCaseClass(field1: Double, field2: Boolean, field3: String) 88 | 89 | Given("I have the following datatable") { (data: java.util.List[MyCaseClass]) => 90 | // Do something 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/build.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | To compile and test the whole project you can run the following command: 4 | 5 | ```shell 6 | $ sbt clean compile test 7 | ``` 8 | 9 | ## Project structure 10 | 11 | The project contains several subprojects: 12 | - `cucumber-scala`: contains the codebase of the Cucumber Scala implementation 13 | - `integration-tests`: contains integration tests projects 14 | - `common`: general integration tests 15 | - `jackson2`: Jackson 2.x integration specific tests 16 | - `jackson3`: Jackson 3.x integration specific tests 17 | - `picocontainer`: Picocontainer integration specific tests 18 | - `examples`: contains a sample project 19 | 20 | Each of these subproject is also derived for each target Scala version. See below. 21 | 22 | ## Cross compilation 23 | 24 | Cross compilation to multiple Scala versions is handled by the [sbt-projectmatrix](https://github.com/sbt/sbt-projectmatrix) plugin. 25 | 26 | The target versions are defined in the `build.sbt` on each sub-project: 27 | ```scala 28 | project 29 | ... 30 | .jvmPlatform(scalaVersions = Seq(scala3, scala213, scala212)) 31 | ``` 32 | 33 | A sbt sub-project is generated for each targeted Scala version with the version as a suffix 34 | (with Scala 2.13 being the current default version): 35 | 36 | ```shell 37 | $ sbt projects 38 | ... 39 | [info] cucumberScala 40 | [info] cucumberScala2_12 41 | [info] cucumberScala3 42 | [info] examples 43 | [info] examples2_12 44 | [info] examples3 45 | [info] * root 46 | ``` 47 | 48 | ### Sources 49 | 50 | Sources should most of the time be compatible for all target Scala versions in order to ease maintenance. 51 | 52 | However, if needed, it's possible to define different sources for each Scala version. 53 | 54 | These version-specific sources should be put in a directory called `src/main/scala-` and the `build.sbt` 55 | should declare them as additional sources directories like: 56 | ```scala 57 | Compile / unmanagedSourceDirectories ++= { 58 | val sourceDir = (Compile / sourceDirectory).value 59 | CrossVersion.partialVersion(scalaVersion.value) match { 60 | case Some((2, n)) => 61 | Seq(sourceDir / "scala-2") 62 | case Some((3, 0)) => 63 | Seq(sourceDir / "scala-3") 64 | case _ => 65 | Seq() 66 | } 67 | } 68 | ``` 69 | 70 | The same can be done for tests. 71 | 72 | ## Language traits generation 73 | 74 | The language traits (`io.cucumber.scala.EN` for instance) are generated automatically at compile time. 75 | 76 | The `project` meta project defines a `I18nGenerator` object responsible for generating their content. 77 | 78 | At compilation, this generated content is injected as source files thanks to a sbt source generator: 79 | ```scala 80 | // Generate I18n traits 81 | Compile / sourceGenerators += Def.task { 82 | val file = 83 | (Compile / sourceManaged).value / "io/cucumber/scala" / "I18n.scala" 84 | IO.write(file, I18nGenerator.i18n) 85 | Seq(file) 86 | }.taskValue 87 | ``` 88 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/cukes/cukes.feature: -------------------------------------------------------------------------------- 1 | Feature: Cukes 2 | 3 | Scenario: in the belly 4 | Given I have 4 "cukes" in my belly 5 | Then I am "happy" 6 | 7 | Scenario: Int in the belly 8 | Given I have eaten an int 100 9 | Then I should have one hundred in my belly 10 | 11 | Scenario: Long in the belly 12 | Given I have eaten a long 100 13 | Then I should have long one hundred in my belly 14 | 15 | Scenario: String in the belly 16 | Given I have eaten "numnumnum" 17 | Then I should have numnumnum in my belly 18 | 19 | Scenario: Double in the belly 20 | Given I have eaten 1.5 doubles 21 | Then I should have one and a half doubles in my belly 22 | 23 | Scenario: Float in the belly 24 | Given I have eaten 1.5 floats 25 | Then I should have one and a half floats in my belly 26 | 27 | Scenario: Short in the belly 28 | Given I have eaten a short 100 29 | Then I should have short one hundred in my belly 30 | 31 | Scenario: Byte in the belly 32 | Given I have eaten a byte 2 33 | Then I should have two byte in my belly 34 | 35 | Scenario: BigDecimal in the belly 36 | Given I have eaten 1.5 big decimals 37 | Then I should have one and a half big decimals in my belly 38 | 39 | Scenario: BigInt in the belly 40 | Given I have eaten 10 big int 41 | Then I should have a ten big int in my belly 42 | 43 | Scenario: Char in the belly 44 | Given I have eaten char 'C' 45 | Then I should have character C in my belly 46 | 47 | Scenario: Boolean in the belly 48 | Given I have eaten boolean true 49 | Then I should have truth in my belly 50 | 51 | Scenario: DataTable in the belly 52 | Given I have the following foods : 53 | | FOOD | CALORIES | 54 | | cheese | 500 | 55 | | burger | 1000 | 56 | | fries | 750 | 57 | Then I am "definitely happy" 58 | And have eaten 2250.0 calories today 59 | 60 | Scenario: DataTable with args in the belly 61 | Given I have a table the sum of all rows should be 400 : 62 | | ROW | 63 | | 20 | 64 | | 80 | 65 | | 300 | 66 | 67 | Scenario: Argh! a snake - to be custom mapped 68 | Given I see in the distance ... =====> 69 | Then I have a snake of length 6 moving east 70 | And I see in the distance ... <==================== 71 | Then I have a snake of length 21 moving west 72 | 73 | Scenario: Custom object with string constructor 74 | Given I have a person Bob 75 | Then he should say "Hello, I'm Bob!" 76 | 77 | Scenario: Custom objects in the belly 78 | Given I have eaten the following cukes 79 | | Color | Number | 80 | | Green | 1 | 81 | | Red | 3 | 82 | | Blue | 2 | 83 | Then I should have eaten 6 cukes 84 | And they should have been Green, Red, Blue 85 | 86 | Scenario: Did you know that we can handle call by name and zero arity 87 | Given I drink gin and vermouth 88 | When I shake my belly 89 | Then I should have lots of martinis 90 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/ScalaDslDefaultParameterTransformerTest.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend._ 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Test 6 | 7 | class ScalaDslDefaultParameterTransformerTest { 8 | 9 | @Test 10 | def testClassDefaultParameterTransformer(): Unit = { 11 | 12 | class Glue extends ScalaDsl with EN { 13 | DefaultParameterTransformer { 14 | (fromValue: String, toValueType: java.lang.reflect.Type) => 15 | new StringBuilder().append(fromValue).append("-").append(toValueType) 16 | } 17 | } 18 | 19 | val glue = new Glue() 20 | 21 | assertClassDefaultParameterTransformer( 22 | glue.registry.defaultParameterTransformers.head, 23 | "meat", 24 | classOf[StringBuilder], 25 | "meat-class scala.collection.mutable.StringBuilder" 26 | ) 27 | } 28 | 29 | // -------------------- Test on object -------------------- 30 | // Note: for now there is no difference between the two in ScalaDsl but better safe than sorry 31 | 32 | @Test 33 | def testObjectDefaultParameterTransformer(): Unit = { 34 | 35 | object Glue extends ScalaDsl with EN { 36 | DefaultParameterTransformer { 37 | (fromValue: String, toValueType: java.lang.reflect.Type) => 38 | new StringBuilder().append(fromValue).append("-").append(toValueType) 39 | } 40 | } 41 | 42 | assertObjectDefaultParameterTransformer( 43 | Glue.registry.defaultParameterTransformers.head, 44 | "meat", 45 | classOf[StringBuilder], 46 | "meat-class scala.collection.mutable.StringBuilder" 47 | ) 48 | } 49 | 50 | private def assertClassDefaultParameterTransformer( 51 | details: ScalaDefaultParameterTransformerDetails, 52 | input: String, 53 | toType: java.lang.reflect.Type, 54 | expectedOutput: AnyRef 55 | ): Unit = { 56 | assertDefaultParameterTransformer( 57 | ScalaDefaultParameterTransformerDefinition(details, true), 58 | input, 59 | toType, 60 | expectedOutput 61 | ) 62 | } 63 | 64 | private def assertObjectDefaultParameterTransformer( 65 | details: ScalaDefaultParameterTransformerDetails, 66 | input: String, 67 | toType: java.lang.reflect.Type, 68 | expectedOutput: AnyRef 69 | ): Unit = { 70 | assertDefaultParameterTransformer( 71 | ScalaDefaultParameterTransformerDefinition(details, false), 72 | input, 73 | toType, 74 | expectedOutput 75 | ) 76 | } 77 | 78 | private def assertDefaultParameterTransformer( 79 | typeDef: DefaultParameterTransformerDefinition, 80 | input: String, 81 | toType: java.lang.reflect.Type, 82 | expectedOutput: AnyRef 83 | ): Unit = { 84 | assertEquals( 85 | toType, 86 | typeDef.parameterByTypeTransformer().transform(input, toType).getClass 87 | ) 88 | assertEquals( 89 | expectedOutput.toString, 90 | typeDef.parameterByTypeTransformer().transform(input, toType).toString 91 | ) 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /docs/scala_implementation.md: -------------------------------------------------------------------------------- 1 | # Scala implementation details 2 | 3 | This page covers some details about the Cucumber Scala implementation. 4 | 5 | ## Running a Cucumber test 6 | 7 | ### Backend 8 | 9 | From Cucumber core perspective, the entrypoint of a Cucumber implementation is what is called "backend". 10 | 11 | The `BackendServiceLoader` core service looks for a `BackendProviderService` implementation. 12 | Ours is defined in the class `ScalaBackendProviderService`. 13 | 14 | The implementing class also has to be registered as a "Java Service" in the `META-INF/services/io.cucumber.core.backend.BackendProviderService` file (in the `resources` folder). 15 | 16 | ### Loading the glue 17 | 18 | When a Cucumber test starts, a Cucumber Runner starts and a `ScalaBackend` instance is created. 19 | The `ScalaBackend` instance will be used for running all the scenarios which are part of the test (defined by the _features path_ and the _glue path_). 20 | 21 | The first thing the Runner does is to "load the glue", that is find all the hooks and step definitions and register them. 22 | This is handled by the `ScalaBackend#loadGlue()` method. 23 | 24 | #### Scala implementation 25 | 26 | In the Cucumber Scala implementation, loading the glue code means: 27 | - finding all the **classes** inheriting `io.cucumber.scala.ScalaDsl` in the _glue path_, and for each: 28 | - add it to the `Container` instance provided by Cucumber Core 29 | - finding all the **objects** singletons instances inheriting `io.cucumber.scala.ScalaDsl` in the _glue path_ and for each: 30 | - extract the hooks and step definitions from it 31 | - add the definitions to the `Glue` instance provided by Cucumber Core, as NOT `ScenarioScoped` 32 | 33 | Ideally all the glue code should be instantiated further (see next section), this is why we register classes (actually a list of `Class`) to the Container. 34 | But this cannot work for objects because they are by definitions singletons and already instantiated way before Cucumber. 35 | Thus, objects are not registered in the Container and their lifecycle is out of Cucumber scope. 36 | 37 | ### Running a scenario 38 | 39 | For each scenario, the `buildWorld()` method of the backend is called. 40 | This is where the glue code should be initialized. 41 | 42 | #### Scala implementation 43 | 44 | For each **class** identified when loading the glue: 45 | - an instance is created by the `Lookup` provided by Cucumber Core 46 | - hooks and steps definitions are extracted from it 47 | - definitions are added to the `Glue` instance provided by Cucumber Core, as `ScenarioScoped` 48 | 49 | Being `ScenarioScoped` ensure instances are flushed at the end of the scenario and recreated for the next one. 50 | 51 | ## Scala DSL 52 | 53 | The Scala DSL is made in a way that any class instance or object extending it contains what we call a **registry**: 54 | a list of the hooks and step definitions it contains. 55 | This is the purpose of `ScalaDslRegistry`. 56 | 57 | The registry is populated when the class instance or the object is created. 58 | Unlike other implementations there is no need to use annotations or reflection here. 59 | This is actually **similar to the Java8/Lambda implementation**. 60 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/datatables/DatatableSteps.scala: -------------------------------------------------------------------------------- 1 | package datatables 2 | 3 | import java.util.{List => JavaList, Map => JavaMap} 4 | 5 | import io.cucumber.datatable.DataTable 6 | import io.cucumber.scala.{EN, ScalaDsl} 7 | 8 | import scala.jdk.CollectionConverters._ 9 | 10 | class DatatableSteps extends ScalaDsl with EN { 11 | 12 | Given("the following table as DataTable") { (table: DataTable) => 13 | val data: Seq[Map[String, String]] = 14 | table.asMaps().asScala.map(_.asScala.toMap).toSeq 15 | val expected = Seq( 16 | Map("key1" -> "val11", "key2" -> "val12", "key3" -> "val13"), 17 | Map("key1" -> "val21", "key2" -> "val22", "key3" -> "val23"), 18 | Map("key1" -> "val31", "key2" -> "val32", "key3" -> "val33") 19 | ) 20 | assert(data == expected) 21 | } 22 | 23 | Given("the following table as List of Map") { 24 | (table: JavaList[JavaMap[String, String]]) => 25 | val data: Seq[Map[String, String]] = 26 | table.asScala.map(_.asScala.toMap).toSeq 27 | val expected = Seq( 28 | Map("key1" -> "val11", "key2" -> "val12", "key3" -> "val13"), 29 | Map("key1" -> "val21", "key2" -> "val22", "key3" -> "val23"), 30 | Map("key1" -> "val31", "key2" -> "val32", "key3" -> "val33") 31 | ) 32 | assert(data == expected) 33 | } 34 | 35 | Given("the following table as List of List") { 36 | (table: JavaList[JavaList[String]]) => 37 | val data: Seq[Seq[String]] = table.asScala.map(_.asScala.toSeq).toSeq 38 | val expected = Seq( 39 | Seq("val11", "val12", "val13"), 40 | Seq("val21", "val22", "val23"), 41 | Seq("val31", "val32", "val33") 42 | ) 43 | assert(data == expected) 44 | } 45 | 46 | Given("the following table as Map of Map") { 47 | (table: JavaMap[String, JavaMap[String, String]]) => 48 | val data: Map[String, Map[String, String]] = table.asScala.map { 49 | case (k, v) => k -> v.asScala.toMap 50 | }.toMap 51 | val expected = Map( 52 | "row1" -> Map("key1" -> "val11", "key2" -> "val12", "key3" -> "val13"), 53 | "row2" -> Map("key1" -> "val21", "key2" -> "val22", "key3" -> "val23"), 54 | "row3" -> Map("key1" -> "val31", "key2" -> "val32", "key3" -> "val33") 55 | ) 56 | assert(data == expected) 57 | } 58 | 59 | Given("the following table as Map of List") { 60 | (table: JavaMap[String, JavaList[String]]) => 61 | val data: Map[String, Seq[String]] = table.asScala.map { case (k, v) => 62 | k -> v.asScala.toSeq 63 | }.toMap 64 | val expected = Map( 65 | "row1" -> Seq("val11", "val12", "val13"), 66 | "row2" -> Seq("val21", "val22", "val23"), 67 | "row3" -> Seq("val31", "val32", "val33") 68 | ) 69 | assert(data == expected) 70 | } 71 | 72 | Given("the following table as Map") { (table: JavaMap[String, String]) => 73 | val data: Map[String, String] = table.asScala.toMap 74 | val expected = Map( 75 | "row1" -> "val11", 76 | "row2" -> "val21", 77 | "row3" -> "val31" 78 | ) 79 | assert(data == expected) 80 | } 81 | 82 | Given("the following table as List") { (table: JavaList[String]) => 83 | val data: Seq[String] = table.asScala.toSeq 84 | val expected = Seq( 85 | "val11", 86 | "val21", 87 | "val31" 88 | ) 89 | assert(data == expected) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /docs/upgrade_v5.md: -------------------------------------------------------------------------------- 1 | # Upgrading from 4.x to 5.x 2 | 3 | If you are using Cucumber Scala 4.7.x and want to upgrade to 5.x, please note there are some major changes in addition to the Cucumber upgrade itself. 4 | 5 | Starting from version 5.x, Cucumber Scala will try to be as close as possible to the Cucumber Java implementation while remaining Scala oriented. 6 | This means that package names, parameters order, internal code... will be more consistent with the Java implementation. 7 | 8 | ## Cucumber Core changes 9 | 10 | Please read the [CHANGELOG](https://github.com/cucumber/cucumber-jvm/blob/main/CHANGELOG.md) from Cucumber Core. 11 | 12 | ### Empty values in Datatables 13 | 14 | Starting with Cucumber 5.x, empty values in Datatables are now represented as `null` instead of an empty string previously. 15 | 16 | Check out the [Empty values section in Transformers documentation](https://github.com/cucumber/cucumber-jvm-scala/blob/main/docs/transformers.md#empty-values) to learn how to have empty values in your Datatables. 17 | 18 | You can also consider [upgrading to Cucumber Scala 6.x](upgrade_v6.md) straight away as Cucumber Scala 6.x provides a way to map Datatables as collections of `Option`s so that you don't have to deal with `null` anyway. 19 | 20 | ## Packages 21 | 22 | All Cucumber Scala classes are now under `io.cucumber.scala` package instead of `cucumber.api.scala`. 23 | 24 | ## Hooks 25 | 26 | The `Before`, `BeforeStep`, `After` and `AfterStep` definitions have slightly changed: 27 | - to apply only to scenarios with some tags, the `String*` parameter is replaced by a single tag expression of type `String`. 28 | This changes comes from Cucumber itself. 29 | - if providing both an _order_ and a _tag expression_, the _order_ is now the second parameter instead of the first. 30 | This is more consistent with the Java implementation. 31 | 32 | For instance, the following code: 33 | 34 | ```scala 35 | Before(1, "@tag1", "@tag2") { _ => 36 | // Do Something 37 | } 38 | ``` 39 | 40 | Is replaced by: 41 | 42 | ```scala 43 | Before("@tag1 or @tag2", 1) { _ => 44 | // Do Something 45 | } 46 | ``` 47 | 48 | ### Other changes 49 | 50 | As a side effect the following usage no longer compiles: 51 | ```scala 52 | Before() { _ => 53 | // Do something 54 | } 55 | ``` 56 | It can be replaced with: 57 | ```scala 58 | Before { _ => 59 | // Do something 60 | } 61 | ``` 62 | 63 | See also the [Hooks documentation](hooks.md). 64 | 65 | ## Transformers 66 | 67 | If you are using transformers defined with `TypeRegistryConfigurer`, please note that this is now deprecated in Cucumber Core. 68 | 69 | The recommended way to define transformers is to define them in glue code. See [Transformers](./transformers.md). 70 | 71 | ## Under the hood 72 | 73 | ### Instantiate glue classes per scenario 74 | 75 | Before Cucumber Scala 5.x, glue classes (classes extending `ScalaDsl`) were instantiated only once for a test suite. 76 | 77 | This means that if you wanted to keep state between steps of your scenarios, you had to make sure the state was not shared to other scenarios by using hooks or manual checks. 78 | 79 | Starting from Cucumber Scala 5.x, **each scenario creates new glue class instances**. 80 | 81 | You should not notice any change unless you rely on state kept between scenarios in your glue classes. 82 | Please note that this is not the proper way to keep a state. 83 | You might want to use an `object` for this purpose. 84 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | Hooks are blocks of code that can run at various points in the Cucumber execution cycle. 4 | They are typically used for setup and teardown of the environment before and after all/each scenario or step. 5 | 6 | See the [reference documentation](https://docs.cucumber.io/docs/cucumber/api/#hooks). 7 | 8 | ## Static hooks 9 | 10 | Static hooks run once before/after all scenarios. 11 | 12 | **Note:** static hooks can only be defined inside `object`s (not classes). 13 | 14 | ### BeforeAll 15 | 16 | `BeforeAll` hooks run once before all scenarios. 17 | 18 | ```scala 19 | BeforeAll { 20 | // Do something before all scenarios 21 | // Must return Unit 22 | } 23 | ``` 24 | 25 | ### AfterAll 26 | 27 | `AfterAll` hooks run once after all scenarios. 28 | 29 | ```scala 30 | AfterAll { 31 | // Do something after each scenario 32 | // Must return Unit 33 | } 34 | ``` 35 | 36 | ## Scenario hooks 37 | 38 | Scenario hooks run for every scenario. 39 | 40 | ### Before 41 | 42 | `Before` hooks run before the first step of each scenario. 43 | 44 | ```scala 45 | Before { scenario : Scenario => 46 | // Do something before each scenario 47 | // Must return Unit 48 | } 49 | 50 | // Or: 51 | Before { 52 | // Do something before each scenario 53 | // Must return Unit 54 | } 55 | ``` 56 | 57 | ### After 58 | 59 | `After` hooks run after the last step of each scenario. 60 | 61 | ```scala 62 | After { scenario : Scenario => 63 | // Do something after each scenario 64 | // Must return Unit 65 | } 66 | 67 | // Or: 68 | After { 69 | // Do something after each scenario 70 | // Must return Unit 71 | } 72 | ``` 73 | 74 | ## Step hooks 75 | 76 | Step hooks invoked before and after a step. 77 | 78 | ### BeforeStep 79 | 80 | ```scala 81 | BeforeStep { scenario : Scenario => 82 | // Do something before step 83 | // Must return Unit 84 | } 85 | 86 | // Or: 87 | BeforeStep { 88 | // Do something before step 89 | // Must return Unit 90 | } 91 | ``` 92 | 93 | ### AfterStep 94 | 95 | ```scala 96 | AfterStep { scenario : Scenario => 97 | // Do something after step 98 | // Must return Unit 99 | } 100 | 101 | // Or: 102 | AfterStep { 103 | // Do something after step 104 | // Must return Unit 105 | } 106 | ``` 107 | 108 | ## Conditional hooks 109 | 110 | Hooks can be conditionally selected for execution based on the tags of the scenario. 111 | 112 | ```scala 113 | Before("@browser and not @headless") { 114 | // Do something before each scenario with tag @browser but not @headless 115 | // Must return Unit 116 | } 117 | ``` 118 | 119 | Note: this cannot be applied to static hooks (`BeforeAll`/`AfterAll`). 120 | 121 | ## Order 122 | 123 | You can define an order between multiple hooks. 124 | 125 | ```scala 126 | Before(10) { 127 | // Do something before each scenario 128 | // Must return Unit 129 | } 130 | 131 | Before(20) { 132 | // Do something before each scenario 133 | // Must return Unit 134 | } 135 | ``` 136 | 137 | The **default order is 1000**. 138 | 139 | ## Conditional and order 140 | 141 | You mix up conditional and order hooks with following syntax: 142 | ```scala 143 | Before("@browser and not @headless", 10) { 144 | // Do something before each scenario 145 | // Must return Unit 146 | } 147 | ``` 148 | 149 | Note: this cannot be applied to static hooks (`BeforeAll`/`AfterAll`). 150 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/DataTableTypeDsl.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import scala.reflect.ClassTag 4 | 5 | private[scala] trait DataTableTypeDsl extends BaseScalaDsl { self => 6 | 7 | /** Register a data table type. 8 | */ 9 | def DataTableType: DataTableTypeBody = DataTableType(NO_REPLACEMENT) 10 | 11 | /** Register a data table type with a replacement.

A data table can only 12 | * represent absent and non-empty strings. By replacing a known value (for 13 | * example [empty]) a data table can also represent empty strings. 14 | * 15 | * @param replaceWithEmptyString 16 | * a string that will be replaced with an empty string. 17 | */ 18 | def DataTableType(replaceWithEmptyString: String): DataTableTypeBody = 19 | DataTableType(Seq(replaceWithEmptyString)) 20 | 21 | private def DataTableType(replaceWithEmptyString: Seq[String]) = 22 | new DataTableTypeBody(replaceWithEmptyString) 23 | 24 | final class DataTableTypeBody(replaceWithEmptyString: Seq[String]) { 25 | 26 | def apply[T]( 27 | body: DataTableEntryDefinitionBody[T] 28 | )(implicit ev: ClassTag[T]): Unit = { 29 | registry.registerDataTableType( 30 | ScalaDataTableEntryTypeDetails[T]( 31 | replaceWithEmptyString, 32 | body, 33 | ev, 34 | Utils.frame(self) 35 | ) 36 | ) 37 | } 38 | 39 | def apply[T]( 40 | body: DataTableOptionalEntryDefinitionBody[T] 41 | )(implicit ev: ClassTag[T]): Unit = { 42 | registry.registerDataTableType( 43 | ScalaDataTableOptionalEntryTypeDetails[T]( 44 | replaceWithEmptyString, 45 | body, 46 | ev, 47 | Utils.frame(self) 48 | ) 49 | ) 50 | } 51 | 52 | def apply[T]( 53 | body: DataTableRowDefinitionBody[T] 54 | )(implicit ev: ClassTag[T]): Unit = { 55 | registry.registerDataTableType( 56 | ScalaDataTableRowTypeDetails[T]( 57 | replaceWithEmptyString, 58 | body, 59 | ev, 60 | Utils.frame(self) 61 | ) 62 | ) 63 | } 64 | 65 | def apply[T]( 66 | body: DataTableOptionalRowDefinitionBody[T] 67 | )(implicit ev: ClassTag[T]): Unit = { 68 | registry.registerDataTableType( 69 | ScalaDataTableOptionalRowTypeDetails[T]( 70 | replaceWithEmptyString, 71 | body, 72 | ev, 73 | Utils.frame(self) 74 | ) 75 | ) 76 | } 77 | 78 | def apply[T]( 79 | body: DataTableCellDefinitionBody[T] 80 | )(implicit ev: ClassTag[T]): Unit = { 81 | registry.registerDataTableType( 82 | ScalaDataTableCellTypeDetails[T]( 83 | replaceWithEmptyString, 84 | body, 85 | ev, 86 | Utils.frame(self) 87 | ) 88 | ) 89 | } 90 | 91 | def apply[T]( 92 | body: DataTableOptionalCellDefinitionBody[T] 93 | )(implicit ev: ClassTag[T]): Unit = { 94 | registry.registerDataTableType( 95 | ScalaDataTableOptionalCellTypeDetails[T]( 96 | replaceWithEmptyString, 97 | body, 98 | ev, 99 | Utils.frame(self) 100 | ) 101 | ) 102 | } 103 | 104 | def apply[T]( 105 | body: DataTableDefinitionBody[T] 106 | )(implicit ev: ClassTag[T]): Unit = { 107 | registry.registerDataTableType( 108 | ScalaDataTableTableTypeDetails[T]( 109 | replaceWithEmptyString, 110 | body, 111 | ev, 112 | Utils.frame(self) 113 | ) 114 | ) 115 | } 116 | 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cucumber Scala 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/io.cucumber/cucumber-scala_2.13.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22io.cucumber%22%20AND%20a:%22cucumber-scala_2.13%22) 4 | ![Build Status](https://github.com/cucumber/cucumber-jvm-scala/workflows/Cucumber%20Scala%20CI/badge.svg) 5 | 6 | Cucumber Scala is the Scala implementation of [Cucumber](https://cucumber.io/). 7 | 8 | ## Help & Support 9 | 10 | See: https://cucumber.io/support 11 | 12 | ## Compatibility matrix 13 | 14 | Cucumber Scala has a different release cycle than other Cucumber projects that you might use 15 | (like _cucumber-junit-platform-engine_). 16 | 17 | As a rule of thumb, you can assume that latest version of Cucumber Scala targets the latest version 18 | of Cucumber Core projects. 19 | 20 | The table below shows the compatible versions: 21 | 22 | | Cucumber Scala version | Cucumber Core version | Scala versions | 23 | |------------------------|-----------------------|------------------------| 24 | | 8.18+ | 7.x | 2.12, 2.13, 3.3+ | 25 | | 8.13-8.17 | 7.x | 2.12, 2.13, 3.2+ | 26 | | 8.0-8.12 | 7.x | 2.12, 2.13, 3.0+ | 27 | | 7.x | 6.x | 2.11, 2.12, 2.13, 3.0+ | 28 | | 6.x | 6.x | 2.11, 2.12, 2.13 | 29 | | 5.x | 5.x | 2.11, 2.12, 2.13 | 30 | | 4.x | 4.x | 2.11, 2.12, 2.13 | 31 | 32 | ## Getting started 33 | 34 | - [Installation](./docs/install.md) 35 | - Upgrade notes 36 | - [Version 8.x](docs/upgrade_v8.md) 37 | - [Version 7.x](docs/upgrade_v7.md) 38 | - [Version 6.x](docs/upgrade_v6.md) 39 | - [Version 5.x](docs/upgrade_v5.md) 40 | - Documentation 41 | - [Basic usage](docs/usage.md) 42 | - [Step Definitions](docs/step_definitions.md) 43 | - [DataTables](docs/datatables.md) 44 | - [Hooks](docs/hooks.md) 45 | - [Transformers](docs/transformers.md) 46 | - [Default Jackson DataTable Transformer](docs/default_jackson_datatable_transformer.md) 47 | - [Example project](examples/examples-junit5/README.md) 48 | - [Reference documentation for Java](https://docs.cucumber.io/docs/cucumber/) 49 | - [Changelog](CHANGELOG.md) 50 | 51 | ## Contributing 52 | 53 | See [here](CONTRIBUTING.md) for internal documentation and information about contributing. 54 | 55 | ## Backers & Sponsors 56 | 57 | Support us with a monthly donation and help us continue our activities. [Become a backer](https://opencollective.com/cucumber#backer) or [a sponsor](https://opencollective.com/cucumber#sponsor)! 58 | 59 | ## They are using it 60 | 61 | You are using Cucumber Scala? We would love to know about you! Please open a PR to add your project or company to the list below. 62 | 63 | |||| 64 | | :---: | :---: | :---: | 65 | | KelkooGroup | Teads | theGardener | 66 | | Lectra | Kapoeira | | 67 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/ScalaBackend.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend._ 4 | import io.cucumber.core.resource.{ClasspathScanner, ClasspathSupport} 5 | import io.cucumber.scala.ScalaBackend.isRegularClass 6 | 7 | import java.lang.reflect.Modifier 8 | import java.net.URI 9 | import java.util.function.Supplier 10 | import java.util.{List => JList} 11 | import scala.jdk.CollectionConverters._ 12 | import scala.util.{Failure, Try} 13 | 14 | object ScalaBackend { 15 | 16 | /** @return 17 | * true if it's a class, false if it's an object 18 | */ 19 | private[scala] def isRegularClass(cls: Class[_]): Try[Boolean] = { 20 | Try { 21 | // Object don't have constructors 22 | cls.getConstructors.headOption 23 | .map(_.getModifiers) 24 | .exists(Modifier.isPublic) 25 | }.recoverWith { case ex: Throwable => 26 | Failure(new UnknownClassType(cls, ex)) 27 | } 28 | } 29 | 30 | } 31 | 32 | class ScalaBackend( 33 | lookup: Lookup, 34 | container: Container, 35 | classLoaderProvider: Supplier[ClassLoader] 36 | ) extends Backend { 37 | 38 | private val classFinder = new ClasspathScanner(classLoaderProvider) 39 | 40 | private var glueAdaptor: GlueAdaptor = _ 41 | private[scala] var scalaGlueClasses: Seq[Class[_ <: ScalaDsl]] = Nil 42 | 43 | override def disposeWorld(): Unit = { 44 | // Nothing to do 45 | } 46 | 47 | override def getSnippet(): Snippet = { 48 | new ScalaSnippet() 49 | } 50 | 51 | override def buildWorld(): Unit = { 52 | // Instantiate all the glue classes and load the glue code from them 53 | scalaGlueClasses.foreach { glueClass => 54 | val glueInstance = Option(lookup.getInstance(glueClass)) 55 | glueInstance match { 56 | case Some(glue) => 57 | glueAdaptor.loadRegistry(glue.registry, scenarioScoped = true) 58 | case None => 59 | throw new CucumberBackendException( 60 | s"Not able to instantiate class ${glueClass.getName}. Please report this issue to cucumber-scala project." 61 | ) 62 | } 63 | } 64 | } 65 | 66 | override def loadGlue(glue: Glue, gluePaths: JList[URI]): Unit = { 67 | 68 | glueAdaptor = new GlueAdaptor(glue) 69 | 70 | val dslClasses = gluePaths.asScala 71 | .filter(gluePath => 72 | ClasspathSupport.CLASSPATH_SCHEME.equals(gluePath.getScheme) 73 | ) 74 | .map(ClasspathSupport.packageName) 75 | .flatMap(basePackageName => 76 | classFinder 77 | .scanForSubClassesInPackage(basePackageName, classOf[ScalaDsl]) 78 | .asScala 79 | ) 80 | .filter(glueClass => !glueClass.isInterface) 81 | .distinct 82 | 83 | // Voluntarily throw exception if not able to identify if it's a class 84 | val (clsClasses, objClasses) = 85 | dslClasses.partition(c => isRegularClass(c).get) 86 | 87 | // Retrieve Scala objects (singletons) 88 | val objInstances = objClasses.map { cls => 89 | val instField = cls.getDeclaredField("MODULE$") 90 | instField.setAccessible(true) 91 | instField.get(null).asInstanceOf[ScalaDsl] 92 | } 93 | 94 | // Regular Scala classes are added to the container, they will be instantiated by the container depending on its logic 95 | // Object are not because by definition they are singletons 96 | clsClasses.foreach { glueClass => 97 | container.addClass(glueClass) 98 | scalaGlueClasses = scalaGlueClasses :+ glueClass 99 | } 100 | 101 | // For object, we add the definitions here, once for all 102 | objInstances.foreach { glueInstance => 103 | glueAdaptor.loadRegistry(glueInstance.registry, scenarioScoped = false) 104 | } 105 | 106 | () 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/DefaultTransformerDsl.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.scala.Aliases.{ 4 | DefaultDataTableCellTransformerBody, 5 | DefaultDataTableEntryTransformerBody, 6 | DefaultParameterTransformerBody 7 | } 8 | 9 | private[scala] trait DefaultTransformerDsl extends BaseScalaDsl { self => 10 | 11 | /** Register default parameter type transformer. 12 | * 13 | * @param body 14 | * converts `String` argument to an instance of the `Type` argument 15 | */ 16 | def DefaultParameterTransformer( 17 | body: DefaultParameterTransformerBody 18 | ): Unit = { 19 | registry.registerDefaultParameterTransformer( 20 | ScalaDefaultParameterTransformerDetails(body, Utils.frame(self)) 21 | ) 22 | } 23 | 24 | /** Register default data table cell transformer. 25 | * 26 | * @param body 27 | * converts `String` argument to an instance of the `Type` argument 28 | */ 29 | def DefaultDataTableCellTransformer( 30 | body: DefaultDataTableCellTransformerBody 31 | ): Unit = { 32 | DefaultDataTableCellTransformer(NO_REPLACEMENT)(body) 33 | } 34 | 35 | /** Register default data table cell transformer with a replacement.

A 36 | * data table can only represent absent and non-empty strings. By replacing a 37 | * known value (for example [empty]) a data table can also represent empty 38 | * strings. * 39 | * 40 | * @param replaceWithEmptyString 41 | * a string that will be replaced with an empty string. 42 | * @param body 43 | * converts `String` argument to an instance of the `Type` argument 44 | */ 45 | def DefaultDataTableCellTransformer( 46 | replaceWithEmptyString: String 47 | )(body: DefaultDataTableCellTransformerBody): Unit = { 48 | DefaultDataTableCellTransformer(Seq(replaceWithEmptyString))(body) 49 | } 50 | 51 | private def DefaultDataTableCellTransformer( 52 | replaceWithEmptyString: Seq[String] 53 | )(body: DefaultDataTableCellTransformerBody): Unit = { 54 | registry.registerDefaultDataTableCellTransformer( 55 | ScalaDefaultDataTableCellTransformerDetails( 56 | replaceWithEmptyString, 57 | body, 58 | Utils.frame(self) 59 | ) 60 | ) 61 | } 62 | 63 | /** Register default data table entry transformer. 64 | * 65 | * @param body 66 | * converts `Map[String,String]` argument to an instance of the `Type` 67 | * argument 68 | */ 69 | def DefaultDataTableEntryTransformer( 70 | body: DefaultDataTableEntryTransformerBody 71 | ): Unit = { 72 | DefaultDataTableEntryTransformer(NO_REPLACEMENT)(body) 73 | } 74 | 75 | /** Register default data table cell transformer with a replacement.

A 76 | * data table can only represent absent and non-empty strings. By replacing a 77 | * known value (for example [empty]) a data table can also represent empty 78 | * strings. 79 | * 80 | * @param replaceWithEmptyString 81 | * a string that will be replaced with an empty string. 82 | * @param body 83 | * converts `Map[String,String]` argument to an instance of the `Type` 84 | * argument 85 | */ 86 | def DefaultDataTableEntryTransformer( 87 | replaceWithEmptyString: String 88 | )(body: DefaultDataTableEntryTransformerBody): Unit = { 89 | DefaultDataTableEntryTransformer(Seq(replaceWithEmptyString))(body) 90 | } 91 | 92 | private def DefaultDataTableEntryTransformer( 93 | replaceWithEmptyString: Seq[String] 94 | )(body: DefaultDataTableEntryTransformerBody): Unit = { 95 | registry.registerDefaultDataTableEntryTransformer( 96 | ScalaDefaultDataTableEntryTransformerDetails( 97 | replaceWithEmptyString, 98 | body, 99 | Utils.frame(self) 100 | ) 101 | ) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/datatables/DatatableAsScala.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to parse DataTables to Scala types properly 2 | 3 | # Scenarios with Strings 4 | 5 | Scenario: As datatable 6 | Given the following table as Scala DataTable 7 | | key1 | key2 | key3 | 8 | | val11 | val12 | val13 | 9 | | val21 | | val23 | 10 | | val31 | val32 | val33 | 11 | 12 | Scenario: As List of Map 13 | Given the following table as Scala List of Map 14 | | key1 | key2 | key3 | 15 | | val11 | val12 | val13 | 16 | | val21 | | val23 | 17 | | val31 | val32 | val33 | 18 | 19 | Scenario: As List of List 20 | Given the following table as Scala List of List 21 | | val11 | val12 | val13 | 22 | | val21 | | val23 | 23 | | val31 | val32 | val33 | 24 | 25 | Scenario: As Map of Map 26 | Given the following table as Scala Map of Map 27 | | | key1 | key2 | key3 | 28 | | row1 | val11 | val12 | val13 | 29 | | row2 | val21 | | val23 | 30 | | row3 | val31 | val32 | val33 | 31 | 32 | Scenario: As Map of List 33 | Given the following table as Scala Map of List 34 | | row1 | val11 | val12 | val13 | 35 | | row2 | val21 | | val23 | 36 | | row3 | val31 | val32 | val33 | 37 | 38 | Scenario: As Map 39 | Given the following table as Scala Map 40 | | row1 | val11 | 41 | | row2 | | 42 | | row3 | val31 | 43 | 44 | Scenario: As List 45 | Given the following table as Scala List 46 | | val11 | 47 | | | 48 | | val31 | 49 | 50 | # Scenarios with other basic types (Int) 51 | 52 | Scenario: As datatable of integers 53 | Given the following table as Scala DataTable of integers 54 | | 1 | 2 | 3 | 55 | | 11 | 12 | 13 | 56 | | 21 | | 23 | 57 | | 31 | 32 | 33 | 58 | 59 | Scenario: As List of Map of integers 60 | Given the following table as Scala List of Map of integers 61 | | 1 | 2 | 3 | 62 | | 11 | 12 | 13 | 63 | | 21 | | 23 | 64 | | 31 | 32 | 33 | 65 | 66 | Scenario: As List of List of integers 67 | Given the following table as Scala List of List of integers 68 | | 11 | 12 | 13 | 69 | | 21 | | 23 | 70 | | 31 | 32 | 33 | 71 | 72 | Scenario: As Map of Map of integers (partial) 73 | Given the following table as Scala Map of Map of integers 74 | | | key1 | key2 | key3 | 75 | | 10 | val11 | val12 | val13 | 76 | | 20 | val21 | | val23 | 77 | | 30 | val31 | val32 | val33 | 78 | 79 | Scenario: As Map of List of integers (partial) 80 | Given the following table as Scala Map of List of integers 81 | | 10 | val11 | val12 | val13 | 82 | | 20 | val21 | | val23 | 83 | | 30 | val31 | val32 | val33 | 84 | 85 | Scenario: As Map of integers 86 | Given the following table as Scala Map of integers 87 | | 10 | 11 | 88 | | 20 | | 89 | | 30 | 31 | 90 | 91 | Scenario: As List of integers 92 | Given the following table as Scala List of integers 93 | | 11 | 94 | | | 95 | | 31 | 96 | 97 | # With custom types using DatatableType 98 | 99 | Scenario: As List of custom type 100 | Given the following table as Scala List of custom type 101 | | key1 | key2 | key3 | 102 | | val11 | val12 | val13 | 103 | | val21 | | val23 | 104 | | val31 | val32 | val33 | 105 | 106 | Scenario: As List of List of custom type 107 | Given the following table as Scala List of List of custom type 108 | | val11 | val12 | val13 | 109 | | val21 | | val23 | 110 | | val31 | val32 | val33 | 111 | 112 | Scenario: As List of Map of custom type 113 | Given the following table as Scala List of Map of custom type 114 | | key1 | key2 | key3 | 115 | | val11 | val12 | val13 | 116 | | val21 | | val23 | 117 | | val31 | val32 | val33 | 118 | 119 | -------------------------------------------------------------------------------- /cucumber-scala/src/main/scala/io/cucumber/scala/Scenario.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import java.net.URI 4 | import java.util 5 | 6 | import io.cucumber.core.backend.{Status, TestCaseState} 7 | 8 | /** Before or After Hooks that declare a parameter of this type will receive an 9 | * instance of this class. It allows writing text and embedding media into 10 | * reports, as well as inspecting results (in an After block).

Note: This 11 | * class is not intended to be used to create reports. To create custom reports 12 | * use the `io.cucumber.plugin.Plugin` class. The plugin system provides a much 13 | * richer access to Cucumbers then hooks after could provide. For an example 14 | * see `io.cucumber.core.plugin.PrettyFormatter`. 15 | */ 16 | class Scenario(val delegate: TestCaseState) { 17 | 18 | /** @return 19 | * tags of this scenario. 20 | */ 21 | def getSourceTagNames: util.Collection[String] = delegate.getSourceTagNames 22 | 23 | /** Returns the current status of this test case.

The test case status is 24 | * calculate as the most severe status of the executed steps in the testcase 25 | * so far. 26 | * 27 | * @return 28 | * the current status of this test case 29 | */ 30 | def getStatus: Status = Status.valueOf(delegate.getStatus.name) 31 | 32 | /** @return 33 | * true if and only if `getStatus` returns "failed" 34 | */ 35 | def isFailed: Boolean = delegate.isFailed 36 | 37 | /** Attach data to the report(s).

 {@code // Attach a screenshot. See
 38 |     * your UI automation tool's docs for // details about how to take a
 39 |     * screenshot. scenario.attach(pngBytes, "image/png", "Bartholomew and the
 40 |     * Bytes of the Oobleck"); } 

To ensure reporting tools can 41 | * understand what the data is a {@code mediaType} must be provided. For 42 | * example: {@code text/plain} , {@code image/png} , 43 | * {@code text/html;charset=utf-8} .

Media types are defined in RFC 7231 Section 45 | * 3.1.1.1. 46 | * 47 | * @param data 48 | * what to attach, for example an image. 49 | * @param mediaType 50 | * what is the data? 51 | * @param name 52 | * attachment name 53 | */ 54 | def attach(data: Array[Byte], mediaType: String, name: String): Unit = { 55 | delegate.attach(data, mediaType, name) 56 | } 57 | 58 | /** Attaches some text based data to the report. 59 | * 60 | * @param data 61 | * what to attach, for example html. 62 | * @param mediaType 63 | * what is the data? 64 | * @param name 65 | * attachment name 66 | * @see 67 | * #attach(byte[], String, String) 68 | */ 69 | def attach(data: String, mediaType: String, name: String): Unit = { 70 | delegate.attach(data, mediaType, name) 71 | } 72 | 73 | /** Outputs some text into the report. 74 | * 75 | * @param text 76 | * what to put in the report. 77 | */ 78 | def log(text: String): Unit = { 79 | delegate.log(text) 80 | } 81 | 82 | /** @return 83 | * the name of the Scenario 84 | */ 85 | def getName: String = delegate.getName 86 | 87 | /** Returns the unique identifier for this scenario.

If this is a Scenario 88 | * from Scenario Outlines this will return the id of the example row in the 89 | * Scenario Outline.

The id is not stable across multiple executions of 90 | * Cucumber but does correlate with ids used in messages output. Use the uri 91 | * + line number to obtain a somewhat stable identifier of a scenario. 92 | * 93 | * @return 94 | * the id of the Scenario. 95 | */ 96 | def getId: String = delegate.getId 97 | 98 | /** @return 99 | * the uri of the Scenario. 100 | */ 101 | def getUri: URI = delegate.getUri 102 | 103 | /** Returns the line in the feature file of the Scenario.

If this is a 104 | * Scenario from Scenario Outlines this will return the line of the example 105 | * row in the Scenario Outline. 106 | * 107 | * @return 108 | * the line in the feature file of the Scenario 109 | */ 110 | def getLine: Integer = delegate.getLine 111 | 112 | } 113 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/ScalaDslParameterTypeTest.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend._ 4 | import io.cucumber.scala.ScalaDslParameterTypeTest.Point 5 | import org.junit.jupiter.api.Assertions.assertEquals 6 | import org.junit.jupiter.api.Test 7 | 8 | import scala.jdk.CollectionConverters._ 9 | 10 | object ScalaDslParameterTypeTest { 11 | 12 | private case class Point(x: Int, y: Int) 13 | 14 | } 15 | 16 | class ScalaDslParameterTypeTest { 17 | 18 | @Test 19 | def testClassParameterType1(): Unit = { 20 | 21 | class Glue extends ScalaDsl with EN { 22 | ParameterType("string-builder", ".*") { str => 23 | new StringBuilder(str) 24 | } 25 | } 26 | 27 | val glue = new Glue() 28 | 29 | assertClassParameterType( 30 | glue.registry.parameterTypes.head, 31 | "string-builder", 32 | Seq(".*"), 33 | classOf[StringBuilder] 34 | ) 35 | } 36 | 37 | @Test 38 | def testClassParameterType2(): Unit = { 39 | 40 | class Glue extends ScalaDsl with EN { 41 | ParameterType("coordinates", "(.+),(.+)") { (x, y) => 42 | Point(x.toInt, y.toInt) 43 | } 44 | } 45 | 46 | val glue = new Glue() 47 | 48 | assertClassParameterType( 49 | glue.registry.parameterTypes.head, 50 | "coordinates", 51 | Seq("(.+),(.+)"), 52 | classOf[Point] 53 | ) 54 | } 55 | 56 | @Test 57 | def testClassParameterType3(): Unit = { 58 | 59 | class Glue extends ScalaDsl with EN { 60 | ParameterType("ingredients", "(.+), (.+) and (.+)") { (x, y, z) => 61 | s"$x-$y-$z" 62 | } 63 | } 64 | 65 | val glue = new Glue() 66 | 67 | assertClassParameterType( 68 | glue.registry.parameterTypes.head, 69 | "ingredients", 70 | Seq("(.+), (.+) and (.+)"), 71 | classOf[String] 72 | ) 73 | } 74 | 75 | // -------------------- Test on object -------------------- 76 | // Note: for now there is no difference between the two in ScalaDsl but better safe than sorry 77 | 78 | @Test 79 | def testObjectParameterType1(): Unit = { 80 | 81 | object Glue extends ScalaDsl with EN { 82 | ParameterType("string-builder", ".*") { str => 83 | new StringBuilder(str) 84 | } 85 | } 86 | 87 | assertObjectParameterType( 88 | Glue.registry.parameterTypes.head, 89 | "string-builder", 90 | Seq(".*"), 91 | classOf[StringBuilder] 92 | ) 93 | } 94 | 95 | @Test 96 | def testObjectParameterType2(): Unit = { 97 | 98 | object Glue extends ScalaDsl with EN { 99 | ParameterType("coordinates", "(.+),(.+)") { (x, y) => 100 | Point(x.toInt, y.toInt) 101 | } 102 | } 103 | 104 | assertObjectParameterType( 105 | Glue.registry.parameterTypes.head, 106 | "coordinates", 107 | Seq("(.+),(.+)"), 108 | classOf[Point] 109 | ) 110 | } 111 | 112 | @Test 113 | def testObjectParameterType3(): Unit = { 114 | 115 | object Glue extends ScalaDsl with EN { 116 | ParameterType("ingredients", "(.+), (.+) and (.+)") { (x, y, z) => 117 | s"$x-$y-$z" 118 | } 119 | } 120 | 121 | assertObjectParameterType( 122 | Glue.registry.parameterTypes.head, 123 | "ingredients", 124 | Seq("(.+), (.+) and (.+)"), 125 | classOf[String] 126 | ) 127 | } 128 | 129 | private def assertClassParameterType( 130 | details: ScalaParameterTypeDetails[_], 131 | name: String, 132 | regexps: Seq[String], 133 | expectedType: Class[_] 134 | ): Unit = { 135 | assertParameterType( 136 | ScalaParameterTypeDefinition(details, true), 137 | name, 138 | regexps, 139 | expectedType 140 | ) 141 | } 142 | 143 | private def assertObjectParameterType( 144 | details: ScalaParameterTypeDetails[_], 145 | name: String, 146 | regexps: Seq[String], 147 | expectedType: Class[_] 148 | ): Unit = { 149 | assertParameterType( 150 | ScalaParameterTypeDefinition(details, false), 151 | name, 152 | regexps, 153 | expectedType 154 | ) 155 | } 156 | 157 | private def assertParameterType( 158 | parameterTypeDef: ParameterTypeDefinition, 159 | name: String, 160 | regexps: Seq[String], 161 | expectedType: Class[_] 162 | ): Unit = { 163 | val parameterType = parameterTypeDef.parameterType() 164 | 165 | assertEquals(name, parameterType.getName) 166 | assertEquals(regexps, parameterType.getRegexps.asScala) 167 | assertEquals(expectedType, parameterType.getType) 168 | 169 | // Cannot assert more because transform method is private 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/parametertypes/ParameterTypesSteps.scala: -------------------------------------------------------------------------------- 1 | package parametertypes 2 | 3 | import java.util.{List => JavaList} 4 | 5 | import io.cucumber.datatable.DataTable 6 | import io.cucumber.scala.{EN, ScalaDsl} 7 | 8 | import scala.jdk.CollectionConverters._ 9 | 10 | case class Point(x: Int, y: Int) 11 | 12 | class ParameterTypesSteps extends ScalaDsl with EN { 13 | 14 | ParameterType("string-builder", "\"(.*)\"") { (str) => 15 | new StringBuilder(str) 16 | } 17 | 18 | ParameterType("coordinates", "(.+),(.+)") { (x, y) => 19 | Point(x.toInt, y.toInt) 20 | } 21 | 22 | ParameterType("ingredients", "(.+), (.+) and (.+)") { (x, y, z) => 23 | s"-$x-$y-$z-" 24 | } 25 | 26 | ParameterType("optionalint", """\s?(\d*)\s?""") { (str) => 27 | Option(str).filter(_.nonEmpty).map(_.toInt) 28 | } 29 | 30 | ParameterType("optionalstring", "(.*)") { (str) => 31 | Option(str).filter(_.nonEmpty) 32 | } 33 | 34 | DefaultParameterTransformer { (fromValue, toValueType) => 35 | new StringBuilder().append(fromValue).append('-').append(toValueType) 36 | } 37 | 38 | DefaultDataTableCellTransformer("[empty]") { 39 | (fromValue: String, toValueType) => 40 | new StringBuilder().append(fromValue).append("-").append(toValueType) 41 | } 42 | 43 | DefaultDataTableEntryTransformer("[empty]") { 44 | (fromValue: Map[String, String], toValueType) => 45 | new StringBuilder().append(fromValue).append("-").append(toValueType) 46 | } 47 | 48 | Given("{string-builder} parameter, defined by lambda") { 49 | (builder: StringBuilder) => 50 | assert(builder.toString() == "string builder") 51 | } 52 | 53 | Given("balloon coordinates {coordinates}, defined by lambda") { 54 | (coordinates: Point) => 55 | assert(coordinates == Point(123, 456)) 56 | } 57 | 58 | Given("kebab made from {ingredients}, defined by lambda") { 59 | (ingredients: String) => 60 | assert(ingredients == "-mushroom-meat-veg-") 61 | } 62 | 63 | Given("kebab made from anonymous {}, defined by lambda") { 64 | (ingredients: StringBuilder) => 65 | assert( 66 | ingredients 67 | .toString() == "meat-class scala.collection.mutable.StringBuilder" 68 | ) 69 | } 70 | 71 | Given("default data table cells, defined by lambda") { 72 | (dataTable: DataTable) => 73 | val table = dataTable 74 | .asLists[StringBuilder](classOf[StringBuilder]) 75 | .asScala 76 | .map(_.asScala) 77 | assert( 78 | table(0)(0) 79 | .toString() == "Kebab-class scala.collection.mutable.StringBuilder" 80 | ) 81 | assert( 82 | table(1)(0) 83 | .toString() == "-class scala.collection.mutable.StringBuilder" 84 | ) 85 | } 86 | 87 | Given("default data table cells, defined by lambda, as rows") { 88 | (cells: JavaList[JavaList[StringBuilder]]) => 89 | val table = cells.asScala.map(_.asScala) 90 | assert( 91 | table(0)(0) 92 | .toString() == "Kebab-class scala.collection.mutable.StringBuilder" 93 | ) 94 | assert( 95 | table(1)(0) 96 | .toString() == "-class scala.collection.mutable.StringBuilder" 97 | ) 98 | } 99 | 100 | Given("default data table entries, defined by lambda") { 101 | (dataTable: DataTable) => 102 | val table = 103 | dataTable.asList[StringBuilder](classOf[StringBuilder]).asScala 104 | assert( 105 | table(0).toString() == "Map(dinner -> Kebab)-class scala.collection.mutable.StringBuilder" 106 | ) 107 | assert( 108 | table(1).toString() == "Map(dinner -> )-class scala.collection.mutable.StringBuilder" 109 | ) 110 | } 111 | 112 | Given("default data table entries, defined by lambda, as rows") { 113 | (rows: JavaList[StringBuilder]) => 114 | val table = rows.asScala 115 | assert( 116 | table(0).toString() == "Map(dinner -> Kebab)-class scala.collection.mutable.StringBuilder" 117 | ) 118 | assert( 119 | table(1).toString() == "Map(dinner -> )-class scala.collection.mutable.StringBuilder" 120 | ) 121 | } 122 | 123 | Given("""an optional string parameter value "{optionalstring}" undefined""") { 124 | (value: Option[String]) => 125 | assert(value.isEmpty) 126 | } 127 | 128 | Given("""an optional string parameter value "{optionalstring}" defined""") { 129 | (value: Option[String]) => 130 | assert(value.contains("toto")) 131 | } 132 | 133 | Given("""an optional int parameter value{optionalint}undefined""") { 134 | (value: Option[Int]) => 135 | assert(value.isEmpty) 136 | } 137 | 138 | Given("""an optional int parameter value{optionalint}defined""") { 139 | (value: Option[Int]) => 140 | assert(value.contains(5)) 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/ScalaDslDefaultDataTableEntryTransformerTest.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend._ 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Test 6 | 7 | import scala.jdk.CollectionConverters._ 8 | 9 | class ScalaDslDefaultDataTableEntryTransformerTest { 10 | 11 | @Test 12 | def testClassDefaultDataTableEntryTransformer(): Unit = { 13 | 14 | class Glue extends ScalaDsl with EN { 15 | DefaultDataTableEntryTransformer { 16 | (fromValue: Map[String, String], toValueType: java.lang.reflect.Type) => 17 | new StringBuilder().append(fromValue).append("-").append(toValueType) 18 | } 19 | } 20 | 21 | val glue = new Glue() 22 | 23 | assertClassDefaultDataTableEntryTransformer( 24 | glue.registry.defaultDataTableEntryTransformers.head, 25 | Map("a" -> "b", "c" -> "d"), 26 | classOf[StringBuilder], 27 | "Map(a -> b, c -> d)-class scala.collection.mutable.StringBuilder" 28 | ) 29 | } 30 | 31 | @Test 32 | def testClassDefaultDataTableEntryTransformerWithEmpty(): Unit = { 33 | 34 | class Glue extends ScalaDsl with EN { 35 | DefaultDataTableEntryTransformer("[empty]") { 36 | (fromValue: Map[String, String], toValueType: java.lang.reflect.Type) => 37 | new StringBuilder().append(fromValue).append("-").append(toValueType) 38 | } 39 | } 40 | 41 | val glue = new Glue() 42 | 43 | assertClassDefaultDataTableEntryTransformer( 44 | glue.registry.defaultDataTableEntryTransformers.head, 45 | Map("a" -> "b", "c" -> "[empty]"), 46 | classOf[StringBuilder], 47 | "Map(a -> b, c -> )-class scala.collection.mutable.StringBuilder" 48 | ) 49 | } 50 | 51 | // -------------------- Test on object -------------------- 52 | // Note: for now there is no difference between the two in ScalaDsl but better safe than sorry 53 | 54 | @Test 55 | def testObjectDefaultDataTableEntryTransformer(): Unit = { 56 | 57 | object Glue extends ScalaDsl with EN { 58 | DefaultDataTableEntryTransformer { 59 | (fromValue: Map[String, String], toValueType: java.lang.reflect.Type) => 60 | new StringBuilder().append(fromValue).append("-").append(toValueType) 61 | } 62 | } 63 | 64 | assertObjectDefaultDataTableEntryTransformer( 65 | Glue.registry.defaultDataTableEntryTransformers.head, 66 | Map("a" -> "b", "c" -> "d"), 67 | classOf[StringBuilder], 68 | "Map(a -> b, c -> d)-class scala.collection.mutable.StringBuilder" 69 | ) 70 | } 71 | 72 | @Test 73 | def testObjectDefaultDataTableEntryTransformerWithEmpty(): Unit = { 74 | 75 | object Glue extends ScalaDsl with EN { 76 | DefaultDataTableEntryTransformer("[empty]") { 77 | (fromValue: Map[String, String], toValueType: java.lang.reflect.Type) => 78 | new StringBuilder().append(fromValue).append("-").append(toValueType) 79 | } 80 | } 81 | 82 | assertObjectDefaultDataTableEntryTransformer( 83 | Glue.registry.defaultDataTableEntryTransformers.head, 84 | Map("a" -> "b", "c" -> "[empty]"), 85 | classOf[StringBuilder], 86 | "Map(a -> b, c -> )-class scala.collection.mutable.StringBuilder" 87 | ) 88 | } 89 | 90 | private def assertClassDefaultDataTableEntryTransformer( 91 | details: ScalaDefaultDataTableEntryTransformerDetails, 92 | input: Map[String, String], 93 | toType: java.lang.reflect.Type, 94 | expectedOutput: AnyRef 95 | ): Unit = { 96 | assertDefaultDataTableEntryTransformer( 97 | ScalaDefaultDataTableEntryTransformerDefinition(details, true), 98 | input, 99 | toType, 100 | expectedOutput 101 | ) 102 | } 103 | 104 | private def assertObjectDefaultDataTableEntryTransformer( 105 | details: ScalaDefaultDataTableEntryTransformerDetails, 106 | input: Map[String, String], 107 | toType: java.lang.reflect.Type, 108 | expectedOutput: AnyRef 109 | ): Unit = { 110 | assertDefaultDataTableEntryTransformer( 111 | ScalaDefaultDataTableEntryTransformerDefinition(details, false), 112 | input, 113 | toType, 114 | expectedOutput 115 | ) 116 | } 117 | 118 | private def assertDefaultDataTableEntryTransformer( 119 | typeDef: DefaultDataTableEntryTransformerDefinition, 120 | input: Map[String, String], 121 | toType: java.lang.reflect.Type, 122 | expectedOutput: AnyRef 123 | ): Unit = { 124 | assertEquals( 125 | toType, 126 | typeDef 127 | .tableEntryByTypeTransformer() 128 | .transform(input.asJava, toType, null) 129 | .getClass 130 | ) 131 | assertEquals( 132 | expectedOutput.toString, 133 | typeDef 134 | .tableEntryByTypeTransformer() 135 | .transform(input.asJava, toType, null) 136 | .toString 137 | ) 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /cucumber-scala/src/test/scala/io/cucumber/scala/ScalaDslDefaultDataTableCellTransformerTest.scala: -------------------------------------------------------------------------------- 1 | package io.cucumber.scala 2 | 3 | import io.cucumber.core.backend._ 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Test 6 | 7 | class ScalaDslDefaultDataTableCellTransformerTest { 8 | 9 | @Test 10 | def testClassDefaultDataTableCellTransformer(): Unit = { 11 | 12 | class Glue extends ScalaDsl with EN { 13 | DefaultDataTableCellTransformer { 14 | (fromValue: String, toValueType: java.lang.reflect.Type) => 15 | new StringBuilder().append(fromValue).append("-").append(toValueType) 16 | } 17 | } 18 | 19 | val glue = new Glue() 20 | 21 | assertClassDefaultDataTableCellTransformer( 22 | glue.registry.defaultDataTableCellTransformers.head, 23 | "meat", 24 | classOf[StringBuilder], 25 | "meat-class scala.collection.mutable.StringBuilder" 26 | ) 27 | } 28 | 29 | @Test 30 | def testClassDefaultDataTableCellTransformerWithEmpty(): Unit = { 31 | 32 | class Glue extends ScalaDsl with EN { 33 | DefaultDataTableCellTransformer("[empty]") { 34 | (fromValue: String, toValueType: java.lang.reflect.Type) => 35 | new StringBuilder().append(fromValue).append("-").append(toValueType) 36 | } 37 | } 38 | 39 | val glue = new Glue() 40 | 41 | assertClassDefaultDataTableCellTransformer( 42 | glue.registry.defaultDataTableCellTransformers.head, 43 | "meat", 44 | classOf[StringBuilder], 45 | "meat-class scala.collection.mutable.StringBuilder" 46 | ) 47 | assertClassDefaultDataTableCellTransformer( 48 | glue.registry.defaultDataTableCellTransformers.head, 49 | "[empty]", 50 | classOf[StringBuilder], 51 | "-class scala.collection.mutable.StringBuilder" 52 | ) 53 | } 54 | 55 | // -------------------- Test on object -------------------- 56 | // Note: for now there is no difference between the two in ScalaDsl but better safe than sorry 57 | 58 | @Test 59 | def testObjectDefaultDataTableCellTransformer(): Unit = { 60 | 61 | object Glue extends ScalaDsl with EN { 62 | DefaultDataTableCellTransformer { 63 | (fromValue: String, toValueType: java.lang.reflect.Type) => 64 | new StringBuilder().append(fromValue).append("-").append(toValueType) 65 | } 66 | } 67 | 68 | assertObjectDefaultDataTableCellTransformer( 69 | Glue.registry.defaultDataTableCellTransformers.head, 70 | "meat", 71 | classOf[StringBuilder], 72 | "meat-class scala.collection.mutable.StringBuilder" 73 | ) 74 | } 75 | 76 | @Test 77 | def testObjectDefaultDataTableCellTransformerWithEmpty(): Unit = { 78 | 79 | object Glue extends ScalaDsl with EN { 80 | DefaultDataTableCellTransformer("[empty]") { 81 | (fromValue: String, toValueType: java.lang.reflect.Type) => 82 | new StringBuilder().append(fromValue).append("-").append(toValueType) 83 | } 84 | } 85 | 86 | assertObjectDefaultDataTableCellTransformer( 87 | Glue.registry.defaultDataTableCellTransformers.head, 88 | "meat", 89 | classOf[StringBuilder], 90 | "meat-class scala.collection.mutable.StringBuilder" 91 | ) 92 | assertObjectDefaultDataTableCellTransformer( 93 | Glue.registry.defaultDataTableCellTransformers.head, 94 | "[empty]", 95 | classOf[StringBuilder], 96 | "-class scala.collection.mutable.StringBuilder" 97 | ) 98 | } 99 | 100 | private def assertClassDefaultDataTableCellTransformer( 101 | details: ScalaDefaultDataTableCellTransformerDetails, 102 | input: String, 103 | toType: java.lang.reflect.Type, 104 | expectedOutput: AnyRef 105 | ): Unit = { 106 | assertDefaultDataTableCellTransformer( 107 | ScalaDefaultDataTableCellTransformerDefinition(details, true), 108 | input, 109 | toType, 110 | expectedOutput 111 | ) 112 | } 113 | 114 | private def assertObjectDefaultDataTableCellTransformer( 115 | details: ScalaDefaultDataTableCellTransformerDetails, 116 | input: String, 117 | toType: java.lang.reflect.Type, 118 | expectedOutput: AnyRef 119 | ): Unit = { 120 | assertDefaultDataTableCellTransformer( 121 | ScalaDefaultDataTableCellTransformerDefinition(details, false), 122 | input, 123 | toType, 124 | expectedOutput 125 | ) 126 | } 127 | 128 | private def assertDefaultDataTableCellTransformer( 129 | typeDef: DefaultDataTableCellTransformerDefinition, 130 | input: String, 131 | toType: java.lang.reflect.Type, 132 | expectedOutput: AnyRef 133 | ): Unit = { 134 | assertEquals( 135 | toType, 136 | typeDef.tableCellByTypeTransformer().transform(input, toType).getClass 137 | ) 138 | assertEquals( 139 | expectedOutput.toString, 140 | typeDef.tableCellByTypeTransformer().transform(input, toType).toString 141 | ) 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Basic Usage 2 | 3 | ## Glue code 4 | 5 | To use Cucumber Scala, all your glue code (steps or hooks) has to be defined in **classes** extending both the `ScalaDsl` trait and a language trait. 6 | 7 | For instance, to use the English flavour: 8 | ```scala 9 | import io.cucumber.scala.{EN, ScalaDsl} 10 | 11 | class MyGlueClass extends ScalaDsl with EN { 12 | 13 | // Here some steps or hooks definitions 14 | 15 | Given("""I have {int} cucumbers in my belly"""){ (cucumberCount: Int) => 16 | // Do something 17 | } 18 | 19 | } 20 | ``` 21 | 22 | Cucumber will automatically load all the glue code defined in classes available in the "glue path" (more details in the Run documentation) inheriting `ScalaDsl`. 23 | 24 | ### Using traits 25 | 26 | You can define glue code in **traits** as well and have a **class** extending the traits you need. 27 | 28 | For instance, you can do things like this: 29 | ```scala 30 | import io.cucumber.scala.{EN, ScalaDsl} 31 | 32 | trait StepsForThis extends ScalaDsl with EN { 33 | // Glue code 34 | } 35 | 36 | trait StepsForThat extends ScalaDsl with EN { 37 | // Glue code 38 | } 39 | 40 | class MyStepDefinitions extends StepsForThis with StepsForThat { 41 | } 42 | ``` 43 | 44 | **Note:** using traits can help you split your tests in different groups and provide some steps only to some tests. 45 | 46 | ### Using objects 47 | 48 | You can also define glue code in **objects**. 49 | 50 | Be aware that by definition objects are singleton and if your glue code is stateful you will probably have "state conflicts" 51 | between your scenarios if you use shared variables from objects. 52 | 53 | ### Using dependency-injection 54 | 55 | Starting with cucumber-scala 8.4, it is possible to use DI modules in order to share state between steps. 56 | 57 | You can for instance have the following definition: 58 | ```scala 59 | import io.cucumber.scala.{EN, ScalaDsl} 60 | 61 | class A extends ScalaDsl with EN { 62 | 63 | var input: String = _ 64 | 65 | Given("""a step defined in class A with arg {string}""") { (arg: String) => 66 | input = arg 67 | } 68 | 69 | } 70 | 71 | class B(a: A) extends ScalaDsl with EN { 72 | 73 | When("""a step defined in class B uses A""") { () => 74 | // Do something with a.input 75 | println(a.input) 76 | } 77 | 78 | } 79 | ``` 80 | 81 | To make it work, you only need a Cucumber DI module to be added as a dependency of your project 82 | (like `cucumber-picocontainer`, or `cucumber-guice`, or any other provided by Cucumber). 83 | 84 | ## Running Cucumber tests 85 | 86 | See also the Running Cucumber for Java [documentation](https://docs.cucumber.io/docs/cucumber/api/#running-cucumber). 87 | 88 | ### JUnit 5 89 | 90 | Add the `cucumber-junit-platform-engine` dependency to your project. 91 | 92 | Then create a runner class like this: 93 | ```scala 94 | import io.cucumber.junit.platform.engine.Constants 95 | import org.junit.platform.suite.api._ 96 | 97 | @Suite 98 | @IncludeEngines(Array("cucumber")) // Mandatory to load Cucumber engine 99 | @SelectPackages(Array("cucumber.examples.scalacalculator")) // Load all *.feature files in the given package 100 | // @SelectClasspathResource("cucumber/examples/scalacalculator/basic_arithmetic.feature") // Or, load a single feature file 101 | @ConfigurationParameter( // Set packages in which to look for glue code (classes containing steps definition) 102 | key = Constants.GLUE_PROPERTY_NAME, 103 | value = "cucumber.examples.scalacalculator" 104 | ) 105 | @ConfigurationParameter(key = Constants.PLUGIN_PROPERTY_NAME, value = "pretty") 106 | class RunCucumberTest 107 | ``` 108 | 109 | Some properties can also be applied for all suites by adding them in the `junit-platform.properties` file (must be in the test resources classpath, 110 | usually `src/test/resources`). For instance: 111 | ```properties 112 | cucumber.plugin=pretty 113 | ``` 114 | 115 | #### Additional settings if using SBT 116 | 117 | You need to enable the support of JUnit 5 in SBT: 118 | ```scala 119 | // plugins.sbt 120 | // version 0.15.1+ is important, earlier versions have a bug preventing properly running Cucumber tests (https://github.com/sbt/sbt-jupiter-interface/issues/142) 121 | addSbtPlugin("com.github.sbt.junit" % "sbt-jupiter-interface" % "0.15.1") 122 | // build.sbt 123 | libraryDependencies += "com.github.sbt.junit" % "jupiter-interface" % JupiterKeys.jupiterVersion.value % Test 124 | ``` 125 | 126 | ### JUnit 4 127 | 128 | Add the `cucumber-junit` dependency to your project. 129 | 130 | Then create a runner class like this: 131 | ```scala 132 | import io.cucumber.junit.{Cucumber, CucumberOptions} 133 | import org.junit.runner.RunWith 134 | 135 | @RunWith(classOf[Cucumber]) 136 | @CucumberOptions() 137 | class RunCucumberTest 138 | ``` 139 | 140 | You can define several options via the `@CucumberOptions` parameters like: 141 | - the "glue path" (default to current package): packages in which to look for glue code 142 | - the "features path" (default to current folder): folder in which to look for features file 143 | - some plugins 144 | -------------------------------------------------------------------------------- /project/ScalacOptions.scala: -------------------------------------------------------------------------------- 1 | object ScalacOptions { 2 | 3 | val scalacOptions3 = Seq( 4 | "-encoding", 5 | "utf-8", // Specify character encoding used by source files. 6 | "-deprecation", // Emit warning and location for usages of deprecated APIs. 7 | "-explain", // Explain type errors in more detail. 8 | // "-explaintypes", // Explain type errors in more detail. 9 | "-feature", // Emit warning and location for usages of features that should be imported explicitly. 10 | "-language:existentials", // Existential types (besides wildcard types) can be written and inferred 11 | "-language:experimental.macros", // Allow macro definition (besides implementation and application) 12 | "-language:higherKinds", // Allow higher-kinded types 13 | "-language:implicitConversions", // Allow definition of implicit functions called views 14 | "-unchecked", // Enable additional warnings where generated code depends on assumptions. 15 | "-Wvalue-discard", // Warn when non-Unit expression results are unused. 16 | "-Wunused:implicits", // Warn if an implicit parameter is unused. 17 | "-Wunused:explicits", 18 | "-Wunused:imports", // Warn if an import selector is not referenced. 19 | "-Wunused:locals", // Warn if a local definition is unused. 20 | "-Wunused:params", // Warn if a value parameter is unused. 21 | "-Wunused:privates", // Warn if a private member is unused. 22 | "-Ykind-projector", 23 | "-Ysafe-init", 24 | "-Xfatal-warnings" // Fail the compilation if there are any warnings. 25 | ) 26 | 27 | // Source: https://nathankleyn.com/2019/05/13/recommended-scalac-flags-for-2-13/ 28 | val scalacOptions213 = Seq( 29 | "-deprecation", // Emit warning and location for usages of deprecated APIs. 30 | "-explaintypes", // Explain type errors in more detail. 31 | "-feature", // Emit warning and location for usages of features that should be imported explicitly. 32 | "-language:existentials", // Existential types (besides wildcard types) can be written and inferred 33 | "-language:experimental.macros", // Allow macro definition (besides implementation and application) 34 | "-language:higherKinds", // Allow higher-kinded types 35 | "-language:implicitConversions", // Allow definition of implicit functions called views 36 | "-unchecked", // Enable additional warnings where generated code depends on assumptions. 37 | "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. 38 | "-Xfatal-warnings", // Fail the compilation if there are any warnings. 39 | "-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver. 40 | "-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error. 41 | "-Xlint:delayedinit-select", // Selecting member of DelayedInit. 42 | "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element. 43 | "-Xlint:inaccessible", // Warn about inaccessible types in method signatures. 44 | "-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`. 45 | "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. 46 | // "-Xlint:nullary-override", // Warn when non-nullary `def f()' overrides nullary `def f'. 47 | "-Xlint:nullary-unit", // Warn when nullary methods return Unit. 48 | "-Xlint:option-implicit", // Option.apply used implicit view. 49 | "-Xlint:package-object-classes", // Class or object defined in package object. 50 | "-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds. 51 | "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. 52 | "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. 53 | "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. 54 | "-Ywarn-dead-code", // Warn when dead code is identified. 55 | "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined. 56 | "-Ywarn-numeric-widen", // Warn when numerics are widened. 57 | "-Ywarn-unused:implicits", // Warn if an implicit parameter is unused. 58 | "-Ywarn-unused:imports", // Warn if an import selector is not referenced. 59 | "-Ywarn-unused:locals", // Warn if a local definition is unused. 60 | "-Ywarn-unused:params", // Warn if a value parameter is unused. 61 | "-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused. 62 | "-Ywarn-unused:privates", // Warn if a private member is unused. 63 | "-Ywarn-value-discard", // Warn when non-Unit expression results are unused. 64 | "-Ybackend-parallelism", 65 | "8", // Enable paralellisation — change to desired number! 66 | "-Ycache-plugin-class-loader:last-modified", // Enables caching of classloaders for compiler plugins 67 | "-Ycache-macro-class-loader:last-modified" // and macro definitions. This can lead to performance improvements. 68 | ) 69 | 70 | val scalacOptions212 = Seq( 71 | "-deprecation", // Emit warning and location for usages of deprecated APIs. 72 | "-encoding", 73 | "utf-8", // Specify character encoding used by source files. 74 | "-feature", // Emit warning and location for usages of features that should be imported explicitly. 75 | "-language:implicitConversions", // Allow definition of implicit functions called views 76 | "-language:higherKinds" // Allow higher-kinded types 77 | ) 78 | 79 | } 80 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/resources/datatables/DataTableType.feature: -------------------------------------------------------------------------------- 1 | Feature: As Cucumber Scala, I want to parse properly Datatables and use types and transformers 2 | 3 | Scenario: Using a DataTableType for Entry - JList 4 | Given the following authors as entries 5 | | name | surname | famousBook | 6 | | Alan | Alou | The Lion King | 7 | | Robert | Bob | Le Petit Prince | 8 | When I concat their names 9 | Then I get "Alan,Robert" 10 | 11 | Scenario: Using a DataTableType for Entry with empty values - JList 12 | Given the following authors as entries with empty 13 | | name | surname | famousBook | 14 | | Alan | Alou | The Lion King | 15 | | [empty] | Bob | Le Petit Prince | 16 | When I concat their names 17 | Then I get "Alan," 18 | 19 | Scenario: Using a DataTableType for Entry with empty values - DataTable 20 | Given the following authors as entries with empty, as table 21 | | name | surname | famousBook | 22 | | Alan | Alou | The Lion King | 23 | | [empty] | Bob | Le Petit Prince | 24 | When I concat their names 25 | Then I get "Alan," 26 | 27 | Scenario: Using a DataTableType for Entry with Option values - DataTable 28 | Given the following authors as entries with null, as table 29 | | name | surname | famousBook | 30 | | [empty] | Alou | The Lion King | 31 | | | Bob | Le Petit Prince | 32 | When I concat their names 33 | Then I get ",NoName" 34 | 35 | Scenario: Using a DataTableType for Row - Jlist 36 | Given the following authors as rows 37 | | Alan | Alou | The Lion King | 38 | | Robert | Bob | Le Petit Prince | 39 | When I concat their names 40 | Then I get "Alan,Robert" 41 | 42 | Scenario: Using a DataTableType for Row, with empty values - Jlist 43 | Given the following authors as rows with empty 44 | | Alan | Alou | The Lion King | 45 | | [empty] | Bob | Le Petit Prince | 46 | When I concat their names 47 | Then I get "Alan," 48 | 49 | Scenario: Using a DataTableType for Row, with empty values - DataTable 50 | Given the following authors as rows with empty, as table 51 | | Alan | Alou | The Lion King | 52 | | [empty] | Bob | Le Petit Prince | 53 | When I concat their names 54 | Then I get "Alan," 55 | 56 | Scenario: Using a DataTableType for Row, with Option values - DataTable 57 | Given the following authors as rows with null, as table 58 | | | Alou | The Lion King | 59 | | [empty] | Bob | Le Petit Prince | 60 | When I concat their names 61 | Then I get "NoName," 62 | 63 | Scenario: Using a DataTableType for Cell - Jlist[Jlist] 64 | Given the following authors as cells 65 | | Alan | Alou | The Lion King | 66 | | Robert | Bob | Le Petit Prince | 67 | When I concat their names 68 | Then I get "Alan,Robert" 69 | 70 | Scenario: Using a DataTableType for Cell, with empty values - Jlist[Jlist] 71 | Given the following authors as cells with empty 72 | | Alan | Alou | The Lion King | 73 | | [empty] | Bob | Le Petit Prince | 74 | When I concat their names 75 | Then I get "Alan," 76 | 77 | Scenario: Using a DataTableType for Cell, with empty values - JList[JMap] 78 | Given the following authors as cells with empty, as map 79 | | name | surname | famousBook | 80 | | Alan | Alou | The Lion King | 81 | | [empty] | Bob | Le Petit Prince | 82 | When I concat their names 83 | Then I get "Alan," 84 | 85 | Scenario: Using a DataTableType for Cell, with empty values - DataTable -> asMaps 86 | Given the following authors as cells with empty, as table as map 87 | | name | surname | famousBook | 88 | | Alan | Alou | The Lion King | 89 | | [empty] | Bob | Le Petit Prince | 90 | When I concat their names 91 | Then I get "Alan," 92 | 93 | Scenario: Using a DataTableType for Cell, with Option values - DataTable -> asMaps 94 | Given the following authors as cells with null, as table as map 95 | | name | surname | famousBook | 96 | | | Alou | The Lion King | 97 | | [empty] | Bob | Le Petit Prince | 98 | When I concat their names 99 | Then I get "NoName," 100 | 101 | Scenario: Using a DataTableType for Cell, with empty values - DataTable -> asLists 102 | Given the following authors as cells with empty, as table as list 103 | | Alan | Alou | The Lion King | 104 | | [empty] | Bob | Le Petit Prince | 105 | When I concat their names 106 | Then I get "Alan," 107 | 108 | Scenario: Using a DataTableType for Cell, with Option values - DataTable -> asLists 109 | Given the following authors as cells with null, as table as list 110 | | | Alou | The Lion King | 111 | | [empty] | Bob | Le Petit Prince | 112 | When I concat their names 113 | Then I get "NoName," 114 | 115 | Scenario: Using a DataTableType for DataTable - DataTable 116 | Given the following authors as table 117 | | name | surname | famousBook | 118 | | Alan | Alou | The Lion King | 119 | | Robert | Bob | Le Petit Prince | 120 | When I concat their names 121 | Then I get "Alan,Robert" 122 | 123 | Scenario: Using a DataTableType for DataTable with empty values - DataTable 124 | Given the following authors as table with empty 125 | | name | surname | famousBook | 126 | | Alan | Alou | The Lion King | 127 | | [empty] | Bob | Le Petit Prince | 128 | When I concat their names 129 | Then I get "Alan," 130 | -------------------------------------------------------------------------------- /integration-tests/common/src/test/scala/cukes/StepDefs.scala: -------------------------------------------------------------------------------- 1 | package cukes 2 | 3 | import cukes.model.{Cukes, Person, Snake} 4 | import java.util.{List => JList, Map => JMap} 5 | 6 | import io.cucumber.datatable.DataTable 7 | import io.cucumber.scala.{EN, ScalaDsl} 8 | import org.junit.jupiter.api.Assertions.assertEquals 9 | 10 | import scala.annotation.nowarn 11 | import scala.jdk.CollectionConverters._ 12 | 13 | /** Test step definitions to exercise Scala cucumber 14 | */ 15 | @nowarn 16 | class CukesStepDefinitions extends ScalaDsl with EN { 17 | 18 | var calorieCount = 0.0 19 | var intBelly: Int = 0 20 | var longBelly: Long = 0L 21 | var stringBelly: String = "" 22 | var doubleBelly: Double = 0.0 23 | var floatBelly: Float = 0.0f 24 | var shortBelly: Short = 0.toShort 25 | var byteBelly: Byte = 0.toByte 26 | var bigDecimalBelly: BigDecimal = BigDecimal(0) 27 | var bigIntBelly: BigInt = BigInt(0) 28 | var charBelly: Char = 'A' 29 | var boolBelly: Boolean = false 30 | var snake: Snake = null 31 | var person: Person = null 32 | var cukes: JList[Cukes] = null 33 | var gin: Int = 13 34 | var vermouth: Int = 42 35 | var maritinis: Int = 0 36 | 37 | Given("""I have {} {string} in my belly""") { (howMany: Int, what: String) => 38 | } 39 | 40 | Given("""^I have the following foods :$""") { (table: DataTable) => 41 | val maps: JList[JMap[String, String]] = 42 | table.asMaps(classOf[String], classOf[String]) 43 | calorieCount = 44 | maps.asScala.map(_.get("CALORIES")).map(_.toDouble).fold(0.0)(_ + _) 45 | } 46 | And("""have eaten {double} calories today""") { (calories: Double) => 47 | assertEquals(calories, calorieCount, 0.0) 48 | } 49 | 50 | Given("""I have eaten an int {int}""") { (arg0: Int) => 51 | intBelly = arg0 52 | } 53 | Then("""^I should have one hundred in my belly$""") { () => 54 | assertEquals(100, intBelly) 55 | } 56 | 57 | Given("""I have eaten a long {long}""") { (arg0: Long) => 58 | longBelly = arg0 59 | } 60 | Then("""^I should have long one hundred in my belly$""") { () => 61 | assertEquals(100L, longBelly) 62 | } 63 | 64 | Given("""^I have eaten "(.*)"$""") { (arg0: String) => 65 | stringBelly = arg0 66 | } 67 | Then("""^I should have numnumnum in my belly$""") { () => 68 | assertEquals("numnumnum", stringBelly) 69 | } 70 | 71 | Given("""I have eaten {double} doubles""") { (arg0: Double) => 72 | doubleBelly = arg0 73 | } 74 | Then("""^I should have one and a half doubles in my belly$""") { () => 75 | assertEquals(1.5, doubleBelly, 0.0) 76 | } 77 | 78 | Given("""I have eaten {} floats""") { (arg0: Float) => 79 | floatBelly = arg0 80 | } 81 | Then("""^I should have one and a half floats in my belly$""") { () => 82 | assertEquals(1.5f, floatBelly, 0.0) 83 | } 84 | 85 | Given("""I have eaten a short {short}""") { (arg0: Short) => 86 | shortBelly = arg0 87 | } 88 | Then("""^I should have short one hundred in my belly$""") { () => 89 | assertEquals(100.toShort, shortBelly) 90 | } 91 | 92 | Given("""I have eaten a byte {byte}""") { (arg0: Byte) => 93 | byteBelly = arg0 94 | } 95 | Then("""^I should have two byte in my belly$""") { () => 96 | assertEquals(2.toByte, byteBelly) 97 | } 98 | 99 | Given("""I have eaten {bigdecimal} big decimals""") { 100 | (arg0: java.math.BigDecimal) => 101 | bigDecimalBelly = arg0 102 | } 103 | Then("""^I should have one and a half big decimals in my belly$""") { () => 104 | assertEquals(BigDecimal(1.5), bigDecimalBelly) 105 | } 106 | 107 | Given("""I have eaten {biginteger} big int""") { 108 | (arg0: java.math.BigInteger) => 109 | bigIntBelly = arg0.intValue() 110 | } 111 | Then("""^I should have a ten big int in my belly$""") { () => 112 | assertEquals(BigInt(10), bigIntBelly) 113 | } 114 | 115 | Given("""I have eaten char '{char}'""") { (arg0: Char) => 116 | charBelly = 'C' 117 | } 118 | Then("""^I should have character C in my belly$""") { () => 119 | assertEquals('C', charBelly) 120 | } 121 | 122 | Given("""I have eaten boolean {boolean}""") { (arg0: Boolean) => 123 | boolBelly = arg0 124 | } 125 | Then("""^I should have truth in my belly$""") { () => 126 | assertEquals(true, boolBelly) 127 | } 128 | 129 | Given("""I have a table the sum of all rows should be {int} :""") { 130 | (value: Int, table: DataTable) => 131 | assertEquals( 132 | value, 133 | table 134 | .asList(classOf[String]) 135 | .asScala 136 | .drop(1) 137 | .map(String.valueOf(_: String).toInt) 138 | .foldLeft(0)(_ + _) 139 | ) 140 | } 141 | 142 | Given("""I see in the distance ... {snake}""") { (s: Snake) => 143 | snake = s 144 | } 145 | Then("""^I have a snake of length (\d+) moving (.*)$""") { 146 | (size: Int, dir: String) => 147 | assertEquals(size, snake.length) 148 | assertEquals(Symbol(dir), snake.direction) 149 | } 150 | 151 | Given("""I have a person {person}""") { (p: Person) => 152 | person = p 153 | } 154 | 155 | Then("""^he should say \"(.*)\"""") { (s: String) => 156 | assertEquals(person.hello, s) 157 | } 158 | 159 | Given("^I have eaten the following cukes$") { (cs: JList[Cukes]) => 160 | cukes = cs 161 | } 162 | 163 | Then("""I should have eaten {int} cukes""") { (total: Int) => 164 | assertEquals(total, cukes.asScala.map(_.number).sum) 165 | } 166 | 167 | And("^they should have been (.*)$") { (colors: String) => 168 | assertEquals(colors, cukes.asScala.map(_.color).mkString(", ")) 169 | } 170 | 171 | Given("^I drink gin and vermouth$") { () => 172 | gin = 13 173 | vermouth = 42 174 | } 175 | 176 | When("^I shake my belly$") { // note the lack of () => 177 | maritinis += vermouth * gin 178 | } 179 | 180 | Then("^I should have lots of martinis$") { () => 181 | assertEquals(13 * 42, maritinis) 182 | } 183 | } 184 | 185 | @nowarn 186 | class ThenDefs extends ScalaDsl with EN { 187 | Then("""^I am "([^"]*)"$""") { (arg0: String) => } 188 | } 189 | --------------------------------------------------------------------------------