112 lines
3.4 KiB
JavaScript
112 lines
3.4 KiB
JavaScript
// qkf/stepRegistry.js
|
|
|
|
function escapeRegex(s) {
|
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function normalizeText(t) {
|
|
return String(t || "").trim().replace(/\s+/g, " ");
|
|
}
|
|
|
|
function patternToRegex(pattern) {
|
|
// Supports:
|
|
// - {string} -> "value" OR 'value' OR bareWord(s)
|
|
// Also normalizes whitespace
|
|
const norm = normalizeText(pattern);
|
|
|
|
// Split around {string}
|
|
const parts = norm.split("{string}").map((p) => escapeRegex(p));
|
|
|
|
// Capture:
|
|
// - "..." or '...' or unquoted token(s) (until end)
|
|
// We make it non-greedy and allow extra spaces.
|
|
const CAPTURE = `(?:"([^"]+)"|'([^']+)'|([^]+?))`;
|
|
|
|
let regexStr = "^";
|
|
for (let i = 0; i < parts.length; i++) {
|
|
regexStr += parts[i];
|
|
if (i < parts.length - 1) {
|
|
// allow flexible whitespace around the capture
|
|
regexStr += "\\s+" + CAPTURE + "\\s+";
|
|
}
|
|
}
|
|
regexStr += "$";
|
|
|
|
return new RegExp(regexStr.replace(/\s+/g, "\\s+"), "i");
|
|
}
|
|
|
|
|
|
function createStepRegistry(initialCtx = {}) {
|
|
const defs = [];
|
|
|
|
function register(pattern, fn, meta = {}) {
|
|
defs.push({
|
|
pattern: String(pattern),
|
|
regex: patternToRegex(pattern),
|
|
fn,
|
|
meta,
|
|
});
|
|
}
|
|
|
|
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;
|
|
|
|
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 };
|
|
|
|
// ✅ Begin step tracking so QKF calls are awaited even if step defs don't `await`
|
|
if (ctx.qkf && typeof ctx.qkf.__beginStep === "function") ctx.qkf.__beginStep();
|
|
|
|
try {
|
|
// Step defs get (...captures, ctx)
|
|
const res = d.fn.apply(null, [...captures, ctx]);
|
|
// Await the step function itself (if it's async)
|
|
await res;
|
|
// ✅ Then await all QKF actions triggered during the step
|
|
if (ctx.qkf && typeof ctx.qkf.__endStep === "function") await ctx.qkf.__endStep();
|
|
return;
|
|
} catch (err) {
|
|
// try to flush pending actions so failures surface cleanly
|
|
try {
|
|
if (ctx.qkf && typeof ctx.qkf.__endStep === "function") await ctx.qkf.__endStep();
|
|
} catch (_) {
|
|
// ignore secondary errors
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
const available = defs.map((d) => `- ${d.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 };
|