harmonyc
Version:
Harmony Code - model-driven BDD for Vitest
213 lines (212 loc) • 7.32 kB
JavaScript
import { basename } from 'path';
import { Arg, Response, Word, } from "../model/model.js";
export class VitestGenerator {
static error(message, stack) {
return `const e = new SyntaxError(${str(message)});
e.stack = undefined;
throw e;
${stack ? `/* ${stack} */` : ''}`;
}
constructor(tf, sf) {
this.tf = tf;
this.sf = sf;
this.framework = 'vitest';
this.phraseFns = new Map();
this.currentFeatureName = '';
this.resultCount = 0;
this.extraArgs = [];
}
feature(feature) {
const phrasesModule = './' + basename(this.sf.name.replace(/.(js|ts)$/, ''));
const fn = (this.currentFeatureName = pascalCase(feature.name));
this.phraseFns = new Map();
if (this.framework === 'vitest') {
this.tf.print(`import { describe, test, expect } from "vitest";`);
}
if (feature.tests.length === 0) {
this.tf.print('');
this.tf.print(`describe.todo(${str(feature.name)});`);
return;
}
this.tf.print(`import ${fn}Phrases from ${str(phrasesModule)};`);
this.tf.print(``);
for (const item of feature.testGroups) {
item.toCode(this);
}
this.sf.print(`export default class ${pascalCase(feature.name)}Phrases {`);
this.sf.indent(() => {
for (const ph of this.phraseFns.keys()) {
const p = this.phraseFns.get(ph);
const params = p.args.map((a, i) => a.toDeclaration(this, i)).join(', ');
this.sf.print(`async ${ph}(${params}) {`);
this.sf.indent(() => {
this.sf.print(`throw new Error(${str(`Pending: ${ph}`)});`);
});
this.sf.print(`}`);
}
});
this.sf.print(`};`);
}
testGroup(g) {
this.tf.print(`describe(${str(g.name)}, () => {`);
this.tf.indent(() => {
for (const item of g.items) {
item.toCode(this);
}
});
this.tf.print('});');
}
test(t) {
this.resultCount = 0;
this.featureVars = new Map();
// avoid shadowing this import name
this.featureVars.set(new Object(), this.currentFeatureName);
this.tf.print(`test(${str(t.name)}, async (context) => {`);
this.tf.indent(() => {
this.tf.print(`context.task.meta.phrases ??= [];`);
for (const step of t.steps) {
step.toCode(this);
}
});
this.tf.print('});');
}
errorStep(action, errorResponse) {
this.declareFeatureVariables([action]);
this.tf.print(`await expect(async () => {`);
this.tf.indent(() => {
action.toCode(this);
this.tf.print(`context.task.meta.phrases.push(${str(errorResponse.toSingleLineString())});`);
});
this.tf.print(`}).rejects.toThrow(${(errorResponse === null || errorResponse === void 0 ? void 0 : errorResponse.message) !== undefined
? str(errorResponse.message.text)
: ''});`);
}
step(action, responses) {
this.declareFeatureVariables([action, ...responses]);
if (responses.length === 0) {
action.toCode(this);
return;
}
if (action.isEmpty) {
for (const response of responses) {
response.toCode(this);
}
return;
}
const res = `r${this.resultCount++ || ''}`;
this.tf.print(`const ${res} =`);
this.tf.indent(() => {
action.toCode(this);
try {
this.extraArgs = [res];
for (const response of responses) {
response.toCode(this);
}
}
finally {
this.extraArgs = [];
}
});
}
declareFeatureVariables(phrases) {
for (const p of phrases) {
const feature = p.feature.name;
let f = this.featureVars.get(feature);
if (!f) {
f = toId(feature, abbrev, this.featureVars);
this.tf.print(`const ${f} = new ${pascalCase(feature)}Phrases(context);`);
}
}
}
phrase(p) {
const phrasefn = functionName(p);
if (!this.phraseFns.has(phrasefn))
this.phraseFns.set(phrasefn, p);
const f = this.featureVars.get(p.feature.name);
const args = p.args.map((a) => a.toCode(this));
args.push(...this.extraArgs);
if (p instanceof Response && p.parts.length === 1 && p.saveToVariable) {
return this.saveToVariable(p.saveToVariable);
}
this.tf.print(`(context.task.meta.phrases.push(${str(p.toString())}),`);
if (p instanceof Response && p.saveToVariable) {
this.saveToVariable(p.saveToVariable, '');
}
this.tf.print(`await ${f}.${functionName(p)}(${args.join(', ')}));`);
}
setVariable(action) {
this.tf.print(`(context.task.meta.variables ??= {})[${str(action.variableName)}] = ${action.value.toCode(this)};`);
}
saveToVariable(s, what = this.extraArgs[0] + ';') {
this.tf.print(`(context.task.meta.variables ??= {})[${str(s.variableName)}] = ${what}`.trimEnd());
}
stringLiteral(text, { withVariables }) {
if (withVariables && text.match(/\$\{/)) {
return templateStr(text).replace(/\\\$\{([^\s}]+)\}/g, (_, x) => `\${context.task.meta.variables?.[${str(x)}]}`);
}
return str(text);
}
codeLiteral(src) {
return src.replace(/\$\{([^\s}]+)\}/g, (_, x) => `context.task.meta.variables?.[${str(x)}]`);
}
paramName(index) {
return 'xyz'.charAt(index) || `a${index + 1}`;
}
stringParamDeclaration(index) {
return `${this.paramName(index)}: string`;
}
variantParamDeclaration(index) {
return `${this.paramName(index)}: any`;
}
}
function str(s) {
if (s.includes('\n'))
return '\n' + templateStr(s);
let r = JSON.stringify(s);
return r;
}
function templateStr(s) {
return '`' + s.replace(/([`$\\])/g, '\\$1') + '`';
}
function capitalize(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
function toId(s, transform, previous) {
if (previous.has(s))
return previous.get(s);
let base = transform(s);
let id = base;
if ([...previous.values()].includes(id)) {
let i = 1;
while ([...previous.values()].includes(id + i))
i++;
id = base + i;
}
previous.set(s, id);
return id;
}
function words(s) {
return s.split(/[^0-9\p{L}]+/gu);
}
function pascalCase(s) {
return words(s).map(capitalize).join('');
}
function underscore(s) {
return words(s).join('_');
}
function abbrev(s) {
return words(s)
.map((x) => x.charAt(0).toUpperCase())
.join('');
}
export function functionName(phrase) {
const { kind } = phrase;
return ((kind === 'response' ? 'Then_' : 'When_') +
(phrase.parts
.flatMap((c) => c instanceof Word
? words(c.text).filter((x) => x)
: c instanceof Arg
? ['']
: [])
.join('_') || ''));
}