/** * 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, };