jest-bdd-generator
Version:
Jest code generator empowered with Gherkin and BDD
171 lines (158 loc) • 7.19 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unused-vars */
import { AstBuilder, GherkinClassicTokenMatcher, Parser /*, compile */ } from '@cucumber/gherkin';
import { GherkinDocument, IdGenerator /*, PickleStep */ } from '@cucumber/messages';
import { JestToGherkin } from '../jest-to-gherkin/JestToGherkin';
import { Step } from '../types';
export * from '../types';
const newId = IdGenerator.uuid();
const builder = new AstBuilder(newId);
const matcher = new GherkinClassicTokenMatcher();
const parser = new Parser(builder, matcher);
export class TestGeneratorFromSource {
/**
* Jest to Gherkin transpiler instance, which compiles TypeScript testing code in Jest.
* `compileKnownStepsFromSource` method fills this property.
*/
transpiler?: JestToGherkin;
/**
* TypeScript testing code in Jest
*/
source!: string;
/**
* Read Gherkin source code and generate GherkinDocument object.
* @param gherkinDoc string of Gherkin soucecode.
* @returns GherkinDocument object.
*/
compileGherkinFromSource(gherkinDoc: string): GherkinDocument {
const gherkinDocument = parser.parse(gherkinDoc);
return gherkinDocument;
// return compile(gherkinDocument, 'uri.feature', newId);
}
/**
* Get steps details from source code. Includes statements, position, accessed variables, defined indentifiers
* @param source TypeScript source code in Jest
* @returns Array of Steps extracted from the source code.
*/
compileKnownStepsFromSource(source: string): JestToGherkin['output'] {
this.source = source;
const _piler = new JestToGherkin();
_piler.fileName = 'pathTestInput.test.ts';
const o = _piler.transpile(source, {
fileName: 'pathTestInput.test.ts'
}).outputText;
this.transpiler = _piler;
return _piler.output;
}
/**
* @deprecated
* Generate Jest testing. Use `generateJestFromGherkin` instead.
* If a step is given source code `statements`, it will use it.
* If there are same step definitions in other scenarios without source code, it will reference them with comments.
* @param steps array of Steps extracted from source code.
* @param gherkinSource source Gherkin text.
* @returns Jest testing code in TypeScript, or undefined if cannot find the Feature definition.
*/
generateGherkinFromSource(steps: Step[], gherkinSource: string): string | undefined {
return this.generateJestFromGherkin(steps, gherkinSource);
}
/**
* Generate Jest testing.
* If a step is given source code `statements`, it will use it.
* If there are same step definitions in other scenarios without source code, it will reference them with comments.
* @param steps array of Steps extracted from source code.
* @param gherkinSource source Gherkin text.
* @returns Jest testing code in TypeScript, or undefined if cannot find the Feature definition.
*/
generateJestFromGherkin(steps: Step[], gherkinSource: string): string | undefined {
const plan = this.compileGherkinFromSource(gherkinSource);
const statements = plan.feature?.children
.map((p) => {
let lastKeyword: string;
const sourceSteps = p.scenario?.steps
.map((pStep) => {
const criteria = pStep.text;
const keyword = pStep.keyword === 'And ' ? lastKeyword : pStep.keyword;
lastKeyword = keyword;
const source = steps.find((s) => s.value === criteria && s.parent === p.scenario?.name); // return s.value.replace(/<[^>]+>/g, '$p') === criteria.replace(/<[^>]+>/g, '$p');
if (source?.sourceCode) {
return [
// `//@${source.key} ${source.value}`,
// ` // import [${source.sourceCode?.imports}]`,
// ` // export [${source.sourceCode?.exports}]`,
` ${(source.sourceCode.statements?.map((a) => a.getFullText()) ?? []).join('')}`
].join('');
}
const sourceInOtherScenario = steps.find((s) => s.value === criteria); // return s.value.replace(/<[^>]+>/g, '$p') === criteria.replace(/<[^>]+>/g, '$p');
if (sourceInOtherScenario) {
return [
`/**`,
`* FOUND: @${keyword}${pStep.text}`,
`* FROM: (Scenario)${sourceInOtherScenario.parent} - [Step]${sourceInOtherScenario.key} ${sourceInOtherScenario.value}`,
`* referenced: [${sourceInOtherScenario.sourceCode?.imports}]`,
`* declared: [${sourceInOtherScenario.sourceCode?.exports}]`,
`* (remove this comments block with: /^\\s*?\\/\\*\\*\\n\\*\\s*?FOUND: @.+\\n(\\*.+\\n)+/g )`,
`${(sourceInOtherScenario.sourceCode?.statements?.map((a) => a.getFullText()) ?? []).join(
''
)}`,
`*/`
].join('\n');
}
return [
`//@${keyword}${pStep.text}`,
` // TODO: implement assertions here;`,
` return 'pending';`,
``
].join(' \n');
})
.join('\n');
let fnTitle = 'it';
let sourceParams: string[] = [];
if (p.scenario?.examples) {
const exp = p.scenario.examples
.map((ex) => {
const ret: string[] = [];
sourceParams = ex.tableHeader?.cells.map((cell) => cell.value) ?? [];
ex.tableBody.forEach((line) => {
const row: Record<string, string> = {};
line.cells.forEach((v, i) => {
row[sourceParams[i]] = JSON.parse(v.value);
});
const lineExpression = Object.keys(row)
.map((k) => {
const valueExpression = JSON.stringify(row[k]);
if (k.match(/^[a-zA-Z][0-9a-zA-Z]*?$/)) {
return `${k}: ${valueExpression}`;
} else {
return `"${k}": ${valueExpression}`;
}
})
.join(', ');
ret.push(`{${lineExpression}}`);
});
return ret.join(',\n ');
})
.join('');
fnTitle = `test.each([\n ${exp}\n])`;
sourceParams = sourceParams.map((strVariable) => {
if (strVariable.match(/^[a-zA-Z][0-9a-zA-Z]*?$/)) {
return strVariable;
} else {
return `'${strVariable}': ${strVariable.replace(/-(\w)/g, (x) => x[1].toUpperCase())}`;
}
});
}
return `${fnTitle}('${p.scenario?.name}', async ({ ${sourceParams} }) => {\n${sourceSteps}\n});`;
})
.flat()
.join('\n');
const feature = steps.find((s) => s.key === 'Feature' && s.value === plan.feature?.name);
if (feature?.sourceCode?.statements) {
const contents = feature.sourceCode.statements;
const head = this.source.substring(0, contents[0].getFullStart());
const tail = this.source.substring(contents[contents.length - 1].end);
return [head, statements, tail].join('\n');
} else {
return statements;
}
}
}