Skip to content

Commit

Permalink
Merge branch 'missing-steps-option'
Browse files Browse the repository at this point in the history
  • Loading branch information
vitalets committed Oct 18, 2024
2 parents 913c011 + 4e66061 commit d16da25
Show file tree
Hide file tree
Showing 24 changed files with 228 additions and 113 deletions.
3 changes: 2 additions & 1 deletion src/config/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BDDInputConfig } from './types';

export const defaults: Required<
Pick<BDDInputConfig, 'outputDir' | 'verbose' | 'quotes' | 'language'>
Pick<BDDInputConfig, 'outputDir' | 'verbose' | 'quotes' | 'language' | 'missingSteps'>
> = {
outputDir: '.features-gen',
verbose: false,
quotes: 'single',
language: 'en',
missingSteps: 'fail-on-gen',
};
3 changes: 2 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { saveConfigToEnv } from './env';
import { getPlaywrightConfigDir } from './configDir';
import { BDDConfig, BDDInputConfig } from './types';
import { defaults } from './defaults';
import { removeUndefined } from '../utils';

export function defineBddProject(config: BDDInputConfig & { name: string }) {
const { name, ...bddConfig } = config;
Expand All @@ -32,7 +33,7 @@ export function defineBddConfig(inputConfig: BDDInputConfig) {

// eslint-disable-next-line visual/complexity
function getConfig(configDir: string, inputConfig: BDDInputConfig): BDDConfig {
const config = Object.assign({}, defaults, inputConfig);
const config = Object.assign({}, defaults, removeUndefined(inputConfig));

const features = config.features || config.paths;
if (!features) throw new Error(`Please provide 'features' option in defineBddConfig()`);
Expand Down
2 changes: 2 additions & 0 deletions src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export type BDDInputConfig = CucumberConfigDeprecated & {
statefulPoms?: boolean;
/** Disable warnings */
disableWarnings?: DisableWarningsConfig;
/** Behavior for missing step definitions */
missingSteps?: 'fail-on-gen' | 'fail-on-run' | 'skip-scenario';
};

export type BDDConfig = BDDInputConfig &
Expand Down
11 changes: 11 additions & 0 deletions src/features/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Step, PickleStep } from '@cucumber/messages';

export function getStepTextWithKeyword(scenarioStep: Step | undefined, pickleStep: PickleStep) {
// fallback to empty keyword if scenario step is not provided
const keyword = scenarioStep?.keyword || '';
// There is no full original step text in gherkin document.
// Build it by concatenation of keyword and pickle text.
// Cucumber html-formatter does the same:
// https://github.com/cucumber/react-components/blob/27b02543a5d7abeded3410a58588ee4b493b4a8f/src/components/gherkin/GherkinStep.tsx#L114
return `${keyword}${pickleStep.text}`;
}
10 changes: 2 additions & 8 deletions src/gen/bddMetaBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { stringifyLocation } from '../utils';
import { GherkinDocumentQuery } from '../features/documentQuery';
import { indent } from './formatter';
import { PickleWithLocation } from '../features/types';
import { getStepTextWithKeyword } from '../features/helpers';

const TEST_KEY_SEPARATOR = '|';

Expand Down Expand Up @@ -98,14 +99,7 @@ export class BddMetaBuilder {

private buildStepTitleWithKeyword(pickleStep: messages.PickleStep) {
const scenarioStep = this.pickleStepToScenarioStep.get(pickleStep);
// scenario step are undefined for skipped scenarios,
// but we still fill pickleSteps for consistency.
const keyword = scenarioStep?.keyword || '';
// There is no full original step text in gherkin document.
// Build it by concatenation of keyword and text.
// Cucumber html-formatter does the same.
// See: https://github.com/cucumber/react-components/blob/27b02543a5d7abeded3410a58588ee4b493b4a8f/src/components/gherkin/GherkinStep.tsx#L114
return `${keyword}${pickleStep.text}`;
return getStepTextWithKeyword(scenarioStep, pickleStep);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/gen/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class Formatter {
}

missingStep(keywordEng: StepKeyword, text: string) {
return `// missing step: ${keywordEng}(${this.quoted(text)});`;
return `await ${keywordEng}(${this.quoted(text)}); // missing step`;
}

fixtures(lines: string[]) {
Expand Down
13 changes: 8 additions & 5 deletions src/gen/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { relativeToCwd } from '../utils/paths';
import { BDDConfig } from '../config/types';
import { stepDefinitions } from '../steps/registry';
import { saveFileSync } from '../utils';
import { MissingStep } from '../snippets/types';

export class TestFilesGenerator {
private featuresLoader = new FeaturesLoader();
Expand All @@ -32,7 +33,7 @@ export class TestFilesGenerator {
await withExitHandler(async () => {
await Promise.all([this.loadFeatures(), this.loadSteps()]);
this.buildFiles();
this.checkUndefinedSteps();
this.checkMissingSteps();
await this.clearOutputDir();
await this.saveFiles();
});
Expand Down Expand Up @@ -109,10 +110,12 @@ export class TestFilesGenerator {
return `${absOutputPath}.spec.js`;
}

private checkUndefinedSteps() {
const snippets = new Snippets(this.files);
if (snippets.hasUndefinedSteps()) {
snippets.print();
private checkMissingSteps() {
if (this.config.missingSteps !== 'fail-on-gen') return;
const missingSteps: MissingStep[] = [];
this.files.forEach((file) => missingSteps.push(...file.missingSteps));
if (missingSteps.length) {
new Snippets(missingSteps).print();
exit();
}
}
Expand Down
121 changes: 55 additions & 66 deletions src/gen/testFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import { fixtureParameterNames } from '../playwright/fixtureParameterNames';
import { StepKeyword } from '../steps/types';
import { GherkinDocumentQuery } from '../features/documentQuery';
import { ExamplesTitleBuilder } from './examplesTitleBuilder';
import { MissingStep } from '../snippets/types';
import { getStepTextWithKeyword } from '../features/helpers';

type TestFileOptions = {
gherkinDocument: GherkinDocumentWithPickles;
Expand All @@ -46,12 +48,6 @@ type TestFileOptions = {
tagsExpression?: ReturnType<typeof parseTagsExpression>;
};

export type UndefinedStep = {
keywordType: KeywordType;
step: Step;
pickleStep: PickleStep;
};

export class TestFile {
private lines: string[] = [];
private i18nKeywordsMap?: KeywordsMap;
Expand All @@ -60,7 +56,7 @@ export class TestFile {
private gherkinDocumentQuery: GherkinDocumentQuery;
private bddMetaBuilder: BddMetaBuilder;

public undefinedSteps: UndefinedStep[] = [];
public missingSteps: MissingStep[] = [];
public featureUri: string;
public usedStepDefinitions = new Set<StepDefinition>();

Expand Down Expand Up @@ -206,7 +202,9 @@ export class TestFile {
private getBeforeEach(bg: Background, parent: TestNode) {
const node = new TestNode({ name: 'background', tags: [] }, parent);
const title = [bg.keyword, bg.name].filter(Boolean).join(': ');
const { fixtures, lines } = this.getSteps(bg, node.tags);
const { fixtures, lines, hasMissingSteps } = this.getSteps(bg, node.tags);
// for bg we pass parent as node to forceFixme if needed
this.handleMissingStepsInScenario(hasMissingSteps, parent);
return this.formatter.beforeEach(title, fixtures, lines);
}

Expand Down Expand Up @@ -242,7 +240,8 @@ export class TestFile {
if (this.isSkippedByTagsExpression(node)) return [];
this.bddMetaBuilder.registerTest(node, exampleRow.id);
if (this.isSkippedBySpecialTag(node)) return this.formatter.test(node, new Set(), []);
const { fixtures, lines } = this.getSteps(scenario, node.tags, exampleRow.id);
const { fixtures, lines, hasMissingSteps } = this.getSteps(scenario, node.tags, exampleRow.id);
this.handleMissingStepsInScenario(hasMissingSteps, node);
return this.formatter.test(node, fixtures, lines);
}

Expand All @@ -254,7 +253,8 @@ export class TestFile {
if (this.isSkippedByTagsExpression(node)) return [];
this.bddMetaBuilder.registerTest(node, scenario.id);
if (this.isSkippedBySpecialTag(node)) return this.formatter.test(node, new Set(), []);
const { fixtures, lines } = this.getSteps(scenario, node.tags);
const { fixtures, lines, hasMissingSteps } = this.getSteps(scenario, node.tags);
this.handleMissingStepsInScenario(hasMissingSteps, node);
return this.formatter.test(node, fixtures, lines);
}

Expand All @@ -272,29 +272,47 @@ export class TestFile {
});

const stepToKeywordType = mapStepsToKeywordTypes(scenario.steps, this.language);
let hasMissingSteps = false;

// todo: split internal fn later
// eslint-disable-next-line max-statements
const lines = scenario.steps.map((step, index) => {
const keywordType = stepToKeywordType.get(step)!;
const {
keywordEng,
fixtureNames: stepFixtureNames,
line,
pickleStep,
stepConfig,
} = this.getStep(step, keywordType, outlineExampleRowId);

const keywordEng = this.getStepEnglishKeyword(step);
testFixtureNames.add(keywordEng);
stepFixtureNames.forEach((fixtureName) => testFixtureNames.add(fixtureName));
this.bddMetaBuilder.registerStep(step);
// pickleStep contains step text with inserted example values and argument
const pickleStep = this.findPickleStep(step, outlineExampleRowId);
const stepDefinition = findStepDefinition(pickleStep.text, this.featureUri);
if (!stepDefinition) {
hasMissingSteps = true;
return this.handleMissingStep(keywordEng, keywordType, pickleStep, step);
}

this.usedStepDefinitions.add(stepDefinition);
const stepConfig = stepDefinition.stepConfig;

if (isDecorator(stepConfig)) {
decoratorSteps.push({
index,
keywordEng,
pickleStep,
pomNode: stepConfig.pomNode,
});

// for decorator steps, line and fixtureNames are filled later in second pass
return '';
}

return line;
const stepFixtureNames = this.getStepFixtureNames(stepDefinition);
stepFixtureNames.forEach((fixtureName) => testFixtureNames.add(fixtureName));

return this.formatter.step(
keywordEng,
pickleStep.text,
pickleStep.argument,
stepFixtureNames,
);
});

// fill decorator step slots in second pass (to guess fixtures)
Expand All @@ -315,56 +333,23 @@ export class TestFile {
);
});

return { fixtures: testFixtureNames, lines };
}

/**
* Generate step for Given, When, Then
*/

private getStep(step: Step, keywordType: KeywordType, outlineExampleRowId?: string) {
this.bddMetaBuilder.registerStep(step);
// pickleStep contains step text with inserted example values and argument
const pickleStep = this.findPickleStep(step, outlineExampleRowId);
const stepDefinition = findStepDefinition(pickleStep.text, this.featureUri);
const keywordEng = this.getStepEnglishKeyword(step);
if (!stepDefinition) {
this.undefinedSteps.push({ keywordType, step, pickleStep });
return this.getMissingStep(keywordEng, keywordType, pickleStep);
}

this.usedStepDefinitions.add(stepDefinition);

const stepConfig = stepDefinition.stepConfig;
// if (stepConfig.hasCustomTest) this.hasCustomTest = true;

const fixtureNames = this.getStepFixtureNames(stepDefinition);
const line = isDecorator(stepConfig)
? ''
: this.formatter.step(keywordEng, pickleStep.text, pickleStep.argument, fixtureNames);

return {
keywordEng,
fixtureNames,
line,
pickleStep,
stepConfig,
};
return { fixtures: testFixtureNames, lines, hasMissingSteps };
}

private getMissingStep(
private handleMissingStep(
keywordEng: StepKeyword,
keywordType: KeywordType,
pickleStep: PickleStep,
step: Step,
) {
return {
keywordEng,
const { line, column } = step.location;
this.missingSteps.push({
location: { uri: this.featureUri, line, column },
textWithKeyword: getStepTextWithKeyword(step, pickleStep),
keywordType,
fixtureNames: [],
line: this.formatter.missingStep(keywordEng, pickleStep.text),
pickleStep,
stepConfig: undefined,
};
});
return this.formatter.missingStep(keywordEng, pickleStep.text);
}

private findPickleStep(step: Step, exampleRowId?: string) {
Expand Down Expand Up @@ -396,10 +381,8 @@ export class TestFile {
}

private getStepFixtureNames({ stepConfig }: StepDefinition) {
// for decorator steps fixtureNames are defined later in second pass
if (isDecorator(stepConfig)) return [];

// for cucumber-style there is no fixtures arg
// for cucumber-style there is no fixtures arg,
// fixtures are accessible via this.world
if (isCucumberStyleStep(stepConfig)) return [];

return fixtureParameterNames(stepConfig.fn) // prettier-ignore
Expand Down Expand Up @@ -451,4 +434,10 @@ export class TestFile {
scenario,
});
}

private handleMissingStepsInScenario(hasMissingSteps: boolean, node: TestNode) {
if (hasMissingSteps && this.config.missingSteps === 'skip-scenario') {
node.specialTags.forceFixme();
}
}
}
2 changes: 1 addition & 1 deletion src/run/StepInvoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class StepInvoker {
);

if (!stepDefinition) {
throw new Error(`Undefined step: "${text}"`);
throw new Error(`Missing step: ${text}`);
}

return stepDefinition;
Expand Down
Loading

0 comments on commit d16da25

Please sign in to comment.