UNPKG

taiko

Version:

Taiko is a Node.js library for automating Chromium based browsers

451 lines (426 loc) 12.1 kB
const fs = require("fs-extra"); const path = require("node:path"); const util = require("node:util"); const { aEval } = require("./awaitEval"); const { initSearch } = require("./repl-search"); const { defaultConfig } = require("../config"); const { removeQuotes, symbols, taikoInstallationLocation } = require("../util"); const { EOL } = require("node:os"); const funcs = {}; const commands = []; const stringColor = util.inspect.styles.string; let taikoCommands = []; let lastStack = ""; let version = ""; let browserVersion = ""; let doc = ""; module.exports.initialize = async ( taiko, previousSessionFile, recordedSession, ) => { await setVersionInfo(); const repl = require("node:repl").start({ prompt: "> ", ignoreUndefined: true, preview: false, useGlobal: true, }); repl.writer = writer(repl.writer); if (!recordedSession) { const eventEmitter = taiko.emitter; eventEmitter.on("success", () => { repl.setPrompt(""); process.nextTick(() => { repl.setPrompt("> "); repl.prompt(); }); }); } aEval(repl, (cmd, res) => { if (util.isError(res)) { return res; } commands.push(cmd.trim()); if (isTaikoFunc(cmd) && cmd.includes("goto(")) { return; } return res; }); initTaiko(taiko, repl); initCommands(taiko, repl, previousSessionFile); initSearch(repl); return repl; }; async function setVersionInfo() { const browserPath = process.env.TAIKO_BROWSER_PATH; try { version = require("../../package.json").version; doc = require("../api.json"); if (!browserPath) { browserVersion = `Chromium: ${require("../../package.json").taiko.browser.version}`; } else { browserVersion = browserPath; } } catch (_) { // ignore } displayTaiko(browserPath); } const writer = (w) => (output) => { if (util.isError(output)) { return output.message; } return w(output); }; function initCommands(taiko, repl, previousSessionFile) { repl.defineCommand("highlight", { help: "Customize highlight actions for current session.", async action(arg) { switch (arg) { case "enable": defaultConfig.highlightOnAction = true; break; case "disable": defaultConfig.highlightOnAction = false; break; case "clear": await taiko.clearHighlights(); break; default: break; } this.displayPrompt(); }, }); repl.defineCommand("trace", { help: "Show last error stack trace", action() { console.log( lastStack ? lastStack : util.inspect(undefined, { colors: true }), ); this.displayPrompt(); }, }); repl.defineCommand("code", { help: "Prints or saves the code for all evaluated commands in this REPL session", action(file) { if (!file) { console.log(code()); } else { writeCode(file, previousSessionFile); } this.displayPrompt(); }, }); repl.defineCommand("step", { help: "Generate gauge steps from recorded script. (openBrowser and closeBrowser are not recorded as part of step)", action(file) { if (!file) { console.log(step()); } else { writeStep(file); } this.displayPrompt(); }, }); repl.defineCommand("version", { help: "Prints version info", action() { console.log(`${version} (${browserVersion})`); this.displayPrompt(); }, }); repl.defineCommand("api", { help: "Prints api info", action(name) { if (!doc) { console.log("API usage not available."); } else if (name) { displayUsageFor(name); } else { displayUsage(taiko); } this.displayPrompt(); }, }); repl.on("reset", () => { commands.length = 0; taikoCommands = []; lastStack = ""; }); repl.on("exit", async () => { if (taiko.client()) { await taiko.closeBrowser(); process.exit(); } }); } function code() { if (commands[commands.length - 1].includes("closeBrowser()")) { commands.pop(); } const text = commands .map((e) => { const _e = e.endsWith(";") ? e : `${e};`; return isTaikoFunc(_e) ? ` await ${_e}` : `\t${_e}`; }) .join("\n"); const cmds = taikoCommands; if (!cmds.includes("closeBrowser")) { cmds.push("closeBrowser"); } const importTaiko = cmds.length > 0 ? `const { ${cmds.join(", ")} } = require('taiko');\n` : ""; return `${importTaiko}(async () => { try { ${text} } catch (error) { console.error(error); } finally { await closeBrowser(); } })(); `; } function step(withImports = false, actions = commands) { const _actions = actions.filter( (e, i) => !(i === 0 && e.includes("openBrowser(")) && !(i === actions.length - 1 && e.includes("closeBrowser()")), ); const actionsString = _actions .map((e) => { const _e = e.endsWith(";") ? e : `${e};`; return isTaikoFunc(_e) ? `\tawait ${_e}` : `\t${_e}`; }) .join("\n"); const cmds = taikoCommands.filter( (c) => c !== "openBrowser" && c !== "closeBrowser", ); const importTaiko = cmds.length > 0 ? `const { ${cmds.join(", ")} } = require('taiko');\n` : ""; const step = actionsString ? `\n// Insert step text below as first parameter\nstep("", async function() {\n${actionsString}\n});\n` : ""; return withImports ? `${importTaiko}${step}` : step; } function writeStep(file) { if (fs.existsSync(file)) { fs.appendFileSync(file, step()); } else { fs.ensureFileSync(file); fs.writeFileSync(file, step(true)); } } function writeCode(file, previousSessionFile) { try { if (fs.existsSync(file)) { fs.appendFileSync(file, code()); } else { fs.ensureFileSync(file); fs.writeFileSync(file, code()); } if (previousSessionFile) { console.log(`Recorded session to ${file}.`); if (path.resolve(file) === path.resolve(previousSessionFile)) { console.log( `Please update contents of ${previousSessionFile} before running it with taiko.`, ); } else { console.log( `The previous session was recorded in ${previousSessionFile}.`, ); console.log( `Please merge contents of ${previousSessionFile} and ${file} before running it with taiko.`, ); } } } catch (error) { console.log(`Failed to write to ${file}.`); console.log(error.stacktrace); } } function initTaiko(taiko, repl) { const openBrowser = taiko.openBrowser; taiko.openBrowser = async (options = {}) => { if (!options.headless) { options.headless = false; } return await openBrowser(options); }; addFunctionToRepl(taiko, repl); } function addFunctionToRepl(target, repl) { for (const func in target) { if (target[func].constructor.name === "AsyncFunction") { repl.context[func] = async function () { try { lastStack = ""; // biome-ignore lint/style/noArguments: Not an easy fix const args = await Promise.all(Object.values(arguments)); const res = await target[func].apply(this, args); if (!taikoCommands.includes(func)) { taikoCommands.push(func); } return res; } catch (e) { return handleError(e); } finally { util.inspect.styles.string = stringColor; } }; } else if (target[func].constructor.name === "Function") { repl.context[func] = function () { if (!taikoCommands.includes(func)) { taikoCommands.push(func); } // biome-ignore lint/style/noArguments: Not an easy fix const res = target[func].apply(this, arguments); return res; }; } else if (Object.prototype.hasOwnProperty.call(target[func], "init")) { repl.context[func] = target[func]; if (!taikoCommands.includes(func)) { taikoCommands.push(func); } } funcs[func] = true; } } function warnIfBrowserPathIsNotAFile(browserPath) { if (!browserPath) { return; } try { if (!fs.existsSync(browserPath) || !fs.statSync(browserPath).isFile()) { console.log( `Warning: Please check if TAIKO_BROWSER_PATH (${browserPath})\npoints to a valid browser executable file.\n`, ); } } catch (e) { console.log(e); } } function displayTaiko(browserPath) { console.log(`\nVersion: ${version} (${browserVersion})`); if (doc) { console.log("Type .api for help and .exit to quit\n"); } else { console.log( `\x1b[33mCould not load documentation, please re-generate it by running [node lib/documentation.js] in the directory ${taikoInstallationLocation()}`, ); } warnIfBrowserPathIsNotAFile(browserPath); } function displayUsageFor(name) { const e = doc.find((e) => e.name === name); if (!e) { console.log(`Function ${name} doesn't exist.${EOL}`); return; } if (e.deprecated) { console.log(`${EOL}DEPRECATED ${desc(e.deprecated)}${EOL}`); } console.log(`${desc(e.description)}${EOL}`); if (e.params.length > 0) { console.log(e.params.length > 1 ? "Parameters:" : "Parameter:"); console.log(`${EOL}${params(e.params)}${EOL}`); } if (e.returns) { e.returns.map((e) => { console.log( `Returns: ${type(e.type)} ${ e.description.type ? desc(e.description) : e.description }${EOL}`, ); }); } if (e.examples.length > 0) { console.log(e.examples.length > 1 ? "Examples:" : "Example:"); console.log( e.examples .map((e) => e.description .split("\n") .map((e) => `\t${e}`) .join("\n"), ) .join("\n"), ); } } function displayUsage(taiko) { taiko.metadata.Helpers = taiko.metadata.Helpers.filter( (item) => item !== "repl", ); for (const k in taiko.metadata) { console.log(` ${removeQuotes(util.inspect(k, { colors: true }), k)} ${taiko.metadata[k].join(", ")}`); } console.log(` Run \`.api <name>\` for more info on a specific function. For Example: \`.api click\`. Complete documentation is available at https://docs.taiko.dev `); } function handleError(e) { util.inspect.styles.string = "red"; lastStack = removeQuotes(util.inspect(e.stack, { colors: true }), e.stack); e.message = `${symbols.fail}Error: ${e.message}, run \`.trace\` for more info.`; return new Error( removeQuotes(util.inspect(e.message, { colors: true }), e.message), ); } const desc = (d) => d.children .map((c) => (c.children || []) .map((c1, i) => { if (c1.type === "listItem") { return ( (i === 0 ? "\n\n* " : "\n* ") + c1.children[0].children.map((c2) => c2.value).join("") ); } return ( c1.type === "link" ? c1.children[0].value : c1.value || "" ).trim(); }) .join(" "), ) .join(" "); const type = (t) => { switch (t.type) { case "NameExpression": return t.name; case "OptionalType": return type(t.expression); case "RestType": return `...${type(t.expression)}`; case "TypeApplication": return `${type(t.expression)}<${t.applications.map((a) => type(a)).join(",")}>`; case "UnionType": return `${t.elements.map((t) => type(t)).join("|")}`; } }; const param = (p) => { const name = p.name || ""; const t = p.type ? `${type(p.type)} - ` : ""; const d = (p.description ? desc(p.description) : p.description) || ""; const dft = p.default ? `(optional, default ${p.default})` : ""; return `${name} - ${t}${d} ${dft}${EOL}`; }; const params = (p) => { return p .map( (p) => `* ${param(p)}${ p.properties ? p.properties.map((p) => ` * ${param(p)}`).join("") : "" }`, ) .join(""); }; const isTaikoFunc = (keyword) => keyword.split("(")[0].trim() in funcs;