Files
JS-MOCHA-SE-AL/qkf/stepRegistry.js

138 lines
4.2 KiB
JavaScript

// qkf/stepRegistry.js
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function normalizeText(t) {
return String(t || "").trim().replace(/\s+/g, " ");
}
function literalToRegex(lit) {
// Escape + make spaces flexible
return escapeRegex(normalizeText(lit)).replace(/\s+/g, "\\s+");
}
/**
* Pattern supports "{string}" placeholders.
* Matches:
* - "quoted value"
* - 'quoted value'
* - unquoted value (single token or multi-word)
*
* Unquoted capture:
* - if there is a next literal => capture lazily until that literal (lookahead)
* - if last placeholder => capture rest of line
*/
function patternToRegex(pattern) {
const parts = String(pattern).split("{string}");
const litParts = parts.map((p) => literalToRegex(p));
// Each placeholder yields 3 capturing groups: dbl, sgl, unquoted
const QUOTED_OR_UNQUOTED = (nextLiteralRegex) => {
const dbl = `"([^"]+)"`;
const sgl = `'([^']+)'`;
if (nextLiteralRegex && nextLiteralRegex.length > 0) {
// Capture up to the next literal (lazy) without consuming it
// Allow optional whitespace before the next literal
const unq = `(.+?)(?=\\s*${nextLiteralRegex})`;
return `(?:${dbl}|${sgl}|${unq})`;
}
// Last placeholder: capture rest of line
const unqLast = `(.+)`;
return `(?:${dbl}|${sgl}|${unqLast})`;
};
let re = "^\\s*";
for (let i = 0; i < litParts.length; i++) {
const lit = litParts[i];
// Add the literal segment (if any)
if (lit) re += lit;
// If a placeholder follows this literal:
if (i < litParts.length - 1) {
const nextLit = litParts[i + 1];
// Allow whitespace between literal and value
re += "\\s+";
re += QUOTED_OR_UNQUOTED(nextLit);
// IMPORTANT: whitespace after value is optional; next literal (if any) will handle it
re += "\\s*";
}
}
re += "\\s*$";
return new RegExp(re, "i");
}
function createStepRegistry(initialCtx = {}) {
const defs = [];
function register(pattern, fn, meta = {}) {
defs.push({
pattern: String(pattern),
regex: patternToRegex(pattern),
fn,
meta,
});
}
// DSL helpers
function WHEN(pattern, fn) { register(pattern, fn, { keyword: "WHEN" }); }
function AND(pattern, fn) { register(pattern, fn, { keyword: "AND" }); }
function THEN(pattern, fn) { register(pattern, fn, { keyword: "THEN" }); }
function GIVEN(pattern, fn){ register(pattern, fn, { keyword: "GIVEN" }); }
function BUT(pattern, fn) { register(pattern, fn, { keyword: "BUT" }); }
async function run(stepText, extraCtx = {}) {
const text = normalizeText(stepText);
for (const d of defs) {
const m = d.regex.exec(text);
if (!m) continue;
// Each {string} adds 3 capture groups: dbl, sgl, unquoted
const captures = [];
for (let i = 1; i < m.length; i += 3) {
const v = m[i] || m[i + 1] || m[i + 2] || "";
captures.push(normalizeText(v));
}
const ctx = { ...initialCtx, ...extraCtx };
// Auto-await QKF calls (if you implemented these hooks)
if (ctx.qkf && typeof ctx.qkf.__beginStep === "function") ctx.qkf.__beginStep();
try {
const res = d.fn.apply(null, [...captures, ctx]);
await res;
if (ctx.qkf && typeof ctx.qkf.__endStep === "function") {
await ctx.qkf.__endStep();
}
return;
} catch (err) {
try {
if (ctx.qkf && typeof ctx.qkf.__endStep === "function") {
await ctx.qkf.__endStep();
}
} catch (_) {}
throw err;
}
}
const available = defs.map((x) => `- ${x.pattern}`).join("\n");
throw new Error(
`No step definition matched:\n "${text}"\n\nAvailable definitions:\n${available}`
);
}
return { defs, register, run, WHEN, AND, THEN, GIVEN, BUT };
}
module.exports = { createStepRegistry };