UNPKG

harmonyc

Version:

Harmony Code - model-driven BDD for Vitest

213 lines (212 loc) 7.32 kB
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('_') || '')); }