Upload files to "qkf"
This commit is contained in:
64
qkf/driverFactory.js
Normal file
64
qkf/driverFactory.js
Normal 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
135
qkf/loaders.js
Normal 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
76
qkf/qkf.js
Normal 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
54
qkf/runtime.js
Normal 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
87
qkf/stepRegistry.js
Normal 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 };
|
||||
Reference in New Issue
Block a user