harmonyc
Version:
Harmony Code - model-driven BDD for Vitest
241 lines (240 loc) • 8.27 kB
JavaScript
import { basename } from 'path';
import { xyzab } from "../compiler/compile.js";
import { Arg, Response, Word, } from "../model/model.js";
const X = 'X'.codePointAt(0);
const A = 'A'.codePointAt(0);
export class VitestGenerator {
static error(message, stack) {
return `const e = new SyntaxError(${str(message)});
e.stack = undefined;
throw e;
${stack ? `/* ${stack} */` : ''}`;
}
constructor(tf, sourceFileName, opts) {
this.tf = tf;
this.sourceFileName = sourceFileName;
this.opts = opts;
this.framework = 'vitest';
this.phraseFns = new Map();
this.currentFeatureName = '';
this.phraseMethods = [];
this.resultCount = 0;
this.extraArgs = [];
}
feature(feature) {
const phrasesModule = './' + basename(this.sourceFileName.replace(/\.harmony$/, '.phrases.ts'));
const fn = (this.featureClassName =
this.currentFeatureName =
pascalCase(feature.name) + 'Phrases');
this.phraseFns = new Map();
// test file
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} from ${str(phrasesModule)};`);
this.tf.print(``);
for (const item of feature.testGroups) {
item.toCode(this);
}
this.tf.print(``);
for (const ph of this.phraseFns.keys()) {
const p = this.phraseFns.get(ph);
const parameters = p.args.map((a, i) => {
const declaration = a.toDeclaration(this, i);
const parts = declaration.split(': ');
return {
name: parts[0],
type: parts[1] || 'any',
};
});
if (p instanceof Response) {
parameters.push({
name: 'res',
type: 'any',
});
}
this.phraseMethods.push({
name: ph,
parameters,
});
}
}
testGroup(g) {
this.tf.print(`describe(${str(g.label.text)}, () => {`, g.label.start, g.label.text);
this.tf.indent(() => {
for (const item of g.items) {
item.toCode(this);
}
});
this.tf.print('});');
}
test(t) {
var _a;
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) => {`, (_a = t.lastStrain) === null || _a === void 0 ? void 0 : _a.start, t.testNumber);
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 = this.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);
}
const name = p.toSingleLineString();
this.tf.print(`(context.task.meta.phrases.push(${str(p.toString())}),`);
if (p instanceof Response && p.saveToVariable) {
this.saveToVariable(p.saveToVariable, '');
}
this.tf.printn(`await ${f}.`);
this.tf.write(`${phrasefn}(${args.join(', ')})`, p.start, name);
this.tf.write(`);`);
this.tf.nl();
}
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 xyzab(index);
}
stringParamDeclaration(index) {
return `${this.paramName(index)}: string`;
}
variantParamDeclaration(index) {
return `${this.paramName(index)}: any`;
}
functionName(phrase) {
const { kind } = phrase;
let argIndex = -1;
return ((kind === 'response' ? 'Then_' : 'When_') +
(phrase.parts
.flatMap((c) => c instanceof Word
? words(c.text).filter((x) => x)
: c instanceof Arg
? [this.argPlaceholder(++argIndex)]
: [])
.join('_') || ''));
}
argPlaceholder(i) {
return typeof this.opts.argumentPlaceholder === 'function'
? this.opts.argumentPlaceholder(i)
: this.opts.argumentPlaceholder;
}
}
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('');
}