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