amiceli / vitest-cucumber

Use gherkin in your unit tests with vitest
https://vitest-cucumber.miceli.click
40 stars 5 forks source link

Proposition: Enhanced parameter expression #141

Open Odonno opened 6 days ago

Odonno commented 6 days ago

There are already a number of built-in parameter expressions in this package, listed in the documentation here: https://vitest-cucumber.miceli.click/features/step-expression

The proposition is to review and add new expressions to this list.

Review of existing expressions

The existing {number} expression is great. However, I am not really a fan of it being able to convert with symbols like $ or . There is a proposition in the New expressions section for this kind of expressions.

Also, the {list} expression is an interesting idea. In some occasions, the list can be written using a different separator pattern. The question is, what if we can define the separator in the expression like {list:';'} for example. So it can be used for a scenario like this one:

Scenario: scenario with expression
  Given I download a CSV file
  Then The header line is ID;NAME;ADDRESS

Of course, the {list} expression still works, having the , separator acting as default.

New expressions

boolean

A boolean checks when a text is equal to true or false. It will then convert the value into a boolean type.

Example:

Then(`the result is {boolean}`);

will be triggered by

Scenario: scenario with expression
  Then the result is true

Question: should it also parse integers/numbers? 0 being false, true otherwise.

int

A particular case of {number} expression. Similar to {float} but with it only being triggered by natural and non-natural integers, e.g. 14 or -1. It will then convert the value into a number type.

Example:

Given(`I have {int} apples`);

will be triggered by

Scenario: scenario with expression
  Given I have 4 apples

Question: should {float} be removed in favor of {number} as the two would do the same thing?

Question 2: should we also offer {positiveInt} or {naturalInt} (whatever the name) to be triggered only by natural integers?

word

Matches a single word without whitespace. It will then convert the value into a string type.

Example:

Then(`the catalog is full of {word}`);

will be triggered by

Scenario: scenario with expression
  Then the catalog is full of t-shirts

char

Matches a single character. It will then convert the value into a string type.

Example:

Then(`I got an {char} grade`);

will be triggered by

Scenario: scenario with expression
  Then I got an A grade

date

Expects a string to be parsed as a valid date. It will then convert the value into a Date type.

Example:

Then(`the order was created at {date}`);

will be triggered by

Scenario: scenario with expression
  Then the order was created at 2024-10-16

It should support the same parsing as the native Date js type, so basically the previous example will do new Date("2024-10-16").

currency

Matches a currency string that have both a value and the corresponding currency symbol. Bonus point if it can handle currency unit and/or name, e.g. $14.34, 4 USD or even 5 euros.

Then(`I have {currency} in my wallet`);

will be triggered by

Scenario: scenario with expression
  Then I have 14€ in my wallet

Note: the parsecurrency package seems perfect for this use case, see https://www.npmjs.com/package/parsecurrency

empty, string or any

Matches any string (regex being (.*)). For information, the SpecFlow equivalent is empty but any feels more elegant and consistent with the existing TypeScript types. Also note that string can be a valid name. It will then convert the value into a custom object type having at least the raw string value, the number value, the currency unit and/or symbol.

Example:

Then(`I received the following message: {any}`);

will be triggered by

Scenario: scenario with expression
  Then I received the following message: Warning! You have exceeding the quota of the day.

Note: {string}, {any} and {empty} can be aliases if both are considered valid keywords.

Custom expressions

Having the ability to create custom expressions from regex can be interesting especially in TypeScript where you can define union types, whether having an union of different strings or even an union of different types.

This feature is already implemented in other libraries with a function called defineParameterType, see https://github.com/cucumber/cucumber-expressions?tab=readme-ov-file#javascript--typescript

import { defineParameterType } from '@cucumber/cucumber'

defineParameterType({
    name: 'color',
    regexp: /red|blue|yellow/,
    transformer: s => new Color(s)
})

This new parameter expression can then be used in a scenario step:

Scenario: scenario with expression
  Then I have a {color} ball

Default culture

A main pain point when working with some parameter expressions is it being valid with the current culture/language. The most common one being the {date} expression when a date can be recognized as MM-JJ-AAAA format (US, the default one) when in general we expect it to be in the JJ-MM-AAAA format (outside the US).

Either set a culture or language property to change the default behavior of some parameter expressions. Also having the possibility to set a global configured property and one per feature/scenario.

date formats

Example:

Then(`the order was created at {date}`);

will be triggered by

Scenario: scenario with expression
  Then the order was created at 01-02-2024

Given the default culture/language, it will create a native js Date January the 2nd (US format, default) or February the 1st (e.g. FR format).

boolean formats

US example

Given the language is either not set, or is set to en or the culture is set to en-US.

Then(`the result is {boolean}`);

will be triggered by

Scenario: scenario with expression
  Then the result is true

FR example

Given the language is set to fr or the culture is set to fr-FR.

Then(`la proposition est {boolean}`);

will be triggered by

Scenario: scenario with expression
  Then la proposition est vraie

In french, the boolean can be translated to

Odonno commented 4 days ago

@amiceli Can you review this proposition or do you agree with it?

I think we can split this in at least 2 or 3 PRs: the culture/language part and the rest.

amiceli commented 4 days ago

Yes, we will split in multiple PR.

To be brief :

For {int}, I think we can keep just {number} and remove {float} (I don't remember why I did that ^^). We need to improve {numbr} regex for it, what do you think ?

Custom expressions is planned, it's useful. We can add it in configuration or another global function like addStepExpression.

We can see a PR by expression/feature.