vitalets / playwright-bdd

BDD testing with Playwright runner
https://vitalets.github.io/playwright-bdd/
MIT License
287 stars 33 forks source link

Question:converting Regex to Cucumber Regex #133

Closed RichardCariven closed 5 months ago

RichardCariven commented 5 months ago

am continuing to refactor a project from Cucumber Runner to Playwright-BDD, thanks to your help I have all of the Fixture and setup side and element mapping working so am now going through step by step refactoring.

I have a number of steps that look for the nth element or index and also handle both should and should not in the same step. An example of this is given below.

And the "1st" "contact" should be displayed And the "3rd" "contact" should not be displayed The Regex and code for these 2 steps is all in the same step under regex as.

Then( /^the "([0-9]+th|[0-9]+st|[0-9]+nd|[0-9]+rd)" "([^"]*)" should( not)? be displayed$/, async function(this: ScenarioWorld, elementPosition: string, elementKey: ElementKey, negate: boolean) { const { screen: { page }, globalConfig, } = this

    logger.log(the ${elementPosition} ${elementKey} should ${negate?'not ':''}be displayed)

    const elementIdentifier = getElementLocator(page, elementKey, globalConfig)
    const index = Number(elementPosition.match(/\d/g)?.join('')) - 1

    await waitFor(async () => {
            const isElementVisible = await getElementAtIndex(page, elementIdentifier, index) != null
            if (isElementVisible === !negate) {
                return waitForResult.PASS
            } else {
                return waitForResult.ELEMENT_NOT_AVAILABLE
            }
        },

How would I write the above as a Cucumber expression, and make the step work? is it even possible in this format? I did wonder for the nth element would work as per the below but no idea about the should( not)? part.

Then( 'the {string}th/st/nd/rd {string } should( not)? be displayed',

async ({page, globalConfig
       }, elementPosition: string, elementKey: ElementKey, negate: boolean,
       expectedElementText: string)=> {

logger.log(`the ${elementPosition} ${elementKey} should ${negate ? 'not ' : ''}contain the text ${expectedElementText}`)

const elementIdentifier = getElementLocator(page, elementKey, globalConfig)

const index = Number(elementPosition.match(/\d/g)?.join('')) - 1

await waitFor(async () => {
        const elementStable = await waitForSelector(page, elementIdentifier)

        if (elementStable) {
            const elementText = await getElementTextAtIndex(page, elementIdentifier, index)
            if (elementText?.includes(expectedElementText) === !negate) {
                return waitForResult.PASS
            } else {
                return waitForResult.FAIL
            }
        } else {
            return waitForResult.ELEMENT_NOT_AVAILABLE
        }

    },
    globalConfig,
    {
        target: elementKey,
        failureMessage: `🧨 Expected ${elementPosition} ${elementKey} to ${negate ? 'not ' : ''}contain the text ${expectedElementText} 🧨`
    }
)

} )

vitalets commented 5 months ago

You can continue using regex with playwright-bdd. Why do you want to convert it to Cucumber expression?

RichardCariven commented 5 months ago

I have non technical people in my team writing tests (product owners, designers etc), cucumber expressions are easier to teach them as a first pass then regex. So wondered if there was a way of doing this kind of step as a cucumber expression so we don't have a mix of regex and cucumber, but if there is not no problem.

vitalets commented 5 months ago

Fair enough! So, that's possible. For the nth element you can use {int} instead of {string}. For elementKey you correctly used {string}. The most tricky part is should( not). Currently Cucumber expressions can't capture optional text. The solution is to define custom parameter to capture not:

import { defineParameterType } from '@cucumber/cucumber'

defineParameterType({
    name: 'not',
    regexp: /( not)?/,
    transformer: s => Boolean(s)
});

Then('the {int}th/st/nd/rd {string} should{not} be displayed', 
  async ({}, elementPosition: number, elementKey: string, negate: boolean) => {
    console.log(`the ${elementPosition} ${elementKey} should ${negate?'not ':''}be displayed`);
});

Note: Another idea I came while thinking on your question - it would be useful to provide access to step text inside step body. I didn't find such built-in feature in cucumber-js, although it can be achieved with beforeStep hook:

BeforeStep({tags: "@foo"}, function ({ pickleStep }) {
  this.currentStep = pickleStep;
});

In playwright-bdd we can introduce $step fixture, that will hold info about current step. Having such fixture, we can avoid creating custom parameter type and just check step text for negate:

// proposed syntax
Then('the {int}th/st/nd/rd {string} should( not) be displayed', 
  async ({ $step }, elementPosition: number, elementKey: string) => {
    const negate = /should not/.text($step.text);
    console.log(`the ${elementPosition} ${elementKey} should ${negate?'not ':''}be displayed`);
});
RichardCariven commented 5 months ago

thankyou, just to clarify is the step fixture a future improvement your thinking of making, or something that would be possible now? I think that would be a really useful addition.

vitalets commented 5 months ago

Step fixture is a future improvement, for now I suggest to use defineParameterType. Lets keep this issue open for it.

RichardCariven commented 5 months ago

I can confirm defineParameterType worked great, thanks.

vitalets commented 5 months ago

$step fixture released in v6.2.0. Docs