playwright-bdd
Version:
BDD Testing with Playwright runner
218 lines (189 loc) • 7.49 kB
text/typescript
/**
* Class to generate single test from pickle.
*/
import { PickleStep, Step } from '@cucumber/messages';
import { formatDuplicateStepsMessage, StepFinder } from '../../steps/finder';
import { Formatter } from '../formatter';
import { getKeywordEng, KeywordsMap } from '../i18n';
import { getStepTextWithKeyword } from '../../gherkin/helpers';
import { stringifyLocation } from '../../utils';
import { exit } from '../../utils/exit';
import { MatchedStepDefinition } from '../../steps/matchedStepDefinition';
import { PomNode } from '../../steps/decorators/pomGraph';
import { fixtureParameterNames } from '../../playwright/fixtureParameterNames';
import { isBddAutoInjectFixture } from '../../runtime/bddTestFixturesAuto';
import { PickleWithLocation } from '../../gherkin/types';
import { BackgroundGen } from '../background';
import { BDDConfig } from '../../config/types';
import {
isTestSkippedByCollectedTags,
isTestSlowByCollectedTags,
SpecialTags,
} from '../specialTags';
import { DecoratorFixtureResolver } from './decoratorFixtureResolver';
import { getStepHooksFixtureNames, getStepHooksToRun } from '../../hooks/step';
export type StepData = {
pickleStep: PickleStep;
gherkinStep: Step;
location: string; // full location with feature file path
fixtureNames: string[];
matchedDefinition?: MatchedStepDefinition; // empty for missing steps
pomNode?: PomNode; // for decorator steps
pomFixtureName?: string; // for decorator steps
isBg?: boolean;
};
// todo: class StepGen ?
/**
* Generates steps for a single scenario (test) and updates background steps.
*/
export class TestGen {
public stepsData = new Map<string /* pickle step id */, StepData>();
private specialTags: SpecialTags;
public skippedByTag: boolean;
private skippedByMissingSteps = false;
public slow: boolean;
private stepHooksFixtureNames: string[] = [];
// eslint-disable-next-line max-params
constructor(
private config: BDDConfig,
public featureUri: string,
private i18nKeywordsMap: KeywordsMap | undefined,
private stepFinder: StepFinder,
private formatter: Formatter,
private backgrounds: BackgroundGen[],
public pickle: PickleWithLocation,
public testTitle: string,
private scenarioSteps: readonly Step[],
public tags: string[], // all tags of test (including tags from path)
ownTestTags: string[], // tags from scenario (without inherited)
) {
this.specialTags = new SpecialTags(ownTestTags);
this.skippedByTag = isTestSkippedByCollectedTags(this.tags);
this.slow = isTestSlowByCollectedTags(this.tags);
this.fillStepHooksFixtureNames();
this.fillStepsData();
this.resolveFixtureNamesForDecoratorSteps();
}
get ownTimeout() {
return this.specialTags.timeout;
}
get skipped() {
return this.skippedByTag || this.skippedByMissingSteps;
}
render() {
const testFixtureNames: string[] = [];
const stepLines = [...this.stepsData.values()]
.filter((stepData) => this.scenarioSteps.includes(stepData.gherkinStep))
.map((stepData) => {
const { pickleStep, gherkinStep, fixtureNames } = stepData;
const keywordEng = getKeywordEng(this.i18nKeywordsMap, gherkinStep.keyword);
const pickleStepIds = [pickleStep.id];
testFixtureNames.push(keywordEng, ...fixtureNames);
return this.formatter.step(
keywordEng,
pickleStep.text,
pickleStep.argument,
new Set(fixtureNames),
pickleStepIds,
);
});
this.handleMissingDefinitions();
return this.formatter.test(
this.testTitle,
this.tags,
this.specialTags,
new Set(testFixtureNames),
this.pickle.id,
stepLines,
);
}
private fillStepsData() {
this.pickle.steps.forEach((pickleStep) => {
const { gherkinStep, bg } = this.findGherkinStep(pickleStep);
const location = `${this.featureUri}:${stringifyLocation(gherkinStep.location)}`;
const matchedDefinition = this.findMatchedDefinition(pickleStep, gherkinStep);
const fixtureNames = this.getStepFixtureNames(matchedDefinition);
fixtureNames.push(...this.stepHooksFixtureNames);
const pomNode = matchedDefinition?.definition.pomNode;
const stepData: StepData = {
pickleStep,
gherkinStep,
location,
matchedDefinition,
fixtureNames,
pomNode,
isBg: Boolean(bg),
};
this.stepsData.set(pickleStep.id, stepData);
bg?.addStepData(stepData);
});
}
private fillStepHooksFixtureNames() {
const beforeStepHooksToRun = getStepHooksToRun('beforeStep', this.tags);
const afterStepHooksToRun = getStepHooksToRun('afterStep', this.tags);
this.stepHooksFixtureNames = getStepHooksFixtureNames([
...beforeStepHooksToRun,
...afterStepHooksToRun,
]);
}
private handleMissingDefinitions() {
if (
!this.skippedByTag &&
this.hasMissingDefinitions() &&
this.config.missingSteps === 'skip-scenario'
) {
this.skippedByMissingSteps = true;
this.specialTags.forceFixme();
}
}
private findGherkinStep(pickleStep: PickleStep) {
const { bg, bgGherkinStep } = this.findBackgroundStep(pickleStep) || {};
const gherkinStep = bgGherkinStep || this.findScenarioStep(pickleStep);
if (!gherkinStep) exit(`Gherkin step not found for pickle step: ${pickleStep.text}`);
return { gherkinStep, bg };
}
private findBackgroundStep(pickleStep: PickleStep) {
for (const bg of this.backgrounds) {
const bgGherkinStep = bg.findGherkinStep(pickleStep);
if (bgGherkinStep) return { bg, bgGherkinStep };
}
}
private findScenarioStep(pickleStep: PickleStep) {
return this.scenarioSteps.find(({ id }) => pickleStep.astNodeIds.includes(id));
}
private hasMissingDefinitions() {
return [...this.stepsData.values()].some((stepData) => !stepData.matchedDefinition);
}
private findMatchedDefinition(pickleStep: PickleStep, gherkinStep: Step) {
// for skipped tests don't search for definition
if (this.skipped) return;
const matchedDefinitions = this.stepFinder.findDefinitions(
pickleStep.type,
pickleStep.text,
this.tags,
);
if (matchedDefinitions.length === 0) return;
if (matchedDefinitions.length > 1) {
const stepTextWithKeyword = getStepTextWithKeyword(gherkinStep.keyword, pickleStep.text);
const stepLocation = `${this.featureUri}:${stringifyLocation(gherkinStep.location)}`;
// exit immediately, b/c with multiple step definitions we can't proceed
exit(formatDuplicateStepsMessage(matchedDefinitions, stepTextWithKeyword, stepLocation));
}
return matchedDefinitions[0];
}
private resolveFixtureNamesForDecoratorSteps() {
new DecoratorFixtureResolver(this.config, this.tags).resolveFixtureNames(this.stepsData);
}
private getStepFixtureNames(matchedDefinition?: MatchedStepDefinition) {
if (!matchedDefinition) return [];
const { definition } = matchedDefinition;
// for decorator steps fixture names are resolved later,
// when all steps are collected
if (definition.isDecorator()) return [];
// for cucumber-style there is no fixtures arg,
// fixtures are accessible via this.world
if (definition.isCucumberStyle()) return [];
return fixtureParameterNames(definition.fn) // prettier-ignore
.filter((name) => !isBddAutoInjectFixture(name));
}
}