Upload files to "qkf"

This commit is contained in:
2026-02-08 20:37:34 -06:00
parent aeae7b5828
commit 869d3508ab
5 changed files with 416 additions and 0 deletions

64
qkf/driverFactory.js Normal file
View File

@@ -0,0 +1,64 @@
// qkf/selenium/driverFactory.js
const { Builder } = require("selenium-webdriver");
function envBool(v, def = false) {
if (v == null) return def;
return String(v).toLowerCase() === "true" || String(v) === "1";
}
/**
* Driver factory:
* - Local: uses selenium-webdriver Builder().forBrowser()
* - Remote: if SELENIUM_REMOTE_URL is set, uses .usingServer(url)
*
* Env:
* - QKF_BROWSER=chrome|firefox|MicrosoftEdge
* - QKF_HEADLESS=true|false
* - SELENIUM_REMOTE_URL=http://localhost:4444/wd/hub (optional)
*/
async function createDriver() {
const browser = process.env.QKF_BROWSER || "chrome";
const headless = envBool(process.env.QKF_HEADLESS, false);
const remoteUrl = process.env.SELENIUM_REMOTE_URL || "";
const builder = new Builder().forBrowser(browser);
if (remoteUrl) builder.usingServer(remoteUrl);
// Headless options (best-effort; safe if missing)
try {
if (browser.toLowerCase() === "chrome") {
const chrome = require("selenium-webdriver/chrome");
const opts = new chrome.Options();
if (headless) opts.addArguments("--headless=new");
opts.addArguments("--window-size=1440,900");
builder.setChromeOptions(opts);
} else if (browser.toLowerCase() === "firefox") {
const firefox = require("selenium-webdriver/firefox");
const opts = new firefox.Options();
if (headless) opts.addArguments("-headless");
builder.setFirefoxOptions(opts);
} else if (browser.toLowerCase().includes("edge")) {
const edge = require("selenium-webdriver/edge");
const opts = new edge.Options();
if (headless) opts.addArguments("--headless=new");
opts.addArguments("--window-size=1440,900");
builder.setEdgeOptions(opts);
}
} catch {
// If browser-specific options modules aren't available, continue without them.
}
const driver = await builder.build();
// Default timeouts (adjust as needed)
await driver.manage().setTimeouts({
implicit: 0,
pageLoad: 60000,
script: 30000
});
return driver;
}
module.exports = { createDriver };

135
qkf/loaders.js Normal file
View File

@@ -0,0 +1,135 @@
/**
* qkf/loaders.js
* Loads your existing (non-standard) JS files without modifying them:
* - pages/*.js may contain "export const X = ..." and use "By"
* - step-definitons/*.js may call global WHEN/AND/etc and use qkf/pages globals
*/
const fs = require("fs");
const path = require("path");
const vm = require("vm");
function readText(p) {
return fs.readFileSync(p, "utf-8");
}
function listJsFiles(dir) {
if (!fs.existsSync(dir)) return [];
return fs
.readdirSync(dir)
.filter((f) => f.toLowerCase().endsWith(".js"))
.map((f) => path.join(dir, f))
.sort();
}
function createBy() {
// Try selenium-webdriver's By. If not present, return a safe fallback.
try {
const { By } = require("selenium-webdriver");
return By;
} catch {
return {
id: (v) => ({ using: "id", value: v }),
xpath: (v) => ({ using: "xpath", value: v }),
css: (v) => ({ using: "css", value: v }),
name: (v) => ({ using: "name", value: v }),
};
}
}
/**
* Transforms tiny ESM snippets into CommonJS exports:
* - "export const Login = ..." -> "exports.Login = ..."
* Only handles the patterns present in your zip (simple export const).
*/
function transformEsmExportsToCjs(src) {
return String(src)
.replace(/^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*/gm, "exports.$1 = ");
}
/**
* Runs a JS file in a vm context and returns module.exports/exports.
*/
function runFileInVm(filePath, { injectedGlobals = {}, filenameLabel } = {}) {
const codeRaw = readText(filePath);
const code = transformEsmExportsToCjs(codeRaw);
const module = { exports: {} };
const exports = module.exports;
const sandbox = {
module,
exports,
require,
__filename: filePath,
__dirname: path.dirname(filePath),
console,
// ✅ Node globals your step files may rely on
process,
Buffer,
setTimeout,
clearTimeout,
setInterval,
clearInterval,
...injectedGlobals,
};
vm.createContext(sandbox);
const wrapped = `(function(){\n${code}\n})();`;
vm.runInContext(wrapped, sandbox, {
filename: filenameLabel || filePath,
displayErrors: true,
});
return sandbox.module.exports;
}
/**
* Loads all page selector modules from pagesDir.
* Returns a flat object like { Login: {...}, OtherPage: {...} }
*/
function loadPages(pagesDir) {
const By = createBy();
const files = listJsFiles(pagesDir);
const pages = {};
for (const f of files) {
const exp = runFileInVm(f, { injectedGlobals: { By }, filenameLabel: `pages:${path.basename(f)}` });
Object.assign(pages, exp);
}
return pages;
}
/**
* Loads all step-definition modules from stepDefsDir and executes them.
* Your current step file just calls WHEN/AND directly at top-level, so executing it is enough.
*/
function loadStepDefinitions(stepDefsDir, registry, { qkf, pages } = {}) {
const files = listJsFiles(stepDefsDir);
for (const f of files) {
runFileInVm(f, {
filenameLabel: `steps:${path.basename(f)}`,
injectedGlobals: {
// bind DSL to registry
WHEN: registry.WHEN,
AND: registry.AND,
THEN: registry.THEN,
GIVEN: registry.GIVEN,
BUT: registry.BUT,
// inject the things your step files expect
qkf,
...pages, // makes "Login" available directly if they do "Login.username"
},
});
}
}
module.exports = {
loadPages,
loadStepDefinitions,
};

76
qkf/qkf.js Normal file
View File

@@ -0,0 +1,76 @@
// qkf/qkf.js
let _impl = null;
// Tracks pending async actions triggered inside a step definition
let _pending = null;
function requireImpl() {
if (!_impl) {
throw new Error("QKF Selenium is not initialized. Did you call runtime.beforeEachTest()?");
}
return _impl;
}
function setImpl(impl) {
_impl = impl;
}
function clearImpl() {
_impl = null;
_pending = null;
}
/**
* Called by the registry before executing a step-definition.
* Resets the pending promise list.
*/
function __beginStep() {
_pending = [];
}
/**
* Called by the registry after the step-definition returns.
* Waits for all async actions triggered during the step.
*/
async function __endStep() {
const pending = _pending || [];
_pending = null;
if (!pending.length) return;
// Ensure we surface failures to Mocha
const results = await Promise.allSettled(pending);
const rejected = results.filter((r) => r.status === "rejected");
if (rejected.length) {
// Throw the first error (keeps stack readable)
throw rejected[0].reason;
}
}
/**
* Track a promise so it can be awaited even if caller forgets `await`.
*/
function track(p) {
if (_pending && p && typeof p.then === "function") _pending.push(p);
return p;
}
const qkf = {
setImpl,
clearImpl,
// internal hooks used by registry
__beginStep,
__endStep,
// public api used by step-definitions
goto(url) { return track(requireImpl().goto(url)); },
enterValue(locator, value) { return track(requireImpl().enterValue(locator, value)); },
click(locator) { return track(requireImpl().click(locator)); },
waitVisible(locator, timeoutMs) { return track(requireImpl().waitVisible(locator, timeoutMs)); },
getText(locator, timeoutMs) { return track(requireImpl().getText(locator, timeoutMs)); },
screenshotBase64() { return track(requireImpl().screenshotBase64()); },
quit() { return track(requireImpl().quit()); }
};
module.exports = qkf;

54
qkf/runtime.js Normal file
View File

@@ -0,0 +1,54 @@
// qkf/runtime.js
const path = require("path");
const qkf = require("./qkf");
const { createStepRegistry } = require("./stepRegistry");
const { loadPages, loadStepDefinitions } = require("./loaders");
const { createDriver } = require("./driverFactory");
const { createQkfSelenium } = require("./selenium/qkfSelenium");
function createRuntime({ rootDir } = {}) {
const base = rootDir || process.cwd();
const pagesDir = path.resolve(base, "pages");
const stepDefsDir = path.resolve(base, "step-definitons");
const pages = loadPages(pagesDir);
// registry is global (step defs registered once)
const registry = createStepRegistry({ qkf, pages });
// registers steps by executing the user's files in vm
loadStepDefinitions(stepDefsDir, registry, { qkf, pages });
async function beforeEachTest() {
const driver = await createDriver();
const impl = createQkfSelenium(driver);
qkf.setImpl(impl);
return { driver };
}
async function afterEachTest({ test, allureAttachment } = {}) {
try {
if (test && test.state === "failed") {
// screenshot as base64 png
const pngBase64 = await qkf.screenshotBase64();
if (typeof allureAttachment === "function") {
// attach raw PNG buffer to Allure
const buf = Buffer.from(pngBase64, "base64");
await allureAttachment("Screenshot (failure)", buf, "image/png");
}
}
} catch (e) {
// don't crash teardown for reporting failures
// console.warn("[qkf] failed to attach screenshot:", e?.message || e);
} finally {
try { await qkf.quit(); } catch {}
qkf.clearImpl();
}
}
return { qkf, pages, registry, beforeEachTest, afterEachTest };
}
module.exports = { createRuntime };

87
qkf/stepRegistry.js Normal file
View File

@@ -0,0 +1,87 @@
// qkf/stepRegistry.js
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function normalizeText(t) {
return String(t || "").trim().replace(/\s+/g, " ");
}
function patternToRegex(pattern) {
const parts = String(pattern).split("{string}");
const escaped = parts.map((p) => escapeRegex(normalizeText(p)));
const joined = escaped.join("\\s+(.+?)\\s+");
const final = "^" + joined.replace(/\s+/g, "\\s+") + "$";
return new RegExp(final, "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 = m.slice(1).map((x) => normalizeText(x));
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 };