Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Custom Cucumber World #113

Closed
ludmilanesvitiy opened this issue Nov 15, 2018 · 27 comments
Closed

Support Custom Cucumber World #113

ludmilanesvitiy opened this issue Nov 15, 2018 · 27 comments

Comments

@ludmilanesvitiy
Copy link

Need to have possibilities to override or extend Custom Cucumber World.
https://docs.cucumber.io/cucumber/state/#world-object
It'll give us the next possibilities:

  • extend world with any additional services, which help developers make scenarios and features independent
  • get any additional unique data from world constructor available in any step, like given/when/then

As an example of usage: developer want to create unique entities for each scenario and he wants to use the name of this entity in each step of a specific scenario. In this case, the developer can extend Cucumber World with specific name service, which will generate a name for an entity and this name will be accessible from any step inside the scenario.

Example of such usage you can find in this repository:
https://github.com/ludmilanesvitiy/QaDay-TestProject/
https://github.com/ludmilanesvitiy/QaDay-TestProject/blob/master/e2e/support/world.ts

If you'll need any additional info - don't hesitate to ask.
Thank you for your work! I really appreciate it)

@kpturner
Copy link

Clearly this request is going nowhere so what have other people done to work around it and make use of the World object with this library?

@badeball
Copy link
Owner

All your step definitions, before and beforeEach blocks are invoked with the same this context, which you can do whatever you want with. What doesn't this solve?

@kpturner
Copy link

kpturner commented Jan 16, 2021

As per the OP, the idea is that you can create your own custom world object with its own properties, methods and it is this object that gets passed to the step definitions and hooks, rather than an empty object. There are lots of benefits, which is why cucumber provides the facility to create customisations: https://github.com/cucumber/cucumber-js/blob/master/docs/support_files/world.md

It may well be possible already with this library but I have not been able to get it to work.

@badeball
Copy link
Owner

Again, I fail to see how this isn't already possible. See below for an example. Here I've implemented the same example outlined in cucumber-js' docs.

# cypress/integration/simple_math.feature
Feature: Simple maths
  In order to do maths
  As a developer
  I want to increment variables

  Scenario: easy maths
    Given a variable set to 1
    When I increment the variable by 1
    Then the variable should contain 2

  Scenario Outline: much more complex stuff
    Given a variable set to <var>
    When I increment the variable by <increment>
    Then the variable should contain <result>

    Examples:
      | var | increment | result |
      | 100 |         5 |    105 |
      |  99 |      1234 |   1333 |
      |  12 |         5 |     17 |
// cypress/support/index.js
beforeEach(function () {
  // This mimics setWorldConstructor of cucumber-js.
  Object.assign(this, {
    variable: 0,

    setTo(number) {
      this.variable = number;
    },
  
    incrementBy(number) {
      this.variable += number;
    }
  })
});
// cypress/support/step_definitions/index.js
Given("a variable set to {int}", function(number) {
  this.setTo(number);
});

When("I increment the variable by {int}", function(number) {
  this.incrementBy(number);
});

Then("the variable should contain {int}", function(number) {
  expect(this.variable).to.equal(number);
});

@kpturner
Copy link

That very simplistic example is, of course, possible - but is "this" providing you with an isolated context for each scenario? Is "this" already an instance of cucumber's "world" that already knows about "attach", "log" and "CLI parameters"? If "this" is truly an instance of cucumbers "world" then it kinda begs the question: why not also allow it to be overridden in the way that cucumber does as per the OPs request? If it is not an instance of World, then it means that users of the library have to contrive their own mechanism to replicate it. I am actually using Typescript (https://github.com/TheBrainFamily/cypress-cucumber-typescript-example) and in a truly typesafe application overriding "this" is not the right way to go (although there are ways to do it).

Don't get me wrong - I fully appreciate this library and the typescript augmentation. It has saved me a lot of pain integrating Cypress and Cucumber. I am refactoring a load of cucumber tests that already override World and it looks like I might have to cobble together something that replicates the behaviour. I think that is all the OP (and I) needed to know - although they may have appreciated knowing it back in 2018 :)

@badeball
Copy link
Owner

badeball commented Jan 17, 2021

is "this" providing you with an isolated context for each scenario?

The intended behavior is described here. (Edit: there's no guarantee that Cypress doesn't make any changes to this)

Is "this" already an instance of cucumber's "world" that already knows about "attach", "log" and "CLI parameters"?

No, because this isn't cucumber. This is merely a library that translates something into some other thing that Cypress understands and this might remind you of cucumber, but this isn't actually cucumber. Sure, there's a dependency on cucumber, but that is merely an implementation detail.

If "this" is truly an instance of cucumbers "world"

It's not truly that in any way and it never will be.

Any attempt at implementing setWorldConstructor would simply be an abstraction layer on top of what Cypress' already offer (except perhaps attach, depending on what it does). Unless CLI parameters is already available from Cypress, then there's no real way for this library to provide it either.

@kpturner
Copy link

kpturner commented Jan 17, 2021

OK thanks. I guess my question was kinda rhetorical and, in anticipation of your answer, I have been working on something that provides the functionality I need already. It replicates what I need from the World class but (at the moment) is just imported by each step definition and gets re-instantiated before each scenario. In fact, based on your earlier example, there may be some mileage in not importing it in each step and, instead, assigning it to "this" (by bending some TS rules).

Thanks for the info. I was not aware that this library is only emulating cucumber rather than using it.

@kpturner
Copy link

I have to give up on using this - at least when using https://github.com/TheBrainFamily/cypress-cucumber-typescript-example.

It seems to be randomly available to some hooks and in others it is undefined, and I cannot spot a pattern.

@badeball
Copy link
Owner

That's not intentional. If you can consistently reproduce such error, I recommend you open a separate issue.

@eliezercazares
Copy link

eliezercazares commented Mar 14, 2022

Why is Cucumber World STILL not available on Cypress Cucumber Preprocessor?
Crucial thing to work with parallelization, and any SDET that respects himself will run his Automation with parallelization during releases.
Scandalously omitted totally.

@badeball the solution you provided falls very short of mimicking Cucumber World behavior.

@badeball
Copy link
Owner

Jfc, how do you even manage to be so unconstructive?

@badeball
Copy link
Owner

You have at the very least be able to describe a use case that isn't satisfied using the above-mentioned solution.

@eliezercazares
Copy link

You have at the very least be able to describe a use case that isn't satisfied using the above-mentioned solution.

#113 (comment)

@badeball
Copy link
Owner

I've already said that I fail to see how this isn't already possible.

@TheDeathConqueror
Copy link

@ludmilanesvitiy What I can understand here is that you're looking for some feature where some values from previous step can be passed to rest steps of the same scenario.

If yes and you can switch to another cypress-cucumber framework then cytorus can help. As per their docs, you can set SC (Scenario Context) with variables you need to share between steps.

step('set scenario context', ()=>{
    SC.someVar = "some value"
})
step('check scenario context', ()=>{
    console.log(SC.someVar) //some value
})

@badeball
Copy link
Owner

This is literally what I have outlined in my proposed solution. this is that context object.

@eliezercazares
Copy link

@ludmilanesvitiy What I can understand here is that you're looking for some feature where some values from previous step can be passed to rest steps of the same scenario.

If yes and you can switch to another cypress-cucumber framework then cytorus can help. As per their docs, you can set SC (Scenario Context) with variables you need to share between steps.

step('set scenario context', ()=>{
    SC.someVar = "some value"
})
step('check scenario context', ()=>{
    console.log(SC.someVar) //some value
})

I also found this contribution:

TheBrainFamily/cypress-cucumber-example#10

@go-green
Copy link

go-green commented Apr 1, 2022

Your this... doesn't quite work for me in steps. it gives me "Object is possibly 'undefined'.ts(2532)"

Given("I am logged in", () => {
       this.context().get..................
});

Also in the closed bug above, I have also asked if there a way to inject services into the step classes?

@badeball
Copy link
Owner

badeball commented Apr 1, 2022

I can't really help you more along than I already have. this is the context which lives throughout the scenario and to which you can attach services. If you want to mix TypeScript into this, then you have to give it some hints as to what the context can / will contain.

@eliezercazares
Copy link

eliezercazares commented Apr 1, 2022

Given("I am logged in", () => {
this.context().get..................
});


Step Definition
import { ScenarioWorld } from "../path/to/scenario_world"
Given("I am logged in", async function (this: ScenarioWorld) => {
    // save or retrieve objects with this.myObj =
});

Scenario World "class"
import { World as CucumberWorld } from "@cucumber/cucumber"
export interface ScenarioWorld extends CucumberWorld {
    // your stuff here
}

BTW, in Cypress you can do this in any step definition:

Store data:
cy.wrap(myObjectOrStringOrIntOrWhatever).as('nameOfMyObjectOrStringOrIntOrWhatevs')

Retrieve data:

cy.get('nameOfMyObjectOrStringOrIntOrWhatevs').then(nameOfMyObjectOrStringOrIntOrWhatevs_ => {
console.log(nameOfMyObjectOrStringOrIntOrWhatevs_);
});

@go-green
Copy link

go-green commented Apr 4, 2022

@eliezercazares & @badeball, Thank you for your reply. I am from C# and Specflow background. Yes, I am using types script along with cypress-cucumber-preprocessor. We use DTOs to share the context between steps. I keep all my scenarios context DTOs in a scenario-specific object basket. I want an instance of ObjectInitializer (as context..... in the code below) to get injected within the scenario lifetime so that I can get any context DTO as below. It keeps all DTOs instances in a MAP, initialize, and return instances. Can I use a DI container like TSyringe to inject these dependencies? How do I attach my ObjectInitializer instance to "this" so that I can use it as "this.context.get(UserUnderTest); in any step within the scenario even if the steps are spread across multiple-step classes? I tried below and this is not recognized by step class.

// cypress/support/index.js
Before(() => {
    Object.assign(this, {
        scenarioContext : new ContextObjectInitializer(),
        context()
        {
            return this.scenarioContext;
        }
    })  
});


// cypress/support/ContextObjects/ObjectInitializer.ts
export class ObjectInitializer {

    private context: Map<string, Object>;

    constructor() {

    }

    get<T>(type: (new () => T)) : T {
        let typeName = type.name;
        if (!this.context.has(typeName)) {
            this.context.set(typeName, type)
        }
        return this.context.get(typeName) as T;
    }
}

// cypress/integration/Login/Login.ts

Given("I am logged in as an admin", () => {
       let loginPage = new LoginPage();
       loginPage.navigate();
       let userCredentials = UserCredentials.get().find(c => c.portal == Portal.Admin);
       let userUnderTest = this.context.get<UserUnderTest>(UserUnderTest);
       userUnderTest.credential = userCredentials;
       loginPage.logIn(userCredentials.userName, userCredentials.password);
});```

@badeball
Copy link
Owner

badeball commented Apr 4, 2022

I tried below and this is not recognized by step class.

What do you mean by this? Are you having a JavaScript problem or a TypeScript problem? (And don't say they're a same thing, because that doesn't help me to help you..)

@badeball
Copy link
Owner

badeball commented Apr 4, 2022

Also, you can't use arrow functions when you want to acquire the test context. IE. don't do this

Given("...", () => {});

.. but rather do this

Given("...", function () {});

@kpturner
Copy link

kpturner commented Apr 4, 2022

Your this... doesn't quite work for me in steps. it gives me "Object is possibly 'undefined'.ts(2532)"

This isn't really anything to do with this library/package. That's a common TS error if you are using version 4+ where TS is simply telling you that you are referencing something and it sees no evidence if it being defined anywhere. There are a couple of ways of telling TS to shut up about it. Google TS2532.

@badeball
Copy link
Owner

Due to personal reasons, the previous maintainers of this package are stepping down and handing the reigns over to me, a long-time contributor to the project and a user of it myself. This is a responsibility I'm very excited about. Furthermore, I'd like to thank @lgandecki ++ for all the work that they've done so far.

Read more about the transfer of ownership here.

The repository has however moved and all outstanding issues are being closed. This is not a reflection of the perceived importance of your reported issue. However, if after upgrading to the new version, you still find there to be an issue, feel free to open up another ticket or comment below. Please make sure to read CONTRIBUTING.md before doing so.

@osmolyar
Copy link

osmolyar commented Oct 8, 2024

@badeball thank you for your maintenance of this great library! Re. 'what is a use case in Typescript that is not supported simply by having 'this' available as the context in all step definitions is, for example, as in the following custom world constructor,

`import FilterBusinessOptions from '../../businessOptions/filterBusinessOptions';
import _ from "lodash";

function CustomWorld() {
const options: Context = _.merge(
{},
new FilterBusinessOptions(),
{
page: "",
chart: "",
stackP: []
}
);
this.nav = {};
this.context=options;
this.httpOptions={};
}

export default CustomWorld`

where 'business option' type definitions for the data structures each type of page object (if using page objects) or service method (and therefore step definition calling the service methods) can accept are defined in potentially multiple type definition files. This is very useful for storing the data input to various steps for use in subsequent scenario steps - yes, we could do it using aliases or the setTo() example you provided but for a large application there are multiple type definition files with dozens of data object type definitions, with defaults, that get initialized in the cucumberWorldConstructor, and which would be extremely cumbersome to maintain in a before() hook.

Furthermore, the this.nav object stores instances of the instantiated page classes, and this.stackP maintains a dynamic array of the names of the visited pages (though suppose we could do that with the existing this, but Typescript would show warnings.)

@badeball
Copy link
Owner

badeball commented Oct 8, 2024

[..] which would be extremely cumbersome to maintain in a before() hook.

I'm not even remotely convinced by this. As I said before - any attempt at implementing setWorldConstructor would simply be an abstraction layer on top of what Cypress' already offer. You can still have a file exporting a world object and merge this into this of a hook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants