cucumber-js
cucumber-js copied to clipboard
[Proposal] Enable use of Enums in Cucumber Expressions / Steps
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:
-
The Cucumber Expression
- Was already possible with
defineParameterTypebut thisregisterTypessyntax makes things less verbose There's now no concept of transformer, name and regex, just an enum
- Was already possible with
-
The parameter type (new!)
- User extrapolates type by doing
keyof typeof MyEnum
- User extrapolates type by doing
-
-
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
registerTypestakes an string-object Record, users will be able to parse huge data sets in JSON with less verbosity thandefineParameterType(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) });
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.
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
}))
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.
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
Adding more eyes on this @aslakhellesoy @mattwynne
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.
I'm confused about why this is useful. Can I see a more realistic example?
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?
- Makes working with many multiple choice parameter data simpler by declaring and re-using the options as types
- Simplifies the use of
defineParameterTypefor simple cases - Makes parameters offer type-safety when doing comparisons in steps.