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

[Proposal] Enable use of Enums in Cucumber Expressions / Steps #2088

Open
nextlevelbeard opened this issue Jul 15, 2022 · 8 comments
Open

[Proposal] Enable use of Enums in Cucumber Expressions / Steps #2088

nextlevelbeard opened this issue Jul 15, 2022 · 8 comments
Labels
⚡ enhancement Request for new functionality

Comments

@nextlevelbeard
Copy link

nextlevelbeard commented Jul 15, 2022

Run-able Proposal Playground

In its essence, allow for something like this to happen:

// Expose registerTypes to user
import { Then, defineParameterType, registerTypes } from '@cucumber/cucumber';

enum Fruit { 'Apple', Orange, Cucumber }
enum Vegetables { Potato, Eggplant }

// Calls defineParameterType for every Enum thrown in single object parameter
registerTypes({ Fruit, Vegetables })

Then('I should have an? {Fruit} in my basket', async function (fruit: keyof typeof Fruit) {

  // 🎉 fruit parameter is now type-safe! 
  // TypeScript will let you know: This first condition will always return 'false'
  // since the types '"Apple" | "Orange" | "Cucumber"' and '"Worm"' have no overlap.ts(2367)
  
  if(fruit === 'Worm'){
    console.log("Asserting its a Worm ?!")
  }
  
  // We can reference the original enum in comparisons
  if(fruit === Fruit[Fruit.Orange]){
    console.log("Asserting its an orange!")
  }
  
  // Or
  if(fruit === 'Orange']){
    console.log("Asserting its an orange!")
  }
}

To take note:

  • Users don't need to write down their N possible options on neither:

    1. The Cucumber Expression

      • Was already possible with defineParameterType but this registerTypes syntax makes things less verbose
        There's now no concept of transformer, name and regex, just an enum
    2. The parameter type (new!)

      • User extrapolates type by doing keyof typeof MyEnum
  • Users can reference the original enum for comparisons inside the step: fruit === Fruit[Fruit.Orange]

  • Users will get specific string literal type safety instead of generic string safety: fruit === 'Worm' will "error"

  • Because registerTypes takes an string-object Record, users will be able to parse huge data sets in JSON with less verbosity than defineParameterType (no mapping of name, regex, transformer):

    // Retrieved from somewhere
    const data = {
      ProvidersState: {
        PayPal: 'accepted',
        SOFORT: 'unsupported'
      },
      FilmCategoryLabels: {
        Action: 'cool',
        Adventure: 'cooler'
      },
      TestStates: {
        Running: 1,
        Skipped: 0,
        Passed: 2,
        Failed: 3
      }
    }
    
    registerTypes(data)



For reference, here's the original, impossible, type-based proposal:

import { When, defineParameterTypes, registerTypes } from '@cucumber/cucumber';

// Possibly imported and re-used from some library or other glue code
type Fruit = 'Apple' | 'Orange' | 'Mango'

// Not what is proposed, but essentially replaces this defineParameterType
defineParameterType({
  regexp: /Apple|Orange|Mango/,
  transformer(s) { return s },
  name: 'Fruit'
})

// Proposal, register types
registerTypes<Fruit, SecondType, ThirdType>()

When('I add an? {Fruit} to my basket', async function (fruit: Fruit) {
  // Declared as putInBasket(fruit: Fruit) { ... }
  putInBasket(fruit)
});
@davidjgoss
Copy link
Contributor

Can you elaborate on how this might be achieved @nextlevelbeard?

TypeScript types are compiler-only and don't exist at runtime, so registerTypes would have no way of seeing them, unless I'm missing something.

@davidjgoss davidjgoss added ❓ question Consider using support forums: https://cucumber.io/tools/cucumber-open/support 🍼 incomplete Blocked until more information is provided labels Jul 15, 2022
@nextlevelbeard
Copy link
Author

nextlevelbeard commented Jul 25, 2022

I also struggled to find how exactly it could be done, just assumed there might be a way.
After looking into this it indeed seems impossible.

I think an alternative could be allowing the use of TypeScript's enum.

Users could then easily derive the type in the steps with keyof typeof.

import { Then, defineParameterTypes, registerTypes } from '@cucumber/cucumber';

enum Fruit { 'Apple', Orange, Cucumber }
enum Vegetables { Potato, Eggplant }

registerTypes({ Fruit, Vegetables })

Then('I should have an? {Fruit} in my basket', async function (fruit: keyof typeof Fruit) {
  if(fruit === Fruit[Fruit.Orange]){
    // Assert
  }
}

Internally, we could transform the enum string values into a regular expression and define the parameter type

// Internally implement something like this
Object.values(types).forEach(([name, type]) => defineParameterType({
  name: name, // i.e. Fruit
  // Enum /w numeric keys are not possible, we can safely filter keys
  regexp: new RegExp(Object.keys(type).filter(k => !Number(k)).join('|')), // i.e. Apple|Orange|Cucumber
}))

@nextlevelbeard nextlevelbeard changed the title Enable use of String-Union types in Cucumber Expressions Enable use of Enums in Cucumber Expressions Jul 25, 2022
@davidjgoss
Copy link
Contributor

Maybe you could pull together a TS playground or something that shows this working on a core level?

Again I don't think what you want will exist at runtime. enum values will be there (as the ordinal by default but you can make them a string) but the enum declaration won't exist in the JS as far as I know.

@nextlevelbeard
Copy link
Author

nextlevelbeard commented Jul 25, 2022

Here's the playground, you can run it.

We'd have to implement the registerTypes function and export it for users, same as defineParameterType.

Again I don't think what you want will exist at runtime. enum values will be there (as the ordinal by default but you can make them a string) but the enum declaration won't exist in the JS as far as I know.

With enum this works because "Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript". You can check what enums turn into on the playground.

@davidjgoss Let me know your thoughts on this

@nextlevelbeard nextlevelbeard changed the title Enable use of Enums in Cucumber Expressions [Proposal] Enable use of Enums in Cucumber Expressions Jul 28, 2022
@nextlevelbeard nextlevelbeard changed the title [Proposal] Enable use of Enums in Cucumber Expressions [Proposal] Enable use of Enums in Cucumber Expressions / Steps Jul 28, 2022
@nextlevelbeard
Copy link
Author

Adding more eyes on this
@aslakhellesoy @mattwynne

@davidjgoss davidjgoss removed the 🍼 incomplete Blocked until more information is provided label Feb 4, 2023
@davidjgoss
Copy link
Contributor

I'm a bit uneasy about adding something that's geared specially around a TypeScript concept. I think plain old arrays would be fine though e.g.

defineSimpleParameterTypes({
  Fruit: ['Apple', 'Orange', 'Cucumber'],
  Vegetables: ['Potato', 'Eggplant']
})

And with TS it's fairly trivial to define a type from an array so not much extra overhead.

@davidjgoss davidjgoss added ⚡ enhancement Request for new functionality and removed ❓ question Consider using support forums: https://cucumber.io/tools/cucumber-open/support labels Feb 4, 2023
@mattwynne
Copy link
Member

I'm confused about why this is useful. Can I see a more realistic example?

@nextlevelbeard
Copy link
Author

nextlevelbeard commented Feb 20, 2023

I'm a bit uneasy about adding something that's geared specially around a TypeScript concept. I think plain old arrays would be fine though e.g.

This wouldnn't take anything away from existing users, it's just making it simpler working with TS enums.
You can easily add this registerTypes function to any existing setup. But I agree arrays could be a better approach.

The arrays would need to be readonly in order for users to be able to reference the parameter type in the steps.
Would look something like this maybe.

// Proposal, a function that takes an array of strings and defines type parameters
const registerTypes = (types: Record<string, readonly string[]>) => {
  Object.entries(types).forEach(([name, type]) => defineParameterType({
    name: name, // i.e. Fruit
    // Enum /w numeric keys are not possible by TypeScript design, we can safely filter keys
    regexp: new RegExp(type.filter(k => !Number(k)).join('|')), // i.e. Apple|Orange|Cucumber,
    transformer: (s: string) => s
  }))
}

import { Then, registerTypes } from '@cucumber/cucumber';

const Fruit = ['Apple', 'Orange', 'Cucumber'] as const
const Vegetables = ['Potato', 'Eggplant'] as const

registerTypes({ Fruit, Vegetables })

Then('I should have an? {Fruit} in my basket', stepFn = async function (fruit: typeof Fruit[number]) {

  // Type safety, IDE complains about impossible comparisons
  if(fruit === "asd"){

  }
})

I'm confused about why this is useful. Can I see a more realistic example?

  1. Makes working with many multiple choice parameter data simpler by declaring and re-using the options as types
  2. Simplifies the use of defineParameterType for simple cases
  3. Makes parameters offer type-safety when doing comparisons in steps.

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

No branches or pull requests

3 participants